guacamole-client-0.9.9/0040775 0001751 0001751 00000000000 12645510326 016536 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/doc/0040775 0001751 0001751 00000000000 12424302752 017300 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/doc/guacamole-example/0040775 0001751 0001751 00000000000 12645510255 022672 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/doc/guacamole-example/src/0040775 0001751 0001751 00000000000 12424302752 023455 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/doc/guacamole-example/src/main/0040775 0001751 0001751 00000000000 12471273272 024407 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/doc/guacamole-example/src/main/webapp/0040775 0001751 0001751 00000000000 12612226251 025655 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/doc/guacamole-example/src/main/webapp/WEB-INF/0040775 0001751 0001751 00000000000 12612226251 026704 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/doc/guacamole-example/src/main/webapp/META-INF/0040775 0001751 0001751 00000000000 12453071211 027012 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/doc/guacamole-example/src/main/java/0040775 0001751 0001751 00000000000 12471273272 025330 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/doc/guacamole-example/src/main/java/org/0040775 0001751 0001751 00000000000 12471273272 026117 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/doc/guacamole-example/src/main/java/org/glyptodon/0040775 0001751 0001751 00000000000 12471273272 030136 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/doc/guacamole-example/src/main/java/org/glyptodon/guacamole/0040775 0001751 0001751 00000000000 12471273272 032073 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/doc/guacamole-example/src/main/java/org/glyptodon/guacamole/net/0040775 0001751 0001751 00000000000 12471273272 032661 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/doc/guacamole-example/src/main/java/org/glyptodon/guacamole/net/example/0040775 0001751 0001751 00000000000 12645510255 034312 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common-js/0040775 0001751 0001751 00000000000 12645510305 022370 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common-js/doc/0040775 0001751 0001751 00000000000 12424302752 023135 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common-js/src/0040775 0001751 0001751 00000000000 12471273272 023165 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common-js/src/main/0040775 0001751 0001751 00000000000 12471273272 024111 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common-js/src/main/webapp/0040775 0001751 0001751 00000000000 12471273272 025367 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common-js/src/main/webapp/modules/0040775 0001751 0001751 00000000000 12645510255 027035 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common-js/src/main/webapp/common/0040775 0001751 0001751 00000000000 12471273272 026657 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/0040775 0001751 0001751 00000000000 12645510306 020471 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/doc/0040775 0001751 0001751 00000000000 12424302752 021235 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/doc/example/0040775 0001751 0001751 00000000000 12612226251 022666 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/0040775 0001751 0001751 00000000000 12635040357 021263 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/0040775 0001751 0001751 00000000000 12612226251 022201 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/resources/0040775 0001751 0001751 00000000000 12612226251 024213 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/0040775 0001751 0001751 00000000000 12645510255 023465 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/0040775 0001751 0001751 00000000000 12612226251 024237 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/0040775 0001751 0001751 00000000000 12612226251 025467 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/directives/0040775 0001751 0001751 00000000000 12612226251 027630 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/styles/0040775 0001751 0001751 00000000000 12645510255 027020 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/controllers/0040775 0001751 0001751 00000000000 12645510255 030043 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/types/0040775 0001751 0001751 00000000000 12645510255 026641 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/templates/0040775 0001751 0001751 00000000000 12645510255 027473 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/history/0040775 0001751 0001751 00000000000 12612226251 025740 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/history/services/0040775 0001751 0001751 00000000000 12612226251 027563 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/history/types/0040775 0001751 0001751 00000000000 12612226251 027104 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/osk/0040775 0001751 0001751 00000000000 12612226251 025033 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/osk/directives/0040775 0001751 0001751 00000000000 12612226251 027174 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/osk/styles/0040775 0001751 0001751 00000000000 12612226251 026356 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/osk/templates/0040775 0001751 0001751 00000000000 12612226251 027031 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/login/0040775 0001751 0001751 00000000000 12612226251 025347 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/login/directives/0040775 0001751 0001751 00000000000 12612226251 027510 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/login/styles/0040775 0001751 0001751 00000000000 12612226251 026672 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/login/templates/0040775 0001751 0001751 00000000000 12612226251 027345 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/locale/0040775 0001751 0001751 00000000000 12612226251 025476 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/locale/services/0040775 0001751 0001751 00000000000 12645510255 027327 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/0040775 0001751 0001751 00000000000 12612226251 026077 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/services/0040775 0001751 0001751 00000000000 12612226251 027722 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/directives/0040775 0001751 0001751 00000000000 12645510255 030246 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/styles/0040775 0001751 0001751 00000000000 12645510255 027430 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/controllers/0040775 0001751 0001751 00000000000 12612226251 030445 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/types/0040775 0001751 0001751 00000000000 12645510255 027251 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/templates/0040775 0001751 0001751 00000000000 12645510255 030103 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/auth/0040775 0001751 0001751 00000000000 12612226251 025200 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/auth/service/0040775 0001751 0001751 00000000000 12645510255 026646 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/storage/0040775 0001751 0001751 00000000000 12612226251 025703 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/storage/services/0040775 0001751 0001751 00000000000 12612226251 027526 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/touch/0040775 0001751 0001751 00000000000 12612226251 025361 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/touch/directives/0040775 0001751 0001751 00000000000 12612226251 027522 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/0040775 0001751 0001751 00000000000 12612226251 025214 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/services/0040775 0001751 0001751 00000000000 12645510255 027045 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/types/0040775 0001751 0001751 00000000000 12645510255 026366 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/0040775 0001751 0001751 00000000000 12612226251 025515 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/services/0040775 0001751 0001751 00000000000 12645510255 027346 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/directives/0040775 0001751 0001751 00000000000 12645506164 027670 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/styles/0040775 0001751 0001751 00000000000 12645510255 027046 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/controllers/0040775 0001751 0001751 00000000000 12645510255 030071 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/types/0040775 0001751 0001751 00000000000 12645510255 026667 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/templates/0040775 0001751 0001751 00000000000 12645510255 027521 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/groupList/0040775 0001751 0001751 00000000000 12612226251 026227 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/groupList/directives/0040775 0001751 0001751 00000000000 12645510255 030376 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/groupList/types/0040775 0001751 0001751 00000000000 12612226251 027373 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/groupList/templates/0040775 0001751 0001751 00000000000 12645510255 030233 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/0040775 0001751 0001751 00000000000 12612226251 025346 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/styles/0040775 0001751 0001751 00000000000 12645510255 026677 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/controllers/0040775 0001751 0001751 00000000000 12631123302 027706 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/config/0040775 0001751 0001751 00000000000 12645510255 026621 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/list/0040775 0001751 0001751 00000000000 12612226251 025212 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/list/directives/0040775 0001751 0001751 00000000000 12612226251 027353 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/list/styles/0040775 0001751 0001751 00000000000 12612226251 026535 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/list/types/0040775 0001751 0001751 00000000000 12612226251 026356 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/list/templates/0040775 0001751 0001751 00000000000 12612226251 027210 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/home/0040775 0001751 0001751 00000000000 12612226251 025167 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/home/directives/0040775 0001751 0001751 00000000000 12612226251 027330 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/home/styles/0040775 0001751 0001751 00000000000 12631654532 026522 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/home/controllers/0040775 0001751 0001751 00000000000 12645510255 027543 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/home/types/0040775 0001751 0001751 00000000000 12612226251 026333 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/home/templates/0040775 0001751 0001751 00000000000 12645510255 027173 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/element/0040775 0001751 0001751 00000000000 12631001061 025657 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/element/directives/0040775 0001751 0001751 00000000000 12612226251 030031 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/element/styles/0040775 0001751 0001751 00000000000 12612226251 027213 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/element/types/0040775 0001751 0001751 00000000000 12612226251 027034 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/element/templates/0040775 0001751 0001751 00000000000 12612226251 027666 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/0040775 0001751 0001751 00000000000 12612226251 025202 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/services/0040775 0001751 0001751 00000000000 12612226251 027025 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/directives/0040775 0001751 0001751 00000000000 12612226251 027343 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/styles/0040775 0001751 0001751 00000000000 12612226251 026525 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/controllers/0040775 0001751 0001751 00000000000 12612226251 027550 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/types/0040775 0001751 0001751 00000000000 12612226251 026346 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/templates/0040775 0001751 0001751 00000000000 12612226251 027200 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/navigation/0040775 0001751 0001751 00000000000 12612226251 026376 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/navigation/services/0040775 0001751 0001751 00000000000 12645510255 030227 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/navigation/directives/0040775 0001751 0001751 00000000000 12612226251 030537 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/navigation/styles/0040775 0001751 0001751 00000000000 12612226251 027721 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/navigation/types/0040775 0001751 0001751 00000000000 12612226251 027542 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/navigation/templates/0040775 0001751 0001751 00000000000 12612226251 030374 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/textInput/0040775 0001751 0001751 00000000000 12612226251 026243 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/textInput/directives/0040775 0001751 0001751 00000000000 12612226251 030404 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/textInput/styles/0040775 0001751 0001751 00000000000 12612226251 027566 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/textInput/templates/0040775 0001751 0001751 00000000000 12612226251 030241 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/notification/0040775 0001751 0001751 00000000000 12612226251 026725 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/notification/services/0040775 0001751 0001751 00000000000 12612226251 030550 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/notification/directives/0040775 0001751 0001751 00000000000 12612226251 031066 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/notification/styles/0040775 0001751 0001751 00000000000 12612226251 030250 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/notification/types/0040775 0001751 0001751 00000000000 12612226251 030071 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/notification/templates/0040775 0001751 0001751 00000000000 12612226251 030723 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/layouts/0040775 0001751 0001751 00000000000 12612226251 025157 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/fonts/0040775 0001751 0001751 00000000000 12612226251 024610 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/fonts/carlito/0040775 0001751 0001751 00000000000 12612226251 026245 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/translations/0040775 0001751 0001751 00000000000 12645510255 026206 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/WEB-INF/0040775 0001751 0001751 00000000000 12612226251 024506 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/lib/0040775 0001751 0001751 00000000000 12622426203 024225 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/lib/filesaver/0040775 0001751 0001751 00000000000 12612226251 026205 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/lib/angular-module-shim/0040775 0001751 0001751 00000000000 12612226251 030077 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/0040775 0001751 0001751 00000000000 12612226251 027062 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/0040775 0001751 0001751 00000000000 12612226251 030321 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/lib/blob/0040775 0001751 0001751 00000000000 12612226251 025143 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/lib/angular-translate/0040775 0001751 0001751 00000000000 12612226251 027651 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/images/0040775 0001751 0001751 00000000000 12645510255 024732 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/images/arrows/0040775 0001751 0001751 00000000000 12612226251 026241 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/images/user-icons/0040775 0001751 0001751 00000000000 12440135707 027017 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/images/settings/0040775 0001751 0001751 00000000000 12471273273 026575 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/images/protocol-icons/0040775 0001751 0001751 00000000000 12440135707 027702 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/images/mouse/0040775 0001751 0001751 00000000000 12440135707 026060 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/images/group-icons/0040775 0001751 0001751 00000000000 12440135707 027175 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/images/action-icons/0040775 0001751 0001751 00000000000 12612226251 027312 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/META-INF/0040775 0001751 0001751 00000000000 12635052306 024622 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/generated/0040775 0001751 0001751 00000000000 12635040407 025417 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/generated/templates-main/0040775 0001751 0001751 00000000000 12635040407 030337 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/0040775 0001751 0001751 00000000000 12471273273 023133 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/0040775 0001751 0001751 00000000000 12471273273 023722 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/0040775 0001751 0001751 00000000000 12471273273 025741 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/0040775 0001751 0001751 00000000000 12471273273 027676 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/0040775 0001751 0001751 00000000000 12471273273 030464 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/0040775 0001751 0001751 00000000000 12645510255 031542 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/0040775 0001751 0001751 00000000000 12612226251 033363 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/xml/0040775 0001751 0001751 00000000000 12612226251 032334 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/xml/usermapping/0040775 0001751 0001751 00000000000 12612226251 034666 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/auth/0040775 0001751 0001751 00000000000 12523311011 032463 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/0040775 0001751 0001751 00000000000 12645510255 033736 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/0040775 0001751 0001751 00000000000 12645510255 032517 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/history/0040775 0001751 0001751 00000000000 12645510255 034220 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/language/0040775 0001751 0001751 00000000000 12612226251 034274 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/0040775 0001751 0001751 00000000000 12645510255 034656 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/0040775 0001751 0001751 00000000000 12645510255 033460 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/schema/0040775 0001751 0001751 00000000000 12645510255 033757 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/0040775 0001751 0001751 00000000000 12612226251 034701 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000147 12645510255 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongrou0040775 0001751 0001751 00000000000 12645510255 035474 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000150 12645510255 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnecti0040775 0001751 0001751 00000000000 12645510255 035436 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/0040775 0001751 0001751 00000000000 12645510255 033475 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/0040775 0001751 0001751 00000000000 12645510255 033556 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/log/0040775 0001751 0001751 00000000000 12612226251 032315 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/0040775 0001751 0001751 00000000000 12645510255 033530 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/0040775 0001751 0001751 00000000000 12645510255 034760 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/0040775 0001751 0001751 00000000000 12645510255 035017 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/0040775 0001751 0001751 00000000000 12645510255 034757 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/net/0040775 0001751 0001751 00000000000 12424302752 023712 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/net/sourceforge/0040775 0001751 0001751 00000000000 12424302752 026235 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/net/sourceforge/guacamole/0040775 0001751 0001751 00000000000 12424302752 030172 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/net/sourceforge/guacamole/net/0040775 0001751 0001751 00000000000 12424302752 030760 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/0040775 0001751 0001751 00000000000 12612226251 032037 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/0040775 0001751 0001751 00000000000 12645510276 021275 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/0040775 0001751 0001751 00000000000 12612226251 022053 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/0040775 0001751 0001751 00000000000 12612226251 022777 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/resources/0040775 0001751 0001751 00000000000 12612226251 025011 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/resources/org/0040775 0001751 0001751 00000000000 12612226251 025600 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/resources/org/glyptodon/0040775 0001751 0001751 00000000000 12612226251 027617 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/resources/org/glyptodon/guacamole/0040775 0001751 0001751 00000000000 12612226251 031554 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/resources/org/glyptodon/guacamole/protocols/0040775 0001751 0001751 00000000000 12645510255 033606 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/0040775 0001751 0001751 00000000000 12527756701 023735 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/0040775 0001751 0001751 00000000000 12471273273 024520 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/0040775 0001751 0001751 00000000000 12471273273 026537 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/0040775 0001751 0001751 00000000000 12612226251 030463 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/token/0040775 0001751 0001751 00000000000 12612226251 031603 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/protocols/0040775 0001751 0001751 00000000000 12612226251 032507 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/xml/0040775 0001751 0001751 00000000000 12612226251 031263 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/environment/0040775 0001751 0001751 00000000000 12612226251 033027 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/properties/0040775 0001751 0001751 00000000000 12612226251 032657 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/0040775 0001751 0001751 00000000000 12471273273 031262 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/0040775 0001751 0001751 00000000000 12471273273 032403 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/listener/0040775 0001751 0001751 00000000000 12471273273 034230 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/0040775 0001751 0001751 00000000000 12645510255 032220 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/0040775 0001751 0001751 00000000000 12645510255 033511 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/permission/0040775 0001751 0001751 00000000000 12612226251 034402 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/credentials/0040775 0001751 0001751 00000000000 12645510255 034515 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/form/0040775 0001751 0001751 00000000000 12612226251 031426 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/test/0040775 0001751 0001751 00000000000 12612226251 023032 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/test/java/0040775 0001751 0001751 00000000000 12612226251 023753 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/test/java/org/0040775 0001751 0001751 00000000000 12612226251 024542 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/test/java/org/glyptodon/0040775 0001751 0001751 00000000000 12612226251 026561 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/test/java/org/glyptodon/guacamole/0040775 0001751 0001751 00000000000 12612226251 030516 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/test/java/org/glyptodon/guacamole/token/0040775 0001751 0001751 00000000000 12612226251 031636 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/0040775 0001751 0001751 00000000000 12612226251 020730 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/0040775 0001751 0001751 00000000000 12645510314 024526 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/src/0040775 0001751 0001751 00000000000 12612226251 025313 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/src/main/0040775 0001751 0001751 00000000000 12612226251 026237 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/src/main/assembly/0040775 0001751 0001751 00000000000 12612226251 030056 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/0040775 0001751 0001751 00000000000 12475140764 026207 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/0040775 0001751 0001751 00000000000 12645510316 033137 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/0040775 0001751 0001751 00000000000 12612226251 033722 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/0040775 0001751 0001751 00000000000 12612226251 034646 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000154 12645510255 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0040775 0001751 0001751 00000000000 12645510255 035366 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000160 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0040775 0001751 0001751 00000000000 12612226251 035360 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000172 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0040775 0001751 0001751 00000000000 12612226251 035360 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000204 12612226251 011626 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0040775 0001751 0001751 00000000000 12612226251 035360 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000211 12612226251 011624 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/auth/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0040775 0001751 0001751 00000000000 12612226251 035360 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000216 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0040775 0001751 0001751 00000000000 12612226251 035360 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000231 12645510255 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0040775 0001751 0001751 00000000000 12645510255 035366 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000231 12612226251 011626 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0040775 0001751 0001751 00000000000 12612226251 035360 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000236 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connectiongroup/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0040775 0001751 0001751 00000000000 12612226251 035360 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000223 12625262336 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/user/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0040775 0001751 0001751 00000000000 12625262336 035370 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000147 12612226251 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/jav0040775 0001751 0001751 00000000000 12612226251 035347 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000153 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/net/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/jav0040775 0001751 0001751 00000000000 12612226251 035347 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000167 12612226251 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/net/sourceforge/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/jav0040775 0001751 0001751 00000000000 12612226251 035347 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000201 12612226251 011623 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/net/sourceforge/guacamole/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/jav0040775 0001751 0001751 00000000000 12612226251 035347 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000205 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/net/sourceforge/guacamole/net/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/jav0040775 0001751 0001751 00000000000 12612226251 035347 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000212 12612226251 011625 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/net/sourceforge/guacamole/net/auth/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/jav0040775 0001751 0001751 00000000000 12612226251 035347 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000220 12645510255 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/jav0040775 0001751 0001751 00000000000 12645510255 035355 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/0040775 0001751 0001751 00000000000 12645510255 034401 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000150 12645510255 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgrade/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgra0040775 0001751 0001751 00000000000 12645510255 035440 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/0040775 0001751 0001751 00000000000 12645510322 034172 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/0040775 0001751 0001751 00000000000 12612226251 034760 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000147 12612226251 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0040775 0001751 0001751 00000000000 12612226251 035447 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000161 12645510255 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0040775 0001751 0001751 00000000000 12645510255 035455 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000165 12612226251 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0040775 0001751 0001751 00000000000 12612226251 035447 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000177 12612226251 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0040775 0001751 0001751 00000000000 12612226251 035447 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000211 12612226251 011624 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0040775 0001751 0001751 00000000000 12612226251 035447 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000216 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0040775 0001751 0001751 00000000000 12612226251 035447 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000223 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0040775 0001751 0001751 00000000000 12612226251 035447 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000236 12645510255 011641 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0040775 0001751 0001751 00000000000 12645510255 035455 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000236 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0040775 0001751 0001751 00000000000 12612226251 035447 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000243 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connectiongroup/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0040775 0001751 0001751 00000000000 12612226251 035447 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000230 12625262336 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/user/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0040775 0001751 0001751 00000000000 12625262336 035457 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000154 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0040775 0001751 0001751 00000000000 12612226251 035447 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000160 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0040775 0001751 0001751 00000000000 12612226251 035447 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000172 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0040775 0001751 0001751 00000000000 12612226251 035447 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000204 12612226251 011626 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0040775 0001751 0001751 00000000000 12612226251 035447 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000211 12612226251 011624 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/auth/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0040775 0001751 0001751 00000000000 12612226251 035447 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000224 12645510255 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/auth/postgresql/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0040775 0001751 0001751 00000000000 12645510255 035455 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000145 12645510255 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/0040775 0001751 0001751 00000000000 12645510255 035437 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000155 12645510255 011641 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/upgrade/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/0040775 0001751 0001751 00000000000 12645510255 035437 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/0040775 0001751 0001751 00000000000 12645510314 032702 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/0040775 0001751 0001751 00000000000 12642777072 033506 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/0040775 0001751 0001751 00000000000 12612226251 034413 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000153 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/reso0040775 0001751 0001751 00000000000 12612226251 035304 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000170 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/reso0040775 0001751 0001751 00000000000 12612226251 035304 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000146 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0040775 0001751 0001751 00000000000 12612226251 035255 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000152 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0040775 0001751 0001751 00000000000 12612226251 035255 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000164 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0040775 0001751 0001751 00000000000 12612226251 035255 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000176 12612226251 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0040775 0001751 0001751 00000000000 12612226251 035255 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000203 12612226251 011625 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0040775 0001751 0001751 00000000000 12612226251 035255 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000210 12645510255 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0040775 0001751 0001751 00000000000 12645510255 035263 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000215 12645510255 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0040775 0001751 0001751 00000000000 12645510255 035263 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000223 12645510255 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0040775 0001751 0001751 00000000000 12645510255 035263 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000217 12645510255 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0040775 0001751 0001751 00000000000 12645510255 035263 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000223 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0040775 0001751 0001751 00000000000 12612226251 035255 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000230 12645510255 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connectiongroup/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0040775 0001751 0001751 00000000000 12645510255 035263 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000231 12612226251 011626 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/activeconnection/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0040775 0001751 0001751 00000000000 12612226251 035255 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000215 12645510255 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0040775 0001751 0001751 00000000000 12645510255 035263 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000221 12612226251 011625 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/security/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0040775 0001751 0001751 00000000000 12612226251 035255 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/test/0040775 0001751 0001751 00000000000 12642777072 034465 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000146 12642777072 011652 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/test/java/guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/test/java0040775 0001751 0001751 00000000000 12642777072 035327 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/0040775 0001751 0001751 00000000000 12645510324 024545 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/doc/0040775 0001751 0001751 00000000000 12471273272 025317 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/doc/examples/0040775 0001751 0001751 00000000000 12471273272 027135 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/0040775 0001751 0001751 00000000000 12471273272 025341 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/0040775 0001751 0001751 00000000000 12612226251 026255 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/resources/0040775 0001751 0001751 00000000000 12645510255 030275 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/resources/translations/0040775 0001751 0001751 00000000000 12612226251 033010 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/assembly/0040775 0001751 0001751 00000000000 12612226251 030074 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/0040775 0001751 0001751 00000000000 12612226251 027176 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/0040775 0001751 0001751 00000000000 12612226251 027765 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/0040775 0001751 0001751 00000000000 12612226251 032004 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/0040775 0001751 0001751 00000000000 12612226251 033741 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/0040775 0001751 0001751 00000000000 12612226251 034702 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000147 12645510255 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/lda0040775 0001751 0001751 00000000000 12645510255 035371 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000162 12645510255 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/connection/guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/lda0040775 0001751 0001751 00000000000 12645510255 035371 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000154 12645510255 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/user/guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/lda0040775 0001751 0001751 00000000000 12645510255 035371 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/net/0040775 0001751 0001751 00000000000 12571440063 027767 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/net/sourceforge/0040775 0001751 0001751 00000000000 12571440063 032312 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/net/sourceforge/guacamole/0040775 0001751 0001751 00000000000 12571440063 034247 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/net/sourceforge/guacamole/net/0040775 0001751 0001751 00000000000 12571440063 035035 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000150 12571440063 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/net/sourceforge/guacamole/net/auth/guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/net/sourceforge/guacamole/net/au0040775 0001751 0001751 00000000000 12571440063 035363 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000155 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/net/sourceforge/guacamole/net/auth/ldap/guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/net/sourceforge/guacamole/net/au0040775 0001751 0001751 00000000000 12612226251 035360 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/schema/0040775 0001751 0001751 00000000000 12471273272 026012 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-noauth/0040775 0001751 0001751 00000000000 12645510326 025125 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-noauth/doc/0040775 0001751 0001751 00000000000 12471273272 025675 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-noauth/doc/example/0040775 0001751 0001751 00000000000 12471273272 027330 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/0040775 0001751 0001751 00000000000 12471273272 025717 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/0040775 0001751 0001751 00000000000 12612226251 026633 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/resources/0040775 0001751 0001751 00000000000 12645510255 030653 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/resources/translations/0040775 0001751 0001751 00000000000 12612226251 033366 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/assembly/0040775 0001751 0001751 00000000000 12612226251 030452 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/java/0040775 0001751 0001751 00000000000 12471273272 027564 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/java/net/0040775 0001751 0001751 00000000000 12471273272 030352 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/java/net/sourceforge/0040775 0001751 0001751 00000000000 12471273272 032675 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/java/net/sourceforge/guacamole/0040775 0001751 0001751 00000000000 12471273272 034632 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000145 12471273272 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/java/net/sourceforge/guacamole/net/guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/java/net/sourceforge/guacamole/net/0040775 0001751 0001751 00000000000 12471273272 035420 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000152 12471273272 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/java/net/sourceforge/guacamole/net/auth/guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/java/net/sourceforge/guacamole/net/0040775 0001751 0001751 00000000000 12471273272 035420 5ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000161 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/java/net/sourceforge/guacamole/net/auth/noauth/guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/java/net/sourceforge/guacamole/net/0040775 0001751 0001751 00000000000 12612226251 035410 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/0040775 0001751 0001751 00000000000 12645510264 021762 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/doc/0040775 0001751 0001751 00000000000 12424302752 022523 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/doc/example/0040775 0001751 0001751 00000000000 12645510255 024162 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/0040775 0001751 0001751 00000000000 12471273273 022554 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/0040775 0001751 0001751 00000000000 12635040357 023475 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/0040775 0001751 0001751 00000000000 12527756701 024425 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/0040775 0001751 0001751 00000000000 12471273272 025207 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/0040775 0001751 0001751 00000000000 12471273272 027226 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/0040775 0001751 0001751 00000000000 12612226251 031153 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/0040775 0001751 0001751 00000000000 12645510255 033022 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/0040775 0001751 0001751 00000000000 12645510255 032645 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/net/0040775 0001751 0001751 00000000000 12612226251 031741 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/io/0040775 0001751 0001751 00000000000 12612226251 031562 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/websocket/0040775 0001751 0001751 00000000000 12645510255 033147 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/test/0040775 0001751 0001751 00000000000 12471273273 023533 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/test/java/0040775 0001751 0001751 00000000000 12471273273 024454 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/test/java/org/0040775 0001751 0001751 00000000000 12471273273 025243 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/test/java/org/glyptodon/0040775 0001751 0001751 00000000000 12471273273 027262 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/test/java/org/glyptodon/guacamole/0040775 0001751 0001751 00000000000 12471273273 031217 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/test/java/org/glyptodon/guacamole/protocol/0040775 0001751 0001751 00000000000 12612226251 033047 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/test/java/org/glyptodon/guacamole/io/0040775 0001751 0001751 00000000000 12471273273 031626 5ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/README0100664 0001751 0001751 00000004625 12612226251 017415 0ustar00mjumpermjumper0000000 0000000 ------------------------------------------------------------ About this README ------------------------------------------------------------ This README is intended to provide quick and to-the-point documentation for technical users intending to compile parts of Guacamole themselves. Source archives and pre-built .war files are available from the files section of the main project page: http://sourceforge.net/projects/guacamole/files/ A full manual is provided on the Guacamole web site: http://guac-dev.org/ ------------------------------------------------------------ What is guacamole-client? ------------------------------------------------------------ guacamole-client is the superproject containing all Maven-based projects that make Guacamole, an HTML5 web application that provides access to your desktop using remote desktop protocols. guacamole-client is used to build the subprojects that make up Guacamole, and to provide a common central repository. Each project contained here is completely independent of guacamole-client and can be built separately, though the others may have to be built first. If all projects are built using guacamole-client, Maven will take care of the proper build order. ------------------------------------------------------------ Compiling and installing Guacamole ------------------------------------------------------------ Guacamole is built using Maven. Building Guacamole compiles all classes and packages them into a deployable .war file. This .war file can be installed and deployed under servlet containers like Apache Tomcat or Jetty. 1) Run mvn package $ mvn package Maven will download any needed dependencies for building the .jar file. Once all dependencies have been downloaded, the .war file will be created in the guacamole/target/ subdirectory of the current directory. 2) Copy the .war file as directed in the instructions provided with your servlet container. Apache Tomcat, Jetty, and other servlet containers have specific and varying locations that .war files must be placed for the web application to be deployed. You will likely need to do this as root. ------------------------------------------------------------ Reporting problems ------------------------------------------------------------ Please report any bugs encountered by opening a new issue in the JIRA system hosted at: http://glyptodon.org/jira/ guacamole-client-0.9.9/doc/guacamole-example/src/main/webapp/dot.gif0100664 0001751 0001751 00000000110 12424302752 027121 0ustar00mjumpermjumper0000000 0000000 GIF89a €ÿÿÿ!ù , DŽ©Ëí£œ´Ú‹³Þ¼û†âH–扦êʶî Çf;guacamole-client-0.9.9/doc/guacamole-example/src/main/webapp/WEB-INF/web.xml0100664 0001751 0001751 00000003747 12612226251 030213 0ustar00mjumpermjumper0000000 0000000 index.html 30 Tunnel servlet. Tunnel org.glyptodon.guacamole.net.example.DummyGuacamoleTunnelServlet Tunnel /tunnel guacamole-client-0.9.9/doc/guacamole-example/src/main/webapp/index.html0100664 0001751 0001751 00000005562 12612226251 027657 0ustar00mjumpermjumper0000000 0000000 Guacamole (EXAMPLE)
guacamole-client-0.9.9/doc/guacamole-example/src/main/webapp/guacamole.css0100664 0001751 0001751 00000002232 12612226251 030320 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .guac-hide-cursor { cursor: url('dot.gif'),default; } guacamole-client-0.9.9/doc/guacamole-example/src/main/webapp/META-INF/context.xml0100664 0001751 0001751 00000000120 12424302752 031213 0ustar00mjumpermjumper0000000 0000000 ././@LongLink0100644 0000000 0000000 00000000200 12645510255 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/doc/guacamole-example/src/main/java/org/glyptodon/guacamole/net/example/DummyGuacamoleTunnelServlet.javaguacamole-client-0.9.9/doc/guacamole-example/src/main/java/org/glyptodon/guacamole/net/example/Dummy0100664 0001751 0001751 00000005304 12645510255 035327 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.example; import javax.servlet.http.HttpServletRequest; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.GuacamoleSocket; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.InetGuacamoleSocket; import org.glyptodon.guacamole.net.SimpleGuacamoleTunnel; import org.glyptodon.guacamole.protocol.ConfiguredGuacamoleSocket; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; import org.glyptodon.guacamole.servlet.GuacamoleHTTPTunnelServlet; /** * Simple tunnel example with hard-coded configuration parameters. * * @author Michael Jumper */ public class DummyGuacamoleTunnelServlet extends GuacamoleHTTPTunnelServlet { @Override protected GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException { // guacd connection information String hostname = "localhost"; int port = 4822; // VNC connection information GuacamoleConfiguration config = new GuacamoleConfiguration(); config.setProtocol("vnc"); config.setParameter("hostname", "localhost"); config.setParameter("port", "5901"); config.setParameter("password", "potato"); // Connect to guacd, proxying a connection to the VNC server above GuacamoleSocket socket = new ConfiguredGuacamoleSocket( new InetGuacamoleSocket(hostname, port), config ); // Create tunnel from now-configured socket GuacamoleTunnel tunnel = new SimpleGuacamoleTunnel(socket); return tunnel; } } guacamole-client-0.9.9/doc/guacamole-example/pom.xml0100664 0001751 0001751 00000005470 12645510255 024212 0ustar00mjumpermjumper0000000 0000000 4.0.0 org.glyptodon.guacamole guacamole-example war 0.9.9 guacamole-example http://guac-dev.org/ UTF-8 org.apache.maven.plugins maven-compiler-plugin 3.3 1.6 1.6 -Xlint:all -Werror true org.apache.maven.plugins maven-war-plugin 2.6 org.glyptodon.guacamole guacamole-common-js zip javax.servlet servlet-api 2.5 provided org.glyptodon.guacamole guacamole-common 0.9.9 compile org.glyptodon.guacamole guacamole-common-js 0.9.9 zip runtime org.slf4j slf4j-simple 1.7.7 guacamole-client-0.9.9/doc/guacamole-example/LICENSE0100664 0001751 0001751 00000002041 12612226251 023663 0ustar00mjumpermjumper0000000 0000000 Copyright (C) 2013 Glyptodon LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. guacamole-client-0.9.9/pom.xml0100664 0001751 0001751 00000004376 12645510255 020063 0ustar00mjumpermjumper0000000 0000000 4.0.0 org.glyptodon.guacamole guacamole-client pom 0.9.9 guacamole-client http://guac-dev.org/ UTF-8 guacamole guacamole-common guacamole-ext guacamole-common-js extensions/guacamole-auth-jdbc extensions/guacamole-auth-ldap extensions/guacamole-auth-noauth maven-assembly-plugin 2.5.3 ${project.artifactId}-${project.version} false gnu project-assembly.xml make-source-archive package single guacamole-client-0.9.9/guacamole-common-js/README0100664 0001751 0001751 00000005404 12424302752 023250 0ustar00mjumpermjumper0000000 0000000 ------------------------------------------------------------ About this README ------------------------------------------------------------ This README is intended to provide quick and to-the-point documentation for technical users intending to compile parts of Guacamole themselves. Distribution-specific packages are available from the files section of the main project page: http://sourceforge.net/projects/guacamole/files/ Distribution-specific documentation is provided on the Guacamole wiki: http://guac-dev.org/ ------------------------------------------------------------ What is guacamole-common-js? ------------------------------------------------------------ guacamole-common-js is the core JavaScript library used by the Guacamole web application. guacamole-common-js provides an efficient HTTP tunnel for transporting protocol data between JavaScript and the web application, as well as an implementation of a Guacamole protocol client and abstract synchronized drawing layers. ------------------------------------------------------------ Compiling and installing guacamole-common-js ------------------------------------------------------------ guacamole-common-js is built using Maven. Note that this is JavaScript code and not actually compiled. "Building" guacamole-common-js actually simply packages it into a redistributable .zip file. This .zip file can be easily included and expanded into other Maven-based projects (like Guacamole). Note that prebuilt versions of guacamole-common-js are available from the main guac-dev.org Maven repository which is referenced in all Maven projects in Guacamole. Unless you want to make changes to guacamole-common-js or you want to use a newer, unreleased version (such as the unstable branch), you do not need to build this manually. You can let Maven download it for you when you build Guacamole. 1) Run mvn package $ mvn package Maven will download any needed dependencies for building the .zip file. Once all dependencies have been downloaded, the .zip file will be created in the target/ subdirectory of the current directory. 2) Run mvn install $ mvn install DO NOT RUN THIS AS ROOT! Maven will install guacamole-common-js to your user's local Maven repository where it can be used in future builds. It will not install into a system-wide repository and does not require root privileges. Once installed, building other Maven projects that depend on guacamole-common-js (such as Guacamole) will be possible. ------------------------------------------------------------ Reporting problems ------------------------------------------------------------ Please report any bugs encountered by opening a new ticket at the Trac system hosted at: http://guac-dev.org/trac/ guacamole-client-0.9.9/guacamole-common-js/doc/guacamole-osk.dtd0100664 0001751 0001751 00000001514 12424302752 026357 0ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole-common-js/static.xml0100664 0001751 0001751 00000001262 12471273272 024405 0ustar00mjumpermjumper0000000 0000000 guacamole-common-js guacamole-common-js zip src/main/webapp/modules/ *.js modules/ target/${project.name}-${project.version}/ *.js guacamole-client-0.9.9/guacamole-common-js/src/main/webapp/modules/Object.js0100664 0001751 0001751 00000015472 12645510255 030607 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ var Guacamole = Guacamole || {}; /** * An object used by the Guacamole client to house arbitrarily-many named * input and output streams. * * @constructor * @param {Guacamole.Client} client * The client owning this object. * * @param {Number} index * The index of this object. */ Guacamole.Object = function guacamoleObject(client, index) { /** * Reference to this Guacamole.Object. * * @private * @type {Guacamole.Object} */ var guacObject = this; /** * Map of stream name to corresponding queue of callbacks. The queue of * callbacks is guaranteed to be in order of request. * * @private * @type {Object.} */ var bodyCallbacks = {}; /** * Removes and returns the callback at the head of the callback queue for * the stream having the given name. If no such callbacks exist, null is * returned. * * @private * @param {String} name * The name of the stream to retrieve a callback for. * * @returns {Function} * The next callback associated with the stream having the given name, * or null if no such callback exists. */ var dequeueBodyCallback = function dequeueBodyCallback(name) { // If no callbacks defined, simply return null var callbacks = bodyCallbacks[name]; if (!callbacks) return null; // Otherwise, pull off first callback, deleting the queue if empty var callback = callbacks.shift(); if (callbacks.length === 0) delete bodyCallbacks[name]; // Return found callback return callback; }; /** * Adds the given callback to the tail of the callback queue for the stream * having the given name. * * @private * @param {String} name * The name of the stream to associate with the given callback. * * @param {Function} callback * The callback to add to the queue of the stream with the given name. */ var enqueueBodyCallback = function enqueueBodyCallback(name, callback) { // Get callback queue by name, creating first if necessary var callbacks = bodyCallbacks[name]; if (!callbacks) { callbacks = []; bodyCallbacks[name] = callbacks; } // Add callback to end of queue callbacks.push(callback); }; /** * The index of this object. * * @type {Number} */ this.index = index; /** * Called when this object receives the body of a requested input stream. * By default, all objects will invoke the callbacks provided to their * requestInputStream() functions based on the name of the stream * requested. This behavior can be overridden by specifying a different * handler here. * * @event * @param {Guacamole.InputStream} inputStream * The input stream of the received body. * * @param {String} mimetype * The mimetype of the data being received. * * @param {String} name * The name of the stream whose body has been received. */ this.onbody = function defaultBodyHandler(inputStream, mimetype, name) { // Call queued callback for the received body, if any var callback = dequeueBodyCallback(name); if (callback) callback(inputStream, mimetype); }; /** * Called when this object is being undefined. Once undefined, no further * communication involving this object may occur. * * @event */ this.onundefine = null; /** * Requests read access to the input stream having the given name. If * successful, a new input stream will be created. * * @param {String} name * The name of the input stream to request. * * @param {Function} [bodyCallback] * The callback to invoke when the body of the requested input stream * is received. This callback will be provided a Guacamole.InputStream * and its mimetype as its two only arguments. If the onbody handler of * this object is overridden, this callback will not be invoked. */ this.requestInputStream = function requestInputStream(name, bodyCallback) { // Queue body callback if provided if (bodyCallback) enqueueBodyCallback(name, bodyCallback); // Send request for input stream client.requestObjectInputStream(guacObject.index, name); }; /** * Creates a new output stream associated with this object and having the * given mimetype and name. The legality of a mimetype and name is dictated * by the object itself. * * @param {String} mimetype * The mimetype of the data which will be sent to the output stream. * * @param {String} name * The defined name of an output stream within this object. * * @returns {Guacamole.OutputStream} * An output stream which will write blobs to the named output stream * of this object. */ this.createOutputStream = function createOutputStream(mimetype, name) { return client.createObjectOutputStream(guacObject.index, mimetype, name); }; }; /** * The reserved name denoting the root stream of any object. The contents of * the root stream MUST be a JSON map of stream name to mimetype. * * @constant * @type {String} */ Guacamole.Object.ROOT_STREAM = '/'; /** * The mimetype of a stream containing JSON which maps available stream names * to their corresponding mimetype. The root stream of a Guacamole.Object MUST * have this mimetype. * * @constant * @type {String} */ Guacamole.Object.STREAM_INDEX_MIMETYPE = 'application/vnd.glyptodon.guacamole.stream-index+json'; guacamole-client-0.9.9/guacamole-common-js/src/main/webapp/modules/VideoPlayer.js0100664 0001751 0001751 00000007726 12645510255 031627 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ var Guacamole = Guacamole || {}; /** * Abstract video player which accepts, queues and plays back arbitrary video * data. It is up to implementations of this class to provide some means of * handling a provided Guacamole.InputStream and rendering the received data to * the provided Guacamole.Display.VisibleLayer. Data received along the * provided stream is to be played back immediately. * * @constructor */ Guacamole.VideoPlayer = function VideoPlayer() { /** * Notifies this Guacamole.VideoPlayer that all video up to the current * point in time has been given via the underlying stream, and that any * difference in time between queued video data and the current time can be * considered latency. */ this.sync = function sync() { // Default implementation - do nothing }; }; /** * Determines whether the given mimetype is supported by any built-in * implementation of Guacamole.VideoPlayer, and thus will be properly handled * by Guacamole.VideoPlayer.getInstance(). * * @param {String} mimetype * The mimetype to check. * * @returns {Boolean} * true if the given mimetype is supported by any built-in * Guacamole.VideoPlayer, false otherwise. */ Guacamole.VideoPlayer.isSupportedType = function isSupportedType(mimetype) { // There are currently no built-in video players (and therefore no // supported types) return false; }; /** * Returns a list of all mimetypes supported by any built-in * Guacamole.VideoPlayer, in rough order of priority. Beware that only the core * mimetypes themselves will be listed. Any mimetype parameters, even required * ones, will not be included in the list. * * @returns {String[]} * A list of all mimetypes supported by any built-in Guacamole.VideoPlayer, * excluding any parameters. */ Guacamole.VideoPlayer.getSupportedTypes = function getSupportedTypes() { // There are currently no built-in video players (and therefore no // supported types) return []; }; /** * Returns an instance of Guacamole.VideoPlayer providing support for the given * video format. If support for the given video format is not available, null * is returned. * * @param {Guacamole.InputStream} stream * The Guacamole.InputStream to read video data from. * * @param {Guacamole.Display.VisibleLayer} layer * The destination layer in which this Guacamole.VideoPlayer should play * the received video data. * * @param {String} mimetype * The mimetype of the video data in the provided stream. * * @return {Guacamole.VideoPlayer} * A Guacamole.VideoPlayer instance supporting the given mimetype and * reading from the given stream, or null if support for the given mimetype * is absent. */ Guacamole.VideoPlayer.getInstance = function getInstance(stream, layer, mimetype) { // There are currently no built-in video players return null; }; guacamole-client-0.9.9/guacamole-common-js/src/main/webapp/modules/OnScreenKeyboard.js0100664 0001751 0001751 00000076055 12645510255 032602 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ var Guacamole = Guacamole || {}; /** * Dynamic on-screen keyboard. Given the layout object for an on-screen * keyboard, this object will construct a clickable on-screen keyboard with its * own key events. * * @constructor * @param {Guacamole.OnScreenKeyboard.Layout} layout * The layout of the on-screen keyboard to display. */ Guacamole.OnScreenKeyboard = function(layout) { /** * Reference to this Guacamole.OnScreenKeyboard. * * @private * @type {Guacamole.OnScreenKeyboard} */ var osk = this; /** * Map of currently-set modifiers to the keysym associated with their * original press. When the modifier is cleared, this keysym must be * released. * * @private * @type {Object.} */ var modifierKeysyms = {}; /** * Map of all key names to their current pressed states. If a key is not * pressed, it may not be in this map at all, but all pressed keys will * have a corresponding mapping to true. * * @private * @type {Object.} */ var pressed = {}; /** * All scalable elements which are part of the on-screen keyboard. Each * scalable element is carefully controlled to ensure the interface layout * and sizing remains constant, even on browsers that would otherwise * experience rounding error due to unit conversions. * * @private * @type {ScaledElement[]} */ var scaledElements = []; /** * Adds a CSS class to an element. * * @private * @function * @param {Element} element * The element to add a class to. * * @param {String} classname * The name of the class to add. */ var addClass = function addClass(element, classname) { // If classList supported, use that if (element.classList) element.classList.add(classname); // Otherwise, simply append the class else element.className += " " + classname; }; /** * Removes a CSS class from an element. * * @private * @function * @param {Element} element * The element to remove a class from. * * @param {String} classname * The name of the class to remove. */ var removeClass = function removeClass(element, classname) { // If classList supported, use that if (element.classList) element.classList.remove(classname); // Otherwise, manually filter out classes with given name else { element.className = element.className.replace(/([^ ]+)[ ]*/g, function removeMatchingClasses(match, testClassname) { // If same class, remove if (testClassname === classname) return ""; // Otherwise, allow return match; } ); } }; /** * Counter of mouse events to ignore. This decremented by mousemove, and * while non-zero, mouse events will have no effect. * * @private * @type {Number} */ var ignoreMouse = 0; /** * Ignores all pending mouse events when touch events are the apparent * source. Mouse events are ignored until at least touchMouseThreshold * mouse events occur without corresponding touch events. * * @private */ var ignorePendingMouseEvents = function ignorePendingMouseEvents() { ignoreMouse = osk.touchMouseThreshold; }; /** * An element whose dimensions are maintained according to an arbitrary * scale. The conversion factor for these arbitrary units to pixels is * provided later via a call to scale(). * * @private * @constructor * @param {Element} element * The element whose scale should be maintained. * * @param {Number} width * The width of the element, in arbitrary units, relative to other * ScaledElements. * * @param {Number} height * The height of the element, in arbitrary units, relative to other * ScaledElements. * * @param {Boolean} [scaleFont=false] * Whether the line height and font size should be scaled as well. */ var ScaledElement = function ScaledElement(element, width, height, scaleFont) { /** * The width of this ScaledElement, in arbitrary units, relative to * other ScaledElements. * * @type {Number} */ this.width = width; /** * The height of this ScaledElement, in arbitrary units, relative to * other ScaledElements. * * @type {Number} */ this.height = height; /** * Resizes the associated element, updating its dimensions according to * the given pixels per unit. * * @param {Number} pixels * The number of pixels to assign per arbitrary unit. */ this.scale = function(pixels) { // Scale element width/height element.style.width = (width * pixels) + "px"; element.style.height = (height * pixels) + "px"; // Scale font, if requested if (scaleFont) { element.style.lineHeight = (height * pixels) + "px"; element.style.fontSize = pixels + "px"; } }; }; /** * Returns whether all modifiers having the given names are currently * active. * * @private * @param {String[]} names * The names of all modifiers to test. * * @returns {Boolean} * true if all specified modifiers are pressed, false otherwise. */ var modifiersPressed = function modifiersPressed(names) { // If any required modifiers are not pressed, return false for (var i=0; i < names.length; i++) { // Test whether current modifier is pressed var name = names[i]; if (!(name in modifierKeysyms)) return false; } // Otherwise, all required modifiers are pressed return true; }; /** * Returns the single matching Key object associated with the key of the * given name, where that Key object's requirements (such as pressed * modifiers) are all currently satisfied. * * @private * @param {String} keyName * The name of the key to retrieve. * * @returns {Guacamole.OnScreenKeyboard.Key} * The Key object associated with the given name, where that object's * requirements are all currently satisfied, or null if no such Key * can be found. */ var getActiveKey = function getActiveKey(keyName) { // Get key array for given name var keys = osk.keys[keyName]; if (!keys) return null; // Find last matching key for (var i = keys.length - 1; i >= 0; i--) { // Get candidate key var candidate = keys[i]; // If all required modifiers are pressed, use that key if (modifiersPressed(candidate.requires)) return candidate; } // No valid key return null; }; /** * Presses the key having the given name, updating the associated key * element with the "guac-keyboard-pressed" CSS class. If the key is * already pressed, this function has no effect. * * @private * @param {String} keyName * The name of the key to press. * * @param {String} keyElement * The element associated with the given key. */ var press = function press(keyName, keyElement) { // Press key if not yet pressed if (!pressed[keyName]) { addClass(keyElement, "guac-keyboard-pressed"); // Get current key based on modifier state var key = getActiveKey(keyName); // Update modifier state if (key.modifier) { // Construct classname for modifier var modifierClass = "guac-keyboard-modifier-" + getCSSName(key.modifier); // Retrieve originally-pressed keysym, if modifier was already pressed var originalKeysym = modifierKeysyms[key.modifier]; // Activate modifier if not pressed if (!originalKeysym) { addClass(keyboard, modifierClass); modifierKeysyms[key.modifier] = key.keysym; // Send key event if (osk.onkeydown) osk.onkeydown(key.keysym); } // Deactivate if not pressed else { removeClass(keyboard, modifierClass); delete modifierKeysyms[key.modifier]; // Send key event if (osk.onkeyup) osk.onkeyup(originalKeysym); } } // If not modifier, send key event now else if (osk.onkeydown) osk.onkeydown(key.keysym); // Mark key as pressed pressed[keyName] = true; } }; /** * Releases the key having the given name, removing the * "guac-keyboard-pressed" CSS class from the associated element. If the * key is already released, this function has no effect. * * @private * @param {String} keyName * The name of the key to release. * * @param {String} keyElement * The element associated with the given key. */ var release = function release(keyName, keyElement) { // Release key if currently pressed if (pressed[keyName]) { removeClass(keyElement, "guac-keyboard-pressed"); // Get current key based on modifier state var key = getActiveKey(keyName); // Send key event if not a modifier key if (!key.modifier && osk.onkeyup) osk.onkeyup(key.keysym); // Mark key as released pressed[keyName] = false; } }; // Create keyboard var keyboard = document.createElement("div"); keyboard.className = "guac-keyboard"; // Do not allow selection or mouse movement to propagate/register. keyboard.onselectstart = keyboard.onmousemove = keyboard.onmouseup = keyboard.onmousedown = function handleMouseEvents(e) { // If ignoring events, decrement counter if (ignoreMouse) ignoreMouse--; e.stopPropagation(); return false; }; /** * The number of mousemove events to require before re-enabling mouse * event handling after receiving a touch event. * * @type {Number} */ this.touchMouseThreshold = 3; /** * Fired whenever the user presses a key on this Guacamole.OnScreenKeyboard. * * @event * @param {Number} keysym The keysym of the key being pressed. */ this.onkeydown = null; /** * Fired whenever the user releases a key on this Guacamole.OnScreenKeyboard. * * @event * @param {Number} keysym The keysym of the key being released. */ this.onkeyup = null; /** * The keyboard layout provided at time of construction. * * @type {Guacamole.OnScreenKeyboard.Layout} */ this.layout = new Guacamole.OnScreenKeyboard.Layout(layout); /** * Returns the element containing the entire on-screen keyboard. * @returns {Element} The element containing the entire on-screen keyboard. */ this.getElement = function() { return keyboard; }; /** * Resizes all elements within this Guacamole.OnScreenKeyboard such that * the width is close to but does not exceed the specified width. The * height of the keyboard is determined based on the width. * * @param {Number} width The width to resize this Guacamole.OnScreenKeyboard * to, in pixels. */ this.resize = function(width) { // Get pixel size of a unit var unit = Math.floor(width * 10 / osk.layout.width) / 10; // Resize all scaled elements for (var i=0; i} keys * A mapping of key name to key definition, where the key definition is * the title of the key (a string), the keysym (a number), a single * Key object, or an array of Key objects. * * @returns {Object.} * A more-predictable mapping of key name to key definition, where the * key definition is always simply an array of Key objects. */ var getKeys = function getKeys(keys) { var keyArrays = {}; // Coerce all keys into individual key arrays for (var name in layout.keys) { keyArrays[name] = asKeyArray(name, keys[name]); } return keyArrays; }; /** * Map of all key names to their corresponding set of keys. Each key name * may correspond to multiple keys due to the effect of modifiers. * * @type {Object.} */ this.keys = getKeys(layout.keys); /** * Given an arbitrary string representing the name of some component of the * on-screen keyboard, returns a string formatted for use as a CSS class * name. The result will be lowercase. Word boundaries previously denoted * by CamelCase will be replaced by individual hyphens, as will all * contiguous non-alphanumeric characters. * * @private * @param {String} name * An arbitrary string representing the name of some component of the * on-screen keyboard. * * @returns {String} * A string formatted for use as a CSS class name. */ var getCSSName = function getCSSName(name) { // Convert name from possibly-CamelCase to hyphenated lowercase var cssName = name .replace(/([a-z])([A-Z])/g, '$1-$2') .replace(/[^A-Za-z0-9]+/g, '-') .toLowerCase(); return cssName; }; /** * Appends DOM elements to the given element as dictated by the layout * structure object provided. If a name is provided, an additional CSS * class, prepended with "guac-keyboard-", will be added to the top-level * element. * * If the layout structure object is an array, all elements within that * array will be recursively appended as children of a group, and the * top-level element will be given the CSS class "guac-keyboard-group". * * If the layout structure object is an object, all properties within that * object will be recursively appended as children of a group, and the * top-level element will be given the CSS class "guac-keyboard-group". The * name of each property will be applied as the name of each child object * for the sake of CSS. Each property will be added in sorted order. * * If the layout structure object is a string, the key having that name * will be appended. The key will be given the CSS class * "guac-keyboard-key" and "guac-keyboard-key-NAME", where NAME is the name * of the key. If the name of the key is a single character, this will * first be transformed into the C-style hexadecimal literal for the * Unicode codepoint of that character. For example, the key "A" would * become "guac-keyboard-key-0x41". * * If the layout structure object is a number, a gap of that size will be * inserted. The gap will be given the CSS class "guac-keyboard-gap", and * will be scaled according to the same size units as each key. * * @private * @param {Element} element * The element to append elements to. * * @param {Array|Object|String|Number} object * The layout structure object to use when constructing the elements to * append. * * @param {String} [name] * The name of the top-level element being appended, if any. */ var appendElements = function appendElements(element, object, name) { var i; // Create div which will become the group or key var div = document.createElement('div'); // Add class based on name, if name given if (name) addClass(div, 'guac-keyboard-' + getCSSName(name)); // If an array, append each element if (object instanceof Array) { // Add group class addClass(div, 'guac-keyboard-group'); // Append all elements of array for (i=0; i < object.length; i++) appendElements(div, object[i]); } // If an object, append each property value else if (object instanceof Object) { // Add group class addClass(div, 'guac-keyboard-group'); // Append all children, sorted by name var names = Object.keys(object).sort(); for (i=0; i < names.length; i++) { var name = names[i]; appendElements(div, object[name], name); } } // If a number, create as a gap else if (typeof object === 'number') { // Add gap class addClass(div, 'guac-keyboard-gap'); // Maintain scale scaledElements.push(new ScaledElement(div, object, object)); } // If a string, create as a key else if (typeof object === 'string') { // If key name is only one character, use codepoint for name var keyName = object; if (keyName.length === 1) keyName = '0x' + keyName.charCodeAt(0).toString(16); // Add key container class addClass(div, 'guac-keyboard-key-container'); // Create key element which will contain all possible caps var keyElement = document.createElement('div'); keyElement.className = 'guac-keyboard-key ' + 'guac-keyboard-key-' + getCSSName(keyName); // Add all associated keys as caps within DOM var keys = osk.keys[object]; if (keys) { for (i=0; i < keys.length; i++) { // Get current key var key = keys[i]; // Create cap element for key var capElement = document.createElement('div'); capElement.className = 'guac-keyboard-cap'; capElement.textContent = key.title; // Add classes for any requirements for (var j=0; j < key.requires.length; j++) { var requirement = key.requires[j]; addClass(capElement, 'guac-keyboard-requires-' + getCSSName(requirement)); addClass(keyElement, 'guac-keyboard-uses-' + getCSSName(requirement)); } // Add cap to key within DOM keyElement.appendChild(capElement); } } // Add key to DOM, maintain scale div.appendChild(keyElement); scaledElements.push(new ScaledElement(div, osk.layout.keyWidths[object] || 1, 1, true)); /** * Handles a touch event which results in the pressing of an OSK * key. Touch events will result in mouse events being ignored for * touchMouseThreshold events. * * @private * @param {TouchEvent} e * The touch event being handled. */ var touchPress = function touchPress(e) { e.preventDefault(); ignoreMouse = osk.touchMouseThreshold; press(object, keyElement); }; /** * Handles a touch event which results in the release of an OSK * key. Touch events will result in mouse events being ignored for * touchMouseThreshold events. * * @private * @param {TouchEvent} e * The touch event being handled. */ var touchRelease = function touchRelease(e) { e.preventDefault(); ignoreMouse = osk.touchMouseThreshold; release(object, keyElement); }; /** * Handles a mouse event which results in the pressing of an OSK * key. If mouse events are currently being ignored, this handler * does nothing. * * @private * @param {MouseEvent} e * The touch event being handled. */ var mousePress = function mousePress(e) { e.preventDefault(); if (ignoreMouse === 0) press(object, keyElement); }; /** * Handles a mouse event which results in the release of an OSK * key. If mouse events are currently being ignored, this handler * does nothing. * * @private * @param {MouseEvent} e * The touch event being handled. */ var mouseRelease = function mouseRelease(e) { e.preventDefault(); if (ignoreMouse === 0) release(object, keyElement); }; // Handle touch events on key keyElement.addEventListener("touchstart", touchPress, true); keyElement.addEventListener("touchend", touchRelease, true); // Handle mouse events on key keyElement.addEventListener("mousedown", mousePress, true); keyElement.addEventListener("mouseup", mouseRelease, true); keyElement.addEventListener("mouseout", mouseRelease, true); } // end if object is key name // Add newly-created group/key element.appendChild(div); }; // Create keyboard layout in DOM appendElements(keyboard, layout.layout); }; /** * Represents an entire on-screen keyboard layout, including all available * keys, their behaviors, and their relative position and sizing. * * @constructor * @param {Guacamole.OnScreenKeyboard.Layout|Object} template * The object whose identically-named properties will be used to initialize * the properties of this layout. */ Guacamole.OnScreenKeyboard.Layout = function(template) { /** * The language of keyboard layout, such as "en_US". This property is for * informational purposes only, but it is recommend to conform to the * [language code]_[country code] format. * * @type {String} */ this.language = template.language; /** * The type of keyboard layout, such as "qwerty". This property is for * informational purposes only, and does not conform to any standard. * * @type {String} */ this.type = template.type; /** * Map of key name to corresponding keysym, title, or key object. If only * the keysym or title is provided, the key object will be created * implicitly. In all cases, the name property of the key object will be * taken from the name given in the mapping. * * @type {Object.} */ this.keys = template.keys; /** * Arbitrarily nested, arbitrarily grouped key names. The contents of the * layout will be traversed to produce an identically-nested grouping of * keys in the DOM tree. All strings will be transformed into their * corresponding sets of keys, while all objects and arrays will be * transformed into named groups and anonymous groups respectively. Any * numbers present will be transformed into gaps of that size, scaled * according to the same units as each key. * * @type {Object} */ this.layout = template.layout; /** * The width of the entire keyboard, in arbitrary units. The width of each * key is relative to this width, as both width values are assumed to be in * the same units. The conversion factor between these units and pixels is * derived later via a call to resize() on the Guacamole.OnScreenKeyboard. * * @type {Number} */ this.width = template.width; /** * The width of each key, in arbitrary units, relative to other keys in * this layout. The true pixel size of each key will be determined by the * overall size of the keyboard. If not defined here, the width of each * key will default to 1. * * @type {Object.} */ this.keyWidths = template.keyWidths || {}; }; /** * Represents a single key, or a single possible behavior of a key. Each key * on the on-screen keyboard must have at least one associated * Guacamole.OnScreenKeyboard.Key, whether that key is explicitly defined or * implied, and may have multiple Guacamole.OnScreenKeyboard.Key if behavior * depends on modifier states. * * @constructor * @param {Guacamole.OnScreenKeyboard.Key|Object} template * The object whose identically-named properties will be used to initialize * the properties of this key. * * @param {String} [name] * The name to use instead of any name provided within the template, if * any. If omitted, the name within the template will be used, assuming the * template contains a name. */ Guacamole.OnScreenKeyboard.Key = function(template, name) { /** * The unique name identifying this key within the keyboard layout. * * @type {String} */ this.name = name || template.name; /** * The human-readable title that will be displayed to the user within the * key. If not provided, this will be derived from the key name. * * @type {String} */ this.title = template.title || this.name; /** * The keysym to be pressed/released when this key is pressed/released. If * not provided, this will be derived from the title if the title is a * single character. * * @type {Number} */ this.keysym = template.keysym || (function deriveKeysym(title) { // Do not derive keysym if title is not exactly one character if (!title || title.length !== 1) return null; // For characters between U+0000 and U+00FF, the keysym is the codepoint var charCode = title.charCodeAt(0); if (charCode >= 0x0000 && charCode <= 0x00FF) return charCode; // For characters between U+0100 and U+10FFFF, the keysym is the codepoint or'd with 0x01000000 if (charCode >= 0x0100 && charCode <= 0x10FFFF) return 0x01000000 | charCode; // Unable to derive keysym return null; })(this.title); /** * The name of the modifier set when the key is pressed and cleared when * this key is released, if any. The names of modifiers are distinct from * the names of keys; both the "RightShift" and "LeftShift" keys may set * the "shift" modifier, for example. By default, the key will affect no * modifiers. * * @type {String} */ this.modifier = template.modifier; /** * An array containing the names of each modifier required for this key to * have an effect. For example, a lowercase letter may require nothing, * while an uppercase letter would require "shift", assuming the Shift key * is named "shift" within the layout. By default, the key will require * no modifiers. * * @type {String[]} */ this.requires = template.requires || []; }; guacamole-client-0.9.9/guacamole-common-js/src/main/webapp/modules/Status.js0100664 0001751 0001751 00000011467 12645510255 030664 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ var Guacamole = Guacamole || {}; /** * A Guacamole status. Each Guacamole status consists of a status code, defined * by the protocol, and an optional human-readable message, usually only * included for debugging convenience. * * @constructor * @param {Number} code * The Guacamole status code, as defined by Guacamole.Status.Code. * * @param {String} [message] * An optional human-readable message. */ Guacamole.Status = function(code, message) { /** * Reference to this Guacamole.Status. * @private */ var guac_status = this; /** * The Guacamole status code. * @see Guacamole.Status.Code * @type {Number} */ this.code = code; /** * An arbitrary human-readable message associated with this status, if any. * The human-readable message is not required, and is generally provided * for debugging purposes only. For user feedback, it is better to translate * the Guacamole status code into a message. * * @type {String} */ this.message = message; /** * Returns whether this status represents an error. * @returns {Boolean} true if this status represents an error, false * otherwise. */ this.isError = function() { return guac_status.code < 0 || guac_status.code > 0x00FF; }; }; /** * Enumeration of all Guacamole status codes. */ Guacamole.Status.Code = { /** * The operation succeeded. * * @type {Number} */ "SUCCESS": 0x0000, /** * The requested operation is unsupported. * * @type {Number} */ "UNSUPPORTED": 0x0100, /** * The operation could not be performed due to an internal failure. * * @type {Number} */ "SERVER_ERROR": 0x0200, /** * The operation could not be performed as the server is busy. * * @type {Number} */ "SERVER_BUSY": 0x0201, /** * The operation could not be performed because the upstream server is not * responding. * * @type {Number} */ "UPSTREAM_TIMEOUT": 0x0202, /** * The operation was unsuccessful due to an error or otherwise unexpected * condition of the upstream server. * * @type {Number} */ "UPSTREAM_ERROR": 0x0203, /** * The operation could not be performed as the requested resource does not * exist. * * @type {Number} */ "RESOURCE_NOT_FOUND": 0x0204, /** * The operation could not be performed as the requested resource is * already in use. * * @type {Number} */ "RESOURCE_CONFLICT": 0x0205, /** * The operation could not be performed because bad parameters were given. * * @type {Number} */ "CLIENT_BAD_REQUEST": 0x0300, /** * Permission was denied to perform the operation, as the user is not yet * authorized (not yet logged in, for example). * * @type {Number} */ "CLIENT_UNAUTHORIZED": 0x0301, /** * Permission was denied to perform the operation, and this permission will * not be granted even if the user is authorized. * * @type {Number} */ "CLIENT_FORBIDDEN": 0x0303, /** * The client took too long to respond. * * @type {Number} */ "CLIENT_TIMEOUT": 0x0308, /** * The client sent too much data. * * @type {Number} */ "CLIENT_OVERRUN": 0x030D, /** * The client sent data of an unsupported or unexpected type. * * @type {Number} */ "CLIENT_BAD_TYPE": 0x030F, /** * The operation failed because the current client is already using too * many resources. * * @type {Number} */ "CLIENT_TOO_MANY": 0x031D }; guacamole-client-0.9.9/guacamole-common-js/src/main/webapp/modules/Tunnel.js0100664 0001751 0001751 00000073165 12645510255 030651 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ var Guacamole = Guacamole || {}; /** * Core object providing abstract communication for Guacamole. This object * is a null implementation whose functions do nothing. Guacamole applications * should use {@link Guacamole.HTTPTunnel} instead, or implement their own tunnel based * on this one. * * @constructor * @see Guacamole.HTTPTunnel */ Guacamole.Tunnel = function() { /** * Connect to the tunnel with the given optional data. This data is * typically used for authentication. The format of data accepted is * up to the tunnel implementation. * * @param {String} data The data to send to the tunnel when connecting. */ this.connect = function(data) {}; /** * Disconnect from the tunnel. */ this.disconnect = function() {}; /** * Send the given message through the tunnel to the service on the other * side. All messages are guaranteed to be received in the order sent. * * @param {...*} elements * The elements of the message to send to the service on the other side * of the tunnel. */ this.sendMessage = function(elements) {}; /** * The current state of this tunnel. * * @type {Number} */ this.state = Guacamole.Tunnel.State.CONNECTING; /** * The maximum amount of time to wait for data to be received, in * milliseconds. If data is not received within this amount of time, * the tunnel is closed with an error. The default value is 15000. * * @type {Number} */ this.receiveTimeout = 15000; /** * Fired whenever an error is encountered by the tunnel. * * @event * @param {Guacamole.Status} status A status object which describes the * error. */ this.onerror = null; /** * Fired whenever the state of the tunnel changes. * * @event * @param {Number} state The new state of the client. */ this.onstatechange = null; /** * Fired once for every complete Guacamole instruction received, in order. * * @event * @param {String} opcode The Guacamole instruction opcode. * @param {Array} parameters The parameters provided for the instruction, * if any. */ this.oninstruction = null; }; /** * All possible tunnel states. */ Guacamole.Tunnel.State = { /** * A connection is in pending. It is not yet known whether connection was * successful. * * @type {Number} */ "CONNECTING": 0, /** * Connection was successful, and data is being received. * * @type {Number} */ "OPEN": 1, /** * The connection is closed. Connection may not have been successful, the * tunnel may have been explicitly closed by either side, or an error may * have occurred. * * @type {Number} */ "CLOSED": 2 }; /** * Guacamole Tunnel implemented over HTTP via XMLHttpRequest. * * @constructor * @augments Guacamole.Tunnel * * @param {String} tunnelURL * The URL of the HTTP tunneling service. * * @param {Boolean} [crossDomain=false] * Whether tunnel requests will be cross-domain, and thus must use CORS * mechanisms and headers. By default, it is assumed that tunnel requests * will be made to the same domain. */ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain) { /** * Reference to this HTTP tunnel. * @private */ var tunnel = this; var tunnel_uuid; var TUNNEL_CONNECT = tunnelURL + "?connect"; var TUNNEL_READ = tunnelURL + "?read:"; var TUNNEL_WRITE = tunnelURL + "?write:"; var POLLING_ENABLED = 1; var POLLING_DISABLED = 0; // Default to polling - will be turned off automatically if not needed var pollingMode = POLLING_ENABLED; var sendingMessages = false; var outputMessageBuffer = ""; // If requests are expected to be cross-domain, the cookie that the HTTP // tunnel depends on will only be sent if withCredentials is true var withCredentials = !!crossDomain; /** * The current receive timeout ID, if any. * @private */ var receive_timeout = null; /** * Initiates a timeout which, if data is not received, causes the tunnel * to close with an error. * * @private */ function reset_timeout() { // Get rid of old timeout (if any) window.clearTimeout(receive_timeout); // Set new timeout receive_timeout = window.setTimeout(function () { close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout.")); }, tunnel.receiveTimeout); } /** * Closes this tunnel, signaling the given status and corresponding * message, which will be sent to the onerror handler if the status is * an error status. * * @private * @param {Guacamole.Status} status The status causing the connection to * close; */ function close_tunnel(status) { // Ignore if already closed if (tunnel.state === Guacamole.Tunnel.State.CLOSED) return; // If connection closed abnormally, signal error. if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror) { // Ignore RESOURCE_NOT_FOUND if we've already connected, as that // only signals end-of-stream for the HTTP tunnel. if (tunnel.state === Guacamole.Tunnel.State.CONNECTING || status.code !== Guacamole.Status.Code.RESOURCE_NOT_FOUND) tunnel.onerror(status); } // Mark as closed tunnel.state = Guacamole.Tunnel.State.CLOSED; // Reset output message buffer sendingMessages = false; if (tunnel.onstatechange) tunnel.onstatechange(tunnel.state); } this.sendMessage = function() { // Do not attempt to send messages if not connected if (tunnel.state !== Guacamole.Tunnel.State.OPEN) return; // Do not attempt to send empty messages if (arguments.length === 0) return; /** * Converts the given value to a length/string pair for use as an * element in a Guacamole instruction. * * @private * @param value The value to convert. * @return {String} The converted value. */ function getElement(value) { var string = new String(value); return string.length + "." + string; } // Initialized message with first element var message = getElement(arguments[0]); // Append remaining elements for (var i=1; i 0) { sendingMessages = true; var message_xmlhttprequest = new XMLHttpRequest(); message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid); message_xmlhttprequest.withCredentials = withCredentials; message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); // Once response received, send next queued event. message_xmlhttprequest.onreadystatechange = function() { if (message_xmlhttprequest.readyState === 4) { // If an error occurs during send, handle it if (message_xmlhttprequest.status !== 200) handleHTTPTunnelError(message_xmlhttprequest); // Otherwise, continue the send loop else sendPendingMessages(); } }; message_xmlhttprequest.send(outputMessageBuffer); outputMessageBuffer = ""; // Clear buffer } else sendingMessages = false; } function handleHTTPTunnelError(xmlhttprequest) { var code = parseInt(xmlhttprequest.getResponseHeader("Guacamole-Status-Code")); var message = xmlhttprequest.getResponseHeader("Guacamole-Error-Message"); close_tunnel(new Guacamole.Status(code, message)); } function handleResponse(xmlhttprequest) { var interval = null; var nextRequest = null; var dataUpdateEvents = 0; // The location of the last element's terminator var elementEnd = -1; // Where to start the next length search or the next element var startIndex = 0; // Parsed elements var elements = new Array(); function parseResponse() { // Do not handle responses if not connected if (tunnel.state !== Guacamole.Tunnel.State.OPEN) { // Clean up interval if polling if (interval !== null) clearInterval(interval); return; } // Do not parse response yet if not ready if (xmlhttprequest.readyState < 2) return; // Attempt to read status var status; try { status = xmlhttprequest.status; } // If status could not be read, assume successful. catch (e) { status = 200; } // Start next request as soon as possible IF request was successful if (!nextRequest && status === 200) nextRequest = makeRequest(); // Parse stream when data is received and when complete. if (xmlhttprequest.readyState === 3 || xmlhttprequest.readyState === 4) { reset_timeout(); // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data) if (pollingMode === POLLING_ENABLED) { if (xmlhttprequest.readyState === 3 && !interval) interval = setInterval(parseResponse, 30); else if (xmlhttprequest.readyState === 4 && !interval) clearInterval(interval); } // If canceled, stop transfer if (xmlhttprequest.status === 0) { tunnel.disconnect(); return; } // Halt on error during request else if (xmlhttprequest.status !== 200) { handleHTTPTunnelError(xmlhttprequest); return; } // Attempt to read in-progress data var current; try { current = xmlhttprequest.responseText; } // Do not attempt to parse if data could not be read catch (e) { return; } // While search is within currently received data while (elementEnd < current.length) { // If we are waiting for element data if (elementEnd >= startIndex) { // We now have enough data for the element. Parse. var element = current.substring(startIndex, elementEnd); var terminator = current.substring(elementEnd, elementEnd+1); // Add element to array elements.push(element); // If last element, handle instruction if (terminator === ";") { // Get opcode var opcode = elements.shift(); // Call instruction handler. if (tunnel.oninstruction) tunnel.oninstruction(opcode, elements); // Clear elements elements.length = 0; } // Start searching for length at character after // element terminator startIndex = elementEnd + 1; } // Search for end of length var lengthEnd = current.indexOf(".", startIndex); if (lengthEnd !== -1) { // Parse length var length = parseInt(current.substring(elementEnd+1, lengthEnd)); // If we're done parsing, handle the next response. if (length === 0) { // Clean up interval if polling if (!interval) clearInterval(interval); // Clean up object xmlhttprequest.onreadystatechange = null; xmlhttprequest.abort(); // Start handling next request if (nextRequest) handleResponse(nextRequest); // Done parsing break; } // Calculate start of element startIndex = lengthEnd + 1; // Calculate location of element terminator elementEnd = startIndex + length; } // If no period yet, continue search when more data // is received else { startIndex = current.length; break; } } // end parse loop } } // If response polling enabled, attempt to detect if still // necessary (via wrapping parseResponse()) if (pollingMode === POLLING_ENABLED) { xmlhttprequest.onreadystatechange = function() { // If we receive two or more readyState==3 events, // there is no need to poll. if (xmlhttprequest.readyState === 3) { dataUpdateEvents++; if (dataUpdateEvents >= 2) { pollingMode = POLLING_DISABLED; xmlhttprequest.onreadystatechange = parseResponse; } } parseResponse(); }; } // Otherwise, just parse else xmlhttprequest.onreadystatechange = parseResponse; parseResponse(); } /** * Arbitrary integer, unique for each tunnel read request. * @private */ var request_id = 0; function makeRequest() { // Make request, increment request ID var xmlhttprequest = new XMLHttpRequest(); xmlhttprequest.open("GET", TUNNEL_READ + tunnel_uuid + ":" + (request_id++)); xmlhttprequest.withCredentials = withCredentials; xmlhttprequest.send(null); return xmlhttprequest; } this.connect = function(data) { // Start waiting for connect reset_timeout(); // Start tunnel and connect var connect_xmlhttprequest = new XMLHttpRequest(); connect_xmlhttprequest.onreadystatechange = function() { if (connect_xmlhttprequest.readyState !== 4) return; // If failure, throw error if (connect_xmlhttprequest.status !== 200) { handleHTTPTunnelError(connect_xmlhttprequest); return; } reset_timeout(); // Get UUID from response tunnel_uuid = connect_xmlhttprequest.responseText; tunnel.state = Guacamole.Tunnel.State.OPEN; if (tunnel.onstatechange) tunnel.onstatechange(tunnel.state); // Start reading data handleResponse(makeRequest()); }; connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, true); connect_xmlhttprequest.withCredentials = withCredentials; connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); connect_xmlhttprequest.send(data); }; this.disconnect = function() { close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed.")); }; }; Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel(); /** * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest. * * @constructor * @augments Guacamole.Tunnel * @param {String} tunnelURL The URL of the WebSocket tunneling service. */ Guacamole.WebSocketTunnel = function(tunnelURL) { /** * Reference to this WebSocket tunnel. * @private */ var tunnel = this; /** * The WebSocket used by this tunnel. * @private */ var socket = null; /** * The current receive timeout ID, if any. * @private */ var receive_timeout = null; /** * The WebSocket protocol corresponding to the protocol used for the current * location. * @private */ var ws_protocol = { "http:": "ws:", "https:": "wss:" }; // Transform current URL to WebSocket URL // If not already a websocket URL if ( tunnelURL.substring(0, 3) !== "ws:" && tunnelURL.substring(0, 4) !== "wss:") { var protocol = ws_protocol[window.location.protocol]; // If absolute URL, convert to absolute WS URL if (tunnelURL.substring(0, 1) === "/") tunnelURL = protocol + "//" + window.location.host + tunnelURL; // Otherwise, construct absolute from relative URL else { // Get path from pathname var slash = window.location.pathname.lastIndexOf("/"); var path = window.location.pathname.substring(0, slash + 1); // Construct absolute URL tunnelURL = protocol + "//" + window.location.host + path + tunnelURL; } } /** * Initiates a timeout which, if data is not received, causes the tunnel * to close with an error. * * @private */ function reset_timeout() { // Get rid of old timeout (if any) window.clearTimeout(receive_timeout); // Set new timeout receive_timeout = window.setTimeout(function () { close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout.")); }, tunnel.receiveTimeout); } /** * Closes this tunnel, signaling the given status and corresponding * message, which will be sent to the onerror handler if the status is * an error status. * * @private * @param {Guacamole.Status} status The status causing the connection to * close; */ function close_tunnel(status) { // Ignore if already closed if (tunnel.state === Guacamole.Tunnel.State.CLOSED) return; // If connection closed abnormally, signal error. if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror) tunnel.onerror(status); // Mark as closed tunnel.state = Guacamole.Tunnel.State.CLOSED; if (tunnel.onstatechange) tunnel.onstatechange(tunnel.state); socket.close(); } this.sendMessage = function(elements) { // Do not attempt to send messages if not connected if (tunnel.state !== Guacamole.Tunnel.State.OPEN) return; // Do not attempt to send empty messages if (arguments.length === 0) return; /** * Converts the given value to a length/string pair for use as an * element in a Guacamole instruction. * * @private * @param value The value to convert. * @return {String} The converted value. */ function getElement(value) { var string = new String(value); return string.length + "." + string; } // Initialized message with first element var message = getElement(arguments[0]); // Append remaining elements for (var i=1; i 0) return pool.shift(); // Otherwise, return a new integer return guac_pool.next_int++; }; /** * Frees the given integer, allowing it to be reused. * * @param {Number} integer The integer to free. */ this.free = function(integer) { pool.push(integer); }; }; guacamole-client-0.9.9/guacamole-common-js/src/main/webapp/modules/OutputStream.js0100664 0001751 0001751 00000004311 12645510255 032043 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ var Guacamole = Guacamole || {}; /** * Abstract stream which can receive data. * * @constructor * @param {Guacamole.Client} client The client owning this stream. * @param {Number} index The index of this stream. */ Guacamole.OutputStream = function(client, index) { /** * Reference to this stream. * @private */ var guac_stream = this; /** * The index of this stream. * @type {Number} */ this.index = index; /** * Fired whenever an acknowledgement is received from the server, indicating * that a stream operation has completed, or an error has occurred. * * @event * @param {Guacamole.Status} status The status of the operation. */ this.onack = null; /** * Writes the given base64-encoded data to this stream as a blob. * * @param {String} data The base64-encoded data to send. */ this.sendBlob = function(data) { client.sendBlob(guac_stream.index, data); }; /** * Closes this stream. */ this.sendEnd = function() { client.endStream(guac_stream.index); }; }; guacamole-client-0.9.9/guacamole-common-js/src/main/webapp/modules/BlobReader.js0100664 0001751 0001751 00000010051 12562523242 031364 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ var Guacamole = Guacamole || {}; /** * A reader which automatically handles the given input stream, assembling all * received blobs into a single blob by appending them to each other in order. * Note that this object will overwrite any installed event handlers on the * given Guacamole.InputStream. * * @constructor * @param {Guacamole.InputStream} stream The stream that data will be read * from. * @param {String} mimetype The mimetype of the blob being built. */ Guacamole.BlobReader = function(stream, mimetype) { /** * Reference to this Guacamole.InputStream. * @private */ var guac_reader = this; /** * The length of this Guacamole.InputStream in bytes. * @private */ var length = 0; // Get blob builder var blob_builder; if (window.BlobBuilder) blob_builder = new BlobBuilder(); else if (window.WebKitBlobBuilder) blob_builder = new WebKitBlobBuilder(); else if (window.MozBlobBuilder) blob_builder = new MozBlobBuilder(); else blob_builder = new (function() { var blobs = []; /** @ignore */ this.append = function(data) { blobs.push(new Blob([data], {"type": mimetype})); }; /** @ignore */ this.getBlob = function() { return new Blob(blobs, {"type": mimetype}); }; })(); // Append received blobs stream.onblob = function(data) { // Convert to ArrayBuffer var binary = window.atob(data); var arrayBuffer = new ArrayBuffer(binary.length); var bufferView = new Uint8Array(arrayBuffer); for (var i=0; i= guac_mouse.scrollThreshold) { // Repeatedly click the down button until insufficient delta remains do { if (guac_mouse.onmousedown) { guac_mouse.currentState.down = true; guac_mouse.onmousedown(guac_mouse.currentState); } if (guac_mouse.onmouseup) { guac_mouse.currentState.down = false; guac_mouse.onmouseup(guac_mouse.currentState); } scroll_delta -= guac_mouse.scrollThreshold; } while (scroll_delta >= guac_mouse.scrollThreshold); // Reset delta scroll_delta = 0; } cancelEvent(e); } element.addEventListener('DOMMouseScroll', mousewheel_handler, false); element.addEventListener('mousewheel', mousewheel_handler, false); element.addEventListener('wheel', mousewheel_handler, false); /** * Whether the browser supports CSS3 cursor styling, including hotspot * coordinates. * * @private * @type {Boolean} */ var CSS3_CURSOR_SUPPORTED = (function() { var div = document.createElement("div"); // If no cursor property at all, then no support if (!("cursor" in div.style)) return false; try { // Apply simple 1x1 PNG div.style.cursor = "url(data:image/png;base64," + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB" + "AQMAAAAl21bKAAAAA1BMVEX///+nxBvI" + "AAAACklEQVQI12NgAAAAAgAB4iG8MwAA" + "AABJRU5ErkJggg==) 0 0, auto"; } catch (e) { return false; } // Verify cursor property is set to URL with hotspot return /\burl\([^()]*\)\s+0\s+0\b/.test(div.style.cursor || ""); })(); /** * Changes the local mouse cursor to the given canvas, having the given * hotspot coordinates. This affects styling of the element backing this * Guacamole.Mouse only, and may fail depending on browser support for * setting the mouse cursor. * * If setting the local cursor is desired, it is up to the implementation * to do something else, such as use the software cursor built into * Guacamole.Display, if the local cursor cannot be set. * * @param {HTMLCanvasElement} canvas The cursor image. * @param {Number} x The X-coordinate of the cursor hotspot. * @param {Number} y The Y-coordinate of the cursor hotspot. * @return {Boolean} true if the cursor was successfully set, false if the * cursor could not be set for any reason. */ this.setCursor = function(canvas, x, y) { // Attempt to set via CSS3 cursor styling if (CSS3_CURSOR_SUPPORTED) { var dataURL = canvas.toDataURL('image/png'); element.style.cursor = "url(" + dataURL + ") " + x + " " + y + ", auto"; return true; } // Otherwise, setting cursor failed return false; }; }; /** * Simple container for properties describing the state of a mouse. * * @constructor * @param {Number} x The X position of the mouse pointer in pixels. * @param {Number} y The Y position of the mouse pointer in pixels. * @param {Boolean} left Whether the left mouse button is pressed. * @param {Boolean} middle Whether the middle mouse button is pressed. * @param {Boolean} right Whether the right mouse button is pressed. * @param {Boolean} up Whether the up mouse button is pressed (the fourth * button, usually part of a scroll wheel). * @param {Boolean} down Whether the down mouse button is pressed (the fifth * button, usually part of a scroll wheel). */ Guacamole.Mouse.State = function(x, y, left, middle, right, up, down) { /** * Reference to this Guacamole.Mouse.State. * @private */ var guac_state = this; /** * The current X position of the mouse pointer. * @type {Number} */ this.x = x; /** * The current Y position of the mouse pointer. * @type {Number} */ this.y = y; /** * Whether the left mouse button is currently pressed. * @type {Boolean} */ this.left = left; /** * Whether the middle mouse button is currently pressed. * @type {Boolean} */ this.middle = middle; /** * Whether the right mouse button is currently pressed. * @type {Boolean} */ this.right = right; /** * Whether the up mouse button is currently pressed. This is the fourth * mouse button, associated with upward scrolling of the mouse scroll * wheel. * @type {Boolean} */ this.up = up; /** * Whether the down mouse button is currently pressed. This is the fifth * mouse button, associated with downward scrolling of the mouse scroll * wheel. * @type {Boolean} */ this.down = down; /** * Updates the position represented within this state object by the given * element and clientX/clientY coordinates (commonly available within event * objects). Position is translated from clientX/clientY (relative to * viewport) to element-relative coordinates. * * @param {Element} element The element the coordinates should be relative * to. * @param {Number} clientX The X coordinate to translate, viewport-relative. * @param {Number} clientY The Y coordinate to translate, viewport-relative. */ this.fromClientPosition = function(element, clientX, clientY) { guac_state.x = clientX - element.offsetLeft; guac_state.y = clientY - element.offsetTop; // This is all JUST so we can get the mouse position within the element var parent = element.offsetParent; while (parent && !(parent === document.body)) { guac_state.x -= parent.offsetLeft - parent.scrollLeft; guac_state.y -= parent.offsetTop - parent.scrollTop; parent = parent.offsetParent; } // Element ultimately depends on positioning within document body, // take document scroll into account. if (parent) { var documentScrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft; var documentScrollTop = document.body.scrollTop || document.documentElement.scrollTop; guac_state.x -= parent.offsetLeft - documentScrollLeft; guac_state.y -= parent.offsetTop - documentScrollTop; } }; }; /** * Provides cross-browser relative touch event translation for a given element. * * Touch events are translated into mouse events as if the touches occurred * on a touchpad (drag to push the mouse pointer, tap to click). * * @constructor * @param {Element} element The Element to use to provide touch events. */ Guacamole.Mouse.Touchpad = function(element) { /** * Reference to this Guacamole.Mouse.Touchpad. * @private */ var guac_touchpad = this; /** * The distance a two-finger touch must move per scrollwheel event, in * pixels. */ this.scrollThreshold = 20 * (window.devicePixelRatio || 1); /** * The maximum number of milliseconds to wait for a touch to end for the * gesture to be considered a click. */ this.clickTimingThreshold = 250; /** * The maximum number of pixels to allow a touch to move for the gesture to * be considered a click. */ this.clickMoveThreshold = 10 * (window.devicePixelRatio || 1); /** * The current mouse state. The properties of this state are updated when * mouse events fire. This state object is also passed in as a parameter to * the handler of any mouse events. * * @type {Guacamole.Mouse.State} */ this.currentState = new Guacamole.Mouse.State( 0, 0, false, false, false, false, false ); /** * Fired whenever a mouse button is effectively pressed. This can happen * as part of a "click" gesture initiated by the user by tapping one * or more fingers over the touchpad element, as part of a "scroll" * gesture initiated by dragging two fingers up or down, etc. * * @event * @param {Guacamole.Mouse.State} state The current mouse state. */ this.onmousedown = null; /** * Fired whenever a mouse button is effectively released. This can happen * as part of a "click" gesture initiated by the user by tapping one * or more fingers over the touchpad element, as part of a "scroll" * gesture initiated by dragging two fingers up or down, etc. * * @event * @param {Guacamole.Mouse.State} state The current mouse state. */ this.onmouseup = null; /** * Fired whenever the user moves the mouse by dragging their finger over * the touchpad element. * * @event * @param {Guacamole.Mouse.State} state The current mouse state. */ this.onmousemove = null; var touch_count = 0; var last_touch_x = 0; var last_touch_y = 0; var last_touch_time = 0; var pixels_moved = 0; var touch_buttons = { 1: "left", 2: "right", 3: "middle" }; var gesture_in_progress = false; var click_release_timeout = null; element.addEventListener("touchend", function(e) { e.preventDefault(); // If we're handling a gesture AND this is the last touch if (gesture_in_progress && e.touches.length === 0) { var time = new Date().getTime(); // Get corresponding mouse button var button = touch_buttons[touch_count]; // If mouse already down, release anad clear timeout if (guac_touchpad.currentState[button]) { // Fire button up event guac_touchpad.currentState[button] = false; if (guac_touchpad.onmouseup) guac_touchpad.onmouseup(guac_touchpad.currentState); // Clear timeout, if set if (click_release_timeout) { window.clearTimeout(click_release_timeout); click_release_timeout = null; } } // If single tap detected (based on time and distance) if (time - last_touch_time <= guac_touchpad.clickTimingThreshold && pixels_moved < guac_touchpad.clickMoveThreshold) { // Fire button down event guac_touchpad.currentState[button] = true; if (guac_touchpad.onmousedown) guac_touchpad.onmousedown(guac_touchpad.currentState); // Delay mouse up - mouse up should be canceled if // touchstart within timeout. click_release_timeout = window.setTimeout(function() { // Fire button up event guac_touchpad.currentState[button] = false; if (guac_touchpad.onmouseup) guac_touchpad.onmouseup(guac_touchpad.currentState); // Gesture now over gesture_in_progress = false; }, guac_touchpad.clickTimingThreshold); } // If we're not waiting to see if this is a click, stop gesture if (!click_release_timeout) gesture_in_progress = false; } }, false); element.addEventListener("touchstart", function(e) { e.preventDefault(); // Track number of touches, but no more than three touch_count = Math.min(e.touches.length, 3); // Clear timeout, if set if (click_release_timeout) { window.clearTimeout(click_release_timeout); click_release_timeout = null; } // Record initial touch location and time for touch movement // and tap gestures if (!gesture_in_progress) { // Stop mouse events while touching gesture_in_progress = true; // Record touch location and time var starting_touch = e.touches[0]; last_touch_x = starting_touch.clientX; last_touch_y = starting_touch.clientY; last_touch_time = new Date().getTime(); pixels_moved = 0; } }, false); element.addEventListener("touchmove", function(e) { e.preventDefault(); // Get change in touch location var touch = e.touches[0]; var delta_x = touch.clientX - last_touch_x; var delta_y = touch.clientY - last_touch_y; // Track pixels moved pixels_moved += Math.abs(delta_x) + Math.abs(delta_y); // If only one touch involved, this is mouse move if (touch_count === 1) { // Calculate average velocity in Manhatten pixels per millisecond var velocity = pixels_moved / (new Date().getTime() - last_touch_time); // Scale mouse movement relative to velocity var scale = 1 + velocity; // Update mouse location guac_touchpad.currentState.x += delta_x*scale; guac_touchpad.currentState.y += delta_y*scale; // Prevent mouse from leaving screen if (guac_touchpad.currentState.x < 0) guac_touchpad.currentState.x = 0; else if (guac_touchpad.currentState.x >= element.offsetWidth) guac_touchpad.currentState.x = element.offsetWidth - 1; if (guac_touchpad.currentState.y < 0) guac_touchpad.currentState.y = 0; else if (guac_touchpad.currentState.y >= element.offsetHeight) guac_touchpad.currentState.y = element.offsetHeight - 1; // Fire movement event, if defined if (guac_touchpad.onmousemove) guac_touchpad.onmousemove(guac_touchpad.currentState); // Update touch location last_touch_x = touch.clientX; last_touch_y = touch.clientY; } // Interpret two-finger swipe as scrollwheel else if (touch_count === 2) { // If change in location passes threshold for scroll if (Math.abs(delta_y) >= guac_touchpad.scrollThreshold) { // Decide button based on Y movement direction var button; if (delta_y > 0) button = "down"; else button = "up"; // Fire button down event guac_touchpad.currentState[button] = true; if (guac_touchpad.onmousedown) guac_touchpad.onmousedown(guac_touchpad.currentState); // Fire button up event guac_touchpad.currentState[button] = false; if (guac_touchpad.onmouseup) guac_touchpad.onmouseup(guac_touchpad.currentState); // Only update touch location after a scroll has been // detected last_touch_x = touch.clientX; last_touch_y = touch.clientY; } } }, false); }; /** * Provides cross-browser absolute touch event translation for a given element. * * Touch events are translated into mouse events as if the touches occurred * on a touchscreen (tapping anywhere on the screen clicks at that point, * long-press to right-click). * * @constructor * @param {Element} element The Element to use to provide touch events. */ Guacamole.Mouse.Touchscreen = function(element) { /** * Reference to this Guacamole.Mouse.Touchscreen. * @private */ var guac_touchscreen = this; /** * Whether a gesture is known to be in progress. If false, touch events * will be ignored. * * @private */ var gesture_in_progress = false; /** * The start X location of a gesture. * @private */ var gesture_start_x = null; /** * The start Y location of a gesture. * @private */ var gesture_start_y = null; /** * The timeout associated with the delayed, cancellable click release. * * @private */ var click_release_timeout = null; /** * The timeout associated with long-press for right click. * * @private */ var long_press_timeout = null; /** * The distance a two-finger touch must move per scrollwheel event, in * pixels. */ this.scrollThreshold = 20 * (window.devicePixelRatio || 1); /** * The maximum number of milliseconds to wait for a touch to end for the * gesture to be considered a click. */ this.clickTimingThreshold = 250; /** * The maximum number of pixels to allow a touch to move for the gesture to * be considered a click. */ this.clickMoveThreshold = 16 * (window.devicePixelRatio || 1); /** * The amount of time a press must be held for long press to be * detected. */ this.longPressThreshold = 500; /** * The current mouse state. The properties of this state are updated when * mouse events fire. This state object is also passed in as a parameter to * the handler of any mouse events. * * @type {Guacamole.Mouse.State} */ this.currentState = new Guacamole.Mouse.State( 0, 0, false, false, false, false, false ); /** * Fired whenever a mouse button is effectively pressed. This can happen * as part of a "mousedown" gesture initiated by the user by pressing one * finger over the touchscreen element, as part of a "scroll" gesture * initiated by dragging two fingers up or down, etc. * * @event * @param {Guacamole.Mouse.State} state The current mouse state. */ this.onmousedown = null; /** * Fired whenever a mouse button is effectively released. This can happen * as part of a "mouseup" gesture initiated by the user by removing the * finger pressed against the touchscreen element, or as part of a "scroll" * gesture initiated by dragging two fingers up or down, etc. * * @event * @param {Guacamole.Mouse.State} state The current mouse state. */ this.onmouseup = null; /** * Fired whenever the user moves the mouse by dragging their finger over * the touchscreen element. Note that unlike Guacamole.Mouse.Touchpad, * dragging a finger over the touchscreen element will always cause * the mouse button to be effectively down, as if clicking-and-dragging. * * @event * @param {Guacamole.Mouse.State} state The current mouse state. */ this.onmousemove = null; /** * Presses the given mouse button, if it isn't already pressed. Valid * button values are "left", "middle", "right", "up", and "down". * * @private * @param {String} button The mouse button to press. */ function press_button(button) { if (!guac_touchscreen.currentState[button]) { guac_touchscreen.currentState[button] = true; if (guac_touchscreen.onmousedown) guac_touchscreen.onmousedown(guac_touchscreen.currentState); } } /** * Releases the given mouse button, if it isn't already released. Valid * button values are "left", "middle", "right", "up", and "down". * * @private * @param {String} button The mouse button to release. */ function release_button(button) { if (guac_touchscreen.currentState[button]) { guac_touchscreen.currentState[button] = false; if (guac_touchscreen.onmouseup) guac_touchscreen.onmouseup(guac_touchscreen.currentState); } } /** * Clicks (presses and releases) the given mouse button. Valid button * values are "left", "middle", "right", "up", and "down". * * @private * @param {String} button The mouse button to click. */ function click_button(button) { press_button(button); release_button(button); } /** * Moves the mouse to the given coordinates. These coordinates must be * relative to the browser window, as they will be translated based on * the touch event target's location within the browser window. * * @private * @param {Number} x The X coordinate of the mouse pointer. * @param {Number} y The Y coordinate of the mouse pointer. */ function move_mouse(x, y) { guac_touchscreen.currentState.fromClientPosition(element, x, y); if (guac_touchscreen.onmousemove) guac_touchscreen.onmousemove(guac_touchscreen.currentState); } /** * Returns whether the given touch event exceeds the movement threshold for * clicking, based on where the touch gesture began. * * @private * @param {TouchEvent} e The touch event to check. * @return {Boolean} true if the movement threshold is exceeded, false * otherwise. */ function finger_moved(e) { var touch = e.touches[0] || e.changedTouches[0]; var delta_x = touch.clientX - gesture_start_x; var delta_y = touch.clientY - gesture_start_y; return Math.sqrt(delta_x*delta_x + delta_y*delta_y) >= guac_touchscreen.clickMoveThreshold; } /** * Begins a new gesture at the location of the first touch in the given * touch event. * * @private * @param {TouchEvent} e The touch event beginning this new gesture. */ function begin_gesture(e) { var touch = e.touches[0]; gesture_in_progress = true; gesture_start_x = touch.clientX; gesture_start_y = touch.clientY; } /** * End the current gesture entirely. Wait for all touches to be done before * resuming gesture detection. * * @private */ function end_gesture() { window.clearTimeout(click_release_timeout); window.clearTimeout(long_press_timeout); gesture_in_progress = false; } element.addEventListener("touchend", function(e) { // Do not handle if no gesture if (!gesture_in_progress) return; // Ignore if more than one touch if (e.touches.length !== 0 || e.changedTouches.length !== 1) { end_gesture(); return; } // Long-press, if any, is over window.clearTimeout(long_press_timeout); // Always release mouse button if pressed release_button("left"); // If finger hasn't moved enough to cancel the click if (!finger_moved(e)) { e.preventDefault(); // If not yet pressed, press and start delay release if (!guac_touchscreen.currentState.left) { var touch = e.changedTouches[0]; move_mouse(touch.clientX, touch.clientY); press_button("left"); // Release button after a delay, if not canceled click_release_timeout = window.setTimeout(function() { release_button("left"); end_gesture(); }, guac_touchscreen.clickTimingThreshold); } } // end if finger not moved }, false); element.addEventListener("touchstart", function(e) { // Ignore if more than one touch if (e.touches.length !== 1) { end_gesture(); return; } e.preventDefault(); // New touch begins a new gesture begin_gesture(e); // Keep button pressed if tap after left click window.clearTimeout(click_release_timeout); // Click right button if this turns into a long-press long_press_timeout = window.setTimeout(function() { var touch = e.touches[0]; move_mouse(touch.clientX, touch.clientY); click_button("right"); end_gesture(); }, guac_touchscreen.longPressThreshold); }, false); element.addEventListener("touchmove", function(e) { // Do not handle if no gesture if (!gesture_in_progress) return; // Cancel long press if finger moved if (finger_moved(e)) window.clearTimeout(long_press_timeout); // Ignore if more than one touch if (e.touches.length !== 1) { end_gesture(); return; } // Update mouse position if dragging if (guac_touchscreen.currentState.left) { e.preventDefault(); // Update state var touch = e.touches[0]; move_mouse(touch.clientX, touch.clientY); } }, false); }; guacamole-client-0.9.9/guacamole-common-js/src/main/webapp/modules/DataURIReader.js0100664 0001751 0001751 00000005410 12645510255 031744 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ var Guacamole = Guacamole || {}; /** * A reader which automatically handles the given input stream, returning * received blobs as a single data URI built over the course of the stream. * Note that this object will overwrite any installed event handlers on the * given Guacamole.InputStream. * * @constructor * @param {Guacamole.InputStream} stream * The stream that data will be read from. */ Guacamole.DataURIReader = function(stream, mimetype) { /** * Reference to this Guacamole.DataURIReader. * @private */ var guac_reader = this; /** * Current data URI. * * @private * @type {String} */ var uri = 'data:' + mimetype + ';base64,'; // Receive blobs as array buffers stream.onblob = function dataURIReaderBlob(data) { // Currently assuming data will ALWAYS be safe to simply append. This // will not be true if the received base64 data encodes a number of // bytes that isn't a multiple of three (as base64 expands in a ratio // of exactly 3:4). uri += data; }; // Simply call onend when end received stream.onend = function dataURIReaderEnd() { if (guac_reader.onend) guac_reader.onend(); }; /** * Returns the data URI of all data received through the underlying stream * thus far. * * @returns {String} * The data URI of all data received through the underlying stream thus * far. */ this.getURI = function getURI() { return uri; }; /** * Fired once this stream is finished and no further data will be written. * * @event */ this.onend = null; };guacamole-client-0.9.9/guacamole-common-js/src/main/webapp/modules/InputStream.js0100664 0001751 0001751 00000004367 12645510255 031655 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ var Guacamole = Guacamole || {}; /** * An input stream abstraction used by the Guacamole client to facilitate * transfer of files or other binary data. * * @constructor * @param {Guacamole.Client} client The client owning this stream. * @param {Number} index The index of this stream. */ Guacamole.InputStream = function(client, index) { /** * Reference to this stream. * @private */ var guac_stream = this; /** * The index of this stream. * @type {Number} */ this.index = index; /** * Called when a blob of data is received. * * @event * @param {String} data The received base64 data. */ this.onblob = null; /** * Called when this stream is closed. * * @event */ this.onend = null; /** * Acknowledges the receipt of a blob. * * @param {String} message A human-readable message describing the error * or status. * @param {Number} code The error code, if any, or 0 for success. */ this.sendAck = function(message, code) { client.sendAck(guac_stream.index, message, code); }; }; guacamole-client-0.9.9/guacamole-common-js/src/main/webapp/modules/Keyboard.js0100664 0001751 0001751 00000112161 12645510255 031132 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ var Guacamole = Guacamole || {}; /** * Provides cross-browser and cross-keyboard keyboard for a specific element. * Browser and keyboard layout variation is abstracted away, providing events * which represent keys as their corresponding X11 keysym. * * @constructor * @param {Element} element The Element to use to provide keyboard events. */ Guacamole.Keyboard = function(element) { /** * Reference to this Guacamole.Keyboard. * @private */ var guac_keyboard = this; /** * Fired whenever the user presses a key with the element associated * with this Guacamole.Keyboard in focus. * * @event * @param {Number} keysym The keysym of the key being pressed. * @return {Boolean} true if the key event should be allowed through to the * browser, false otherwise. */ this.onkeydown = null; /** * Fired whenever the user releases a key with the element associated * with this Guacamole.Keyboard in focus. * * @event * @param {Number} keysym The keysym of the key being released. */ this.onkeyup = null; /** * A key event having a corresponding timestamp. This event is non-specific. * Its subclasses should be used instead when recording specific key * events. * * @private * @constructor */ var KeyEvent = function() { /** * Reference to this key event. */ var key_event = this; /** * An arbitrary timestamp in milliseconds, indicating this event's * position in time relative to other events. * * @type {Number} */ this.timestamp = new Date().getTime(); /** * Whether the default action of this key event should be prevented. * * @type {Boolean} */ this.defaultPrevented = false; /** * The keysym of the key associated with this key event, as determined * by a best-effort guess using available event properties and keyboard * state. * * @type {Number} */ this.keysym = null; /** * Whether the keysym value of this key event is known to be reliable. * If false, the keysym may still be valid, but it's only a best guess, * and future key events may be a better source of information. * * @type {Boolean} */ this.reliable = false; /** * Returns the number of milliseconds elapsed since this event was * received. * * @return {Number} The number of milliseconds elapsed since this * event was received. */ this.getAge = function() { return new Date().getTime() - key_event.timestamp; }; }; /** * Information related to the pressing of a key, which need not be a key * associated with a printable character. The presence or absence of any * information within this object is browser-dependent. * * @private * @constructor * @augments Guacamole.Keyboard.KeyEvent * @param {Number} keyCode The JavaScript key code of the key pressed. * @param {String} keyIdentifier The legacy DOM3 "keyIdentifier" of the key * pressed, as defined at: * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent * @param {String} key The standard name of the key pressed, as defined at: * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent * @param {Number} location The location on the keyboard corresponding to * the key pressed, as defined at: * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent */ var KeydownEvent = function(keyCode, keyIdentifier, key, location) { // We extend KeyEvent KeyEvent.apply(this); /** * The JavaScript key code of the key pressed. * * @type {Number} */ this.keyCode = keyCode; /** * The legacy DOM3 "keyIdentifier" of the key pressed, as defined at: * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent * * @type {String} */ this.keyIdentifier = keyIdentifier; /** * The standard name of the key pressed, as defined at: * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent * * @type {String} */ this.key = key; /** * The location on the keyboard corresponding to the key pressed, as * defined at: * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent * * @type {Number} */ this.location = location; // If key is known from keyCode or DOM3 alone, use that this.keysym = keysym_from_key_identifier(key, location) || keysym_from_keycode(keyCode, location); // DOM3 and keyCode are reliable sources if (this.keysym) this.reliable = true; // Use legacy keyIdentifier as a last resort, if it looks sane if (!this.keysym && key_identifier_sane(keyCode, keyIdentifier)) this.keysym = keysym_from_key_identifier(keyIdentifier, location, guac_keyboard.modifiers.shift); // Determine whether default action for Alt+combinations must be prevented var prevent_alt = !guac_keyboard.modifiers.ctrl && !(navigator && navigator.platform && navigator.platform.match(/^mac/i)); // Determine whether default action for Ctrl+combinations must be prevented var prevent_ctrl = !guac_keyboard.modifiers.alt; // We must rely on the (potentially buggy) keyIdentifier if preventing // the default action is important if ((prevent_ctrl && guac_keyboard.modifiers.ctrl) || (prevent_alt && guac_keyboard.modifiers.alt) || guac_keyboard.modifiers.meta || guac_keyboard.modifiers.hyper) this.reliable = true; // Record most recently known keysym by associated key code recentKeysym[keyCode] = this.keysym; }; KeydownEvent.prototype = new KeyEvent(); /** * Information related to the pressing of a key, which MUST be * associated with a printable character. The presence or absence of any * information within this object is browser-dependent. * * @private * @constructor * @augments Guacamole.Keyboard.KeyEvent * @param {Number} charCode The Unicode codepoint of the character that * would be typed by the key pressed. */ var KeypressEvent = function(charCode) { // We extend KeyEvent KeyEvent.apply(this); /** * The Unicode codepoint of the character that would be typed by the * key pressed. * * @type {Number} */ this.charCode = charCode; // Pull keysym from char code this.keysym = keysym_from_charcode(charCode); // Keypress is always reliable this.reliable = true; }; KeypressEvent.prototype = new KeyEvent(); /** * Information related to the pressing of a key, which need not be a key * associated with a printable character. The presence or absence of any * information within this object is browser-dependent. * * @private * @constructor * @augments Guacamole.Keyboard.KeyEvent * @param {Number} keyCode The JavaScript key code of the key released. * @param {String} keyIdentifier The legacy DOM3 "keyIdentifier" of the key * released, as defined at: * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent * @param {String} key The standard name of the key released, as defined at: * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent * @param {Number} location The location on the keyboard corresponding to * the key released, as defined at: * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent */ var KeyupEvent = function(keyCode, keyIdentifier, key, location) { // We extend KeyEvent KeyEvent.apply(this); /** * The JavaScript key code of the key released. * * @type {Number} */ this.keyCode = keyCode; /** * The legacy DOM3 "keyIdentifier" of the key released, as defined at: * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent * * @type {String} */ this.keyIdentifier = keyIdentifier; /** * The standard name of the key released, as defined at: * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent * * @type {String} */ this.key = key; /** * The location on the keyboard corresponding to the key released, as * defined at: * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent * * @type {Number} */ this.location = location; // If key is known from keyCode or DOM3 alone, use that this.keysym = recentKeysym[keyCode] || keysym_from_keycode(keyCode, location) || keysym_from_key_identifier(key, location); // keyCode is still more reliable for keyup when dead keys are in use // Keyup is as reliable as it will ever be this.reliable = true; }; KeyupEvent.prototype = new KeyEvent(); /** * An array of recorded events, which can be instances of the private * KeydownEvent, KeypressEvent, and KeyupEvent classes. * * @private * @type {KeyEvent[]} */ var eventLog = []; /** * Map of known JavaScript keycodes which do not map to typable characters * to their X11 keysym equivalents. * @private */ var keycodeKeysyms = { 8: [0xFF08], // backspace 9: [0xFF09], // tab 12: [0xFF0B, 0xFF0B, 0xFF0B, 0xFFB5], // clear / KP 5 13: [0xFF0D], // enter 16: [0xFFE1, 0xFFE1, 0xFFE2], // shift 17: [0xFFE3, 0xFFE3, 0xFFE4], // ctrl 18: [0xFFE9, 0xFFE9, 0xFE03], // alt 19: [0xFF13], // pause/break 20: [0xFFE5], // caps lock 27: [0xFF1B], // escape 32: [0x0020], // space 33: [0xFF55, 0xFF55, 0xFF55, 0xFFB9], // page up / KP 9 34: [0xFF56, 0xFF56, 0xFF56, 0xFFB3], // page down / KP 3 35: [0xFF57, 0xFF57, 0xFF57, 0xFFB1], // end / KP 1 36: [0xFF50, 0xFF50, 0xFF50, 0xFFB7], // home / KP 7 37: [0xFF51, 0xFF51, 0xFF51, 0xFFB4], // left arrow / KP 4 38: [0xFF52, 0xFF52, 0xFF52, 0xFFB8], // up arrow / KP 8 39: [0xFF53, 0xFF53, 0xFF53, 0xFFB6], // right arrow / KP 6 40: [0xFF54, 0xFF54, 0xFF54, 0xFFB2], // down arrow / KP 2 45: [0xFF63, 0xFF63, 0xFF63, 0xFFB0], // insert / KP 0 46: [0xFFFF, 0xFFFF, 0xFFFF, 0xFFAE], // delete / KP decimal 91: [0xFFEB], // left window key (hyper_l) 92: [0xFF67], // right window key (menu key?) 93: null, // select key 96: [0xFFB0], // KP 0 97: [0xFFB1], // KP 1 98: [0xFFB2], // KP 2 99: [0xFFB3], // KP 3 100: [0xFFB4], // KP 4 101: [0xFFB5], // KP 5 102: [0xFFB6], // KP 6 103: [0xFFB7], // KP 7 104: [0xFFB8], // KP 8 105: [0xFFB9], // KP 9 106: [0xFFAA], // KP multiply 107: [0xFFAB], // KP add 109: [0xFFAD], // KP subtract 110: [0xFFAE], // KP decimal 111: [0xFFAF], // KP divide 112: [0xFFBE], // f1 113: [0xFFBF], // f2 114: [0xFFC0], // f3 115: [0xFFC1], // f4 116: [0xFFC2], // f5 117: [0xFFC3], // f6 118: [0xFFC4], // f7 119: [0xFFC5], // f8 120: [0xFFC6], // f9 121: [0xFFC7], // f10 122: [0xFFC8], // f11 123: [0xFFC9], // f12 144: [0xFF7F], // num lock 145: [0xFF14], // scroll lock 225: [0xFE03] // altgraph (iso_level3_shift) }; /** * Map of known JavaScript keyidentifiers which do not map to typable * characters to their unshifted X11 keysym equivalents. * @private */ var keyidentifier_keysym = { "Again": [0xFF66], "AllCandidates": [0xFF3D], "Alphanumeric": [0xFF30], "Alt": [0xFFE9, 0xFFE9, 0xFE03], "Attn": [0xFD0E], "AltGraph": [0xFE03], "ArrowDown": [0xFF54], "ArrowLeft": [0xFF51], "ArrowRight": [0xFF53], "ArrowUp": [0xFF52], "Backspace": [0xFF08], "CapsLock": [0xFFE5], "Cancel": [0xFF69], "Clear": [0xFF0B], "Convert": [0xFF21], "Copy": [0xFD15], "Crsel": [0xFD1C], "CrSel": [0xFD1C], "CodeInput": [0xFF37], "Compose": [0xFF20], "Control": [0xFFE3, 0xFFE3, 0xFFE4], "ContextMenu": [0xFF67], "DeadGrave": [0xFE50], "DeadAcute": [0xFE51], "DeadCircumflex": [0xFE52], "DeadTilde": [0xFE53], "DeadMacron": [0xFE54], "DeadBreve": [0xFE55], "DeadAboveDot": [0xFE56], "DeadUmlaut": [0xFE57], "DeadAboveRing": [0xFE58], "DeadDoubleacute": [0xFE59], "DeadCaron": [0xFE5A], "DeadCedilla": [0xFE5B], "DeadOgonek": [0xFE5C], "DeadIota": [0xFE5D], "DeadVoicedSound": [0xFE5E], "DeadSemivoicedSound": [0xFE5F], "Delete": [0xFFFF], "Down": [0xFF54], "End": [0xFF57], "Enter": [0xFF0D], "EraseEof": [0xFD06], "Escape": [0xFF1B], "Execute": [0xFF62], "Exsel": [0xFD1D], "ExSel": [0xFD1D], "F1": [0xFFBE], "F2": [0xFFBF], "F3": [0xFFC0], "F4": [0xFFC1], "F5": [0xFFC2], "F6": [0xFFC3], "F7": [0xFFC4], "F8": [0xFFC5], "F9": [0xFFC6], "F10": [0xFFC7], "F11": [0xFFC8], "F12": [0xFFC9], "F13": [0xFFCA], "F14": [0xFFCB], "F15": [0xFFCC], "F16": [0xFFCD], "F17": [0xFFCE], "F18": [0xFFCF], "F19": [0xFFD0], "F20": [0xFFD1], "F21": [0xFFD2], "F22": [0xFFD3], "F23": [0xFFD4], "F24": [0xFFD5], "Find": [0xFF68], "GroupFirst": [0xFE0C], "GroupLast": [0xFE0E], "GroupNext": [0xFE08], "GroupPrevious": [0xFE0A], "FullWidth": null, "HalfWidth": null, "HangulMode": [0xFF31], "Hankaku": [0xFF29], "HanjaMode": [0xFF34], "Help": [0xFF6A], "Hiragana": [0xFF25], "HiraganaKatakana": [0xFF27], "Home": [0xFF50], "Hyper": [0xFFED, 0xFFED, 0xFFEE], "Insert": [0xFF63], "JapaneseHiragana": [0xFF25], "JapaneseKatakana": [0xFF26], "JapaneseRomaji": [0xFF24], "JunjaMode": [0xFF38], "KanaMode": [0xFF2D], "KanjiMode": [0xFF21], "Katakana": [0xFF26], "Left": [0xFF51], "Meta": [0xFFE7, 0xFFE7, 0xFFE8], "ModeChange": [0xFF7E], "NumLock": [0xFF7F], "PageDown": [0xFF56], "PageUp": [0xFF55], "Pause": [0xFF13], "Play": [0xFD16], "PreviousCandidate": [0xFF3E], "PrintScreen": [0xFD1D], "Redo": [0xFF66], "Right": [0xFF53], "RomanCharacters": null, "Scroll": [0xFF14], "Select": [0xFF60], "Separator": [0xFFAC], "Shift": [0xFFE1, 0xFFE1, 0xFFE2], "SingleCandidate": [0xFF3C], "Super": [0xFFEB, 0xFFEB, 0xFFEC], "Tab": [0xFF09], "Up": [0xFF52], "Undo": [0xFF65], "Win": [0xFFEB], "Zenkaku": [0xFF28], "ZenkakuHankaku": [0xFF2A] }; /** * All keysyms which should not repeat when held down. * @private */ var no_repeat = { 0xFE03: true, // ISO Level 3 Shift (AltGr) 0xFFE1: true, // Left shift 0xFFE2: true, // Right shift 0xFFE3: true, // Left ctrl 0xFFE4: true, // Right ctrl 0xFFE7: true, // Left meta 0xFFE8: true, // Right meta 0xFFE9: true, // Left alt 0xFFEA: true, // Right alt 0xFFEB: true, // Left hyper 0xFFEC: true // Right hyper }; /** * All modifiers and their states. */ this.modifiers = new Guacamole.Keyboard.ModifierState(); /** * The state of every key, indexed by keysym. If a particular key is * pressed, the value of pressed for that keysym will be true. If a key * is not currently pressed, it will not be defined. */ this.pressed = {}; /** * The last result of calling the onkeydown handler for each key, indexed * by keysym. This is used to prevent/allow default actions for key events, * even when the onkeydown handler cannot be called again because the key * is (theoretically) still pressed. * * @private */ var last_keydown_result = {}; /** * The keysym most recently associated with a given keycode when keydown * fired. This object maps keycodes to keysyms. * * @private * @type {Object.} */ var recentKeysym = {}; /** * Timeout before key repeat starts. * @private */ var key_repeat_timeout = null; /** * Interval which presses and releases the last key pressed while that * key is still being held down. * @private */ var key_repeat_interval = null; /** * Given an array of keysyms indexed by location, returns the keysym * for the given location, or the keysym for the standard location if * undefined. * * @private * @param {Number[]} keysyms * An array of keysyms, where the index of the keysym in the array is * the location value. * * @param {Number} location * The location on the keyboard corresponding to the key pressed, as * defined at: http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent */ var get_keysym = function get_keysym(keysyms, location) { if (!keysyms) return null; return keysyms[location] || keysyms[0]; }; function keysym_from_key_identifier(identifier, location, shifted) { if (!identifier) return null; var typedCharacter; // If identifier is U+xxxx, decode Unicode character var unicodePrefixLocation = identifier.indexOf("U+"); if (unicodePrefixLocation >= 0) { var hex = identifier.substring(unicodePrefixLocation+2); typedCharacter = String.fromCharCode(parseInt(hex, 16)); } // If single character and not keypad, use that as typed character else if (identifier.length === 1 && location !== 3) typedCharacter = identifier; // Otherwise, look up corresponding keysym else return get_keysym(keyidentifier_keysym[identifier], location); // Alter case if necessary if (shifted === true) typedCharacter = typedCharacter.toUpperCase(); else if (shifted === false) typedCharacter = typedCharacter.toLowerCase(); // Get codepoint var codepoint = typedCharacter.charCodeAt(0); return keysym_from_charcode(codepoint); } function isControlCharacter(codepoint) { return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F); } function keysym_from_charcode(codepoint) { // Keysyms for control characters if (isControlCharacter(codepoint)) return 0xFF00 | codepoint; // Keysyms for ASCII chars if (codepoint >= 0x0000 && codepoint <= 0x00FF) return codepoint; // Keysyms for Unicode if (codepoint >= 0x0100 && codepoint <= 0x10FFFF) return 0x01000000 | codepoint; return null; } function keysym_from_keycode(keyCode, location) { return get_keysym(keycodeKeysyms[keyCode], location); } /** * Heuristically detects if the legacy keyIdentifier property of * a keydown/keyup event looks incorrectly derived. Chrome, and * presumably others, will produce the keyIdentifier by assuming * the keyCode is the Unicode codepoint for that key. This is not * correct in all cases. * * @private * @param {Number} keyCode * The keyCode from a browser keydown/keyup event. * * @param {String} keyIdentifier * The legacy keyIdentifier from a browser keydown/keyup event. * * @returns {Boolean} * true if the keyIdentifier looks sane, false if the keyIdentifier * appears incorrectly derived or is missing entirely. */ var key_identifier_sane = function key_identifier_sane(keyCode, keyIdentifier) { // Missing identifier is not sane if (!keyIdentifier) return false; // Assume non-Unicode keyIdentifier values are sane var unicodePrefixLocation = keyIdentifier.indexOf("U+"); if (unicodePrefixLocation === -1) return true; // If the Unicode codepoint isn't identical to the keyCode, // then the identifier is likely correct var codepoint = parseInt(keyIdentifier.substring(unicodePrefixLocation+2), 16); if (keyCode !== codepoint) return true; // The keyCodes for A-Z and 0-9 are actually identical to their // Unicode codepoints if ((keyCode >= 65 && keyCode <= 90) || (keyCode >= 48 && keyCode <= 57)) return true; // The keyIdentifier does NOT appear sane return false; }; /** * Marks a key as pressed, firing the keydown event if registered. Key * repeat for the pressed key will start after a delay if that key is * not a modifier. The return value of this function depends on the * return value of the keydown event handler, if any. * * @param {Number} keysym The keysym of the key to press. * @return {Boolean} true if event should NOT be canceled, false otherwise. */ this.press = function(keysym) { // Don't bother with pressing the key if the key is unknown if (keysym === null) return; // Only press if released if (!guac_keyboard.pressed[keysym]) { // Mark key as pressed guac_keyboard.pressed[keysym] = true; // Send key event if (guac_keyboard.onkeydown) { var result = guac_keyboard.onkeydown(keysym); last_keydown_result[keysym] = result; // Stop any current repeat window.clearTimeout(key_repeat_timeout); window.clearInterval(key_repeat_interval); // Repeat after a delay as long as pressed if (!no_repeat[keysym]) key_repeat_timeout = window.setTimeout(function() { key_repeat_interval = window.setInterval(function() { guac_keyboard.onkeyup(keysym); guac_keyboard.onkeydown(keysym); }, 50); }, 500); return result; } } // Return the last keydown result by default, resort to false if unknown return last_keydown_result[keysym] || false; }; /** * Marks a key as released, firing the keyup event if registered. * * @param {Number} keysym The keysym of the key to release. */ this.release = function(keysym) { // Only release if pressed if (guac_keyboard.pressed[keysym]) { // Mark key as released delete guac_keyboard.pressed[keysym]; // Stop repeat window.clearTimeout(key_repeat_timeout); window.clearInterval(key_repeat_interval); // Send key event if (keysym !== null && guac_keyboard.onkeyup) guac_keyboard.onkeyup(keysym); } }; /** * Resets the state of this keyboard, releasing all keys, and firing keyup * events for each released key. */ this.reset = function() { // Release all pressed keys for (var keysym in guac_keyboard.pressed) guac_keyboard.release(parseInt(keysym)); // Clear event log eventLog = []; }; /** * Given a keyboard event, updates the local modifier state and remote * key state based on the modifier flags within the event. This function * pays no attention to keycodes. * * @private * @param {KeyboardEvent} e * The keyboard event containing the flags to update. */ var update_modifier_state = function update_modifier_state(e) { // Get state var state = Guacamole.Keyboard.ModifierState.fromKeyboardEvent(e); // Release alt if implicitly released if (guac_keyboard.modifiers.alt && state.alt === false) { guac_keyboard.release(0xFFE9); // Left alt guac_keyboard.release(0xFFEA); // Right alt guac_keyboard.release(0xFE03); // AltGr } // Release shift if implicitly released if (guac_keyboard.modifiers.shift && state.shift === false) { guac_keyboard.release(0xFFE1); // Left shift guac_keyboard.release(0xFFE2); // Right shift } // Release ctrl if implicitly released if (guac_keyboard.modifiers.ctrl && state.ctrl === false) { guac_keyboard.release(0xFFE3); // Left ctrl guac_keyboard.release(0xFFE4); // Right ctrl } // Release meta if implicitly released if (guac_keyboard.modifiers.meta && state.meta === false) { guac_keyboard.release(0xFFE7); // Left meta guac_keyboard.release(0xFFE8); // Right meta } // Release hyper if implicitly released if (guac_keyboard.modifiers.hyper && state.hyper === false) { guac_keyboard.release(0xFFEB); // Left hyper guac_keyboard.release(0xFFEC); // Right hyper } // Update state guac_keyboard.modifiers = state; }; /** * Reads through the event log, removing events from the head of the log * when the corresponding true key presses are known (or as known as they * can be). * * @private * @return {Boolean} Whether the default action of the latest event should * be prevented. */ function interpret_events() { // Do not prevent default if no event could be interpreted var handled_event = interpret_event(); if (!handled_event) return false; // Interpret as much as possible var last_event; do { last_event = handled_event; handled_event = interpret_event(); } while (handled_event !== null); return last_event.defaultPrevented; } /** * Releases Ctrl+Alt, if both are currently pressed and the given keysym * looks like a key that may require AltGr. * * @private * @param {Number} keysym The key that was just pressed. */ var release_simulated_altgr = function release_simulated_altgr(keysym) { // Both Ctrl+Alt must be pressed if simulated AltGr is in use if (!guac_keyboard.modifiers.ctrl || !guac_keyboard.modifiers.alt) return; // Assume [A-Z] never require AltGr if (keysym >= 0x0041 && keysym <= 0x005A) return; // Assume [a-z] never require AltGr if (keysym >= 0x0061 && keysym <= 0x007A) return; // Release Ctrl+Alt if the keysym is printable if (keysym <= 0xFF || (keysym & 0xFF000000) === 0x01000000) { guac_keyboard.release(0xFFE3); // Left ctrl guac_keyboard.release(0xFFE4); // Right ctrl guac_keyboard.release(0xFFE9); // Left alt guac_keyboard.release(0xFFEA); // Right alt } }; /** * Reads through the event log, interpreting the first event, if possible, * and returning that event. If no events can be interpreted, due to a * total lack of events or the need for more events, null is returned. Any * interpreted events are automatically removed from the log. * * @private * @return {KeyEvent} * The first key event in the log, if it can be interpreted, or null * otherwise. */ var interpret_event = function interpret_event() { // Peek at first event in log var first = eventLog[0]; if (!first) return null; // Keydown event if (first instanceof KeydownEvent) { var keysym = null; var accepted_events = []; // If event itself is reliable, no need to wait for other events if (first.reliable) { keysym = first.keysym; accepted_events = eventLog.splice(0, 1); } // If keydown is immediately followed by a keypress, use the indicated character else if (eventLog[1] instanceof KeypressEvent) { keysym = eventLog[1].keysym; accepted_events = eventLog.splice(0, 2); } // If keydown is immediately followed by anything else, then no // keypress can possibly occur to clarify this event, and we must // handle it now else if (eventLog[1]) { keysym = first.keysym; accepted_events = eventLog.splice(0, 1); } // Fire a key press if valid events were found if (accepted_events.length > 0) { if (keysym) { // Fire event release_simulated_altgr(keysym); var defaultPrevented = !guac_keyboard.press(keysym); recentKeysym[first.keyCode] = keysym; // If a key is pressed while meta is held down, the keyup will // never be sent in Chrome, so send it now. (bug #108404) if (guac_keyboard.modifiers.meta && keysym !== 0xFFE7 && keysym !== 0xFFE8) guac_keyboard.release(keysym); // Record whether default was prevented for (var i=0; i} */ var layers = {}; /** * All audio players currently in use by the client. Initially, this will * be empty, but audio players may be allocated by the server upon request. * * @private * @type {Object.} */ var audioPlayers = {}; /** * All video players currently in use by the client. Initially, this will * be empty, but video players may be allocated by the server upon request. * * @private * @type {Object.} */ var videoPlayers = {}; // No initial parsers var parsers = []; // No initial streams var streams = []; /** * All current objects. The index of each object is dictated by the * Guacamole server. * * @private * @type {Guacamole.Object[]} */ var objects = []; // Pool of available stream indices var stream_indices = new Guacamole.IntegerPool(); // Array of allocated output streams by index var output_streams = []; function setState(state) { if (state != currentState) { currentState = state; if (guac_client.onstatechange) guac_client.onstatechange(currentState); } } function isConnected() { return currentState == STATE_CONNECTED || currentState == STATE_WAITING; } /** * Returns the underlying display of this Guacamole.Client. The display * contains an Element which can be added to the DOM, causing the * display to become visible. * * @return {Guacamole.Display} The underlying display of this * Guacamole.Client. */ this.getDisplay = function() { return display; }; /** * Sends the current size of the screen. * * @param {Number} width The width of the screen. * @param {Number} height The height of the screen. */ this.sendSize = function(width, height) { // Do not send requests if not connected if (!isConnected()) return; tunnel.sendMessage("size", width, height); }; /** * Sends a key event having the given properties as if the user * pressed or released a key. * * @param {Boolean} pressed Whether the key is pressed (true) or released * (false). * @param {Number} keysym The keysym of the key being pressed or released. */ this.sendKeyEvent = function(pressed, keysym) { // Do not send requests if not connected if (!isConnected()) return; tunnel.sendMessage("key", keysym, pressed); }; /** * Sends a mouse event having the properties provided by the given mouse * state. * * @param {Guacamole.Mouse.State} mouseState The state of the mouse to send * in the mouse event. */ this.sendMouseState = function(mouseState) { // Do not send requests if not connected if (!isConnected()) return; // Update client-side cursor display.moveCursor( Math.floor(mouseState.x), Math.floor(mouseState.y) ); // Build mask var buttonMask = 0; if (mouseState.left) buttonMask |= 1; if (mouseState.middle) buttonMask |= 2; if (mouseState.right) buttonMask |= 4; if (mouseState.up) buttonMask |= 8; if (mouseState.down) buttonMask |= 16; // Send message tunnel.sendMessage("mouse", Math.floor(mouseState.x), Math.floor(mouseState.y), buttonMask); }; /** * Sets the clipboard of the remote client to the given text data. * * @deprecated Use createClipboardStream() instead. * @param {String} data The data to send as the clipboard contents. */ this.setClipboard = function(data) { // Do not send requests if not connected if (!isConnected()) return; // Open stream var stream = guac_client.createClipboardStream("text/plain"); var writer = new Guacamole.StringWriter(stream); // Send text chunks for (var i=0; i 0) layer = display.createLayer(); else layer = display.createBuffer(); // Add new layer layers[index] = layer; } return layer; }; function getParser(index) { var parser = parsers[index]; // If parser not yet created, create it, and tie to the // oninstruction handler of the tunnel. if (parser == null) { parser = parsers[index] = new Guacamole.Parser(); parser.oninstruction = tunnel.oninstruction; } return parser; } /** * Handlers for all defined layer properties. * @private */ var layerPropertyHandlers = { "miter-limit": function(layer, value) { display.setMiterLimit(layer, parseFloat(value)); } }; /** * Handlers for all instruction opcodes receivable by a Guacamole protocol * client. * @private */ var instructionHandlers = { "ack": function(parameters) { var stream_index = parseInt(parameters[0]); var reason = parameters[1]; var code = parseInt(parameters[2]); // Get stream var stream = output_streams[stream_index]; if (stream) { // Signal ack if handler defined if (stream.onack) stream.onack(new Guacamole.Status(code, reason)); // If code is an error, invalidate stream if (code >= 0x0100) { stream_indices.free(stream_index); delete output_streams[stream_index]; } } }, "arc": function(parameters) { var layer = getLayer(parseInt(parameters[0])); var x = parseInt(parameters[1]); var y = parseInt(parameters[2]); var radius = parseInt(parameters[3]); var startAngle = parseFloat(parameters[4]); var endAngle = parseFloat(parameters[5]); var negative = parseInt(parameters[6]); display.arc(layer, x, y, radius, startAngle, endAngle, negative != 0); }, "audio": function(parameters) { var stream_index = parseInt(parameters[0]); var mimetype = parameters[1]; // Create stream var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); // Get player instance via callback var audioPlayer = null; if (guac_client.onaudio) audioPlayer = guac_client.onaudio(stream, mimetype); // If unsuccessful, try to use a default implementation if (!audioPlayer) audioPlayer = Guacamole.AudioPlayer.getInstance(stream, mimetype); // If we have successfully retrieved an audio player, send success response if (audioPlayer) { audioPlayers[stream_index] = audioPlayer; guac_client.sendAck(stream_index, "OK", 0x0000); } // Otherwise, mimetype must be unsupported else guac_client.sendAck(stream_index, "BAD TYPE", 0x030F); }, "blob": function(parameters) { // Get stream var stream_index = parseInt(parameters[0]); var data = parameters[1]; var stream = streams[stream_index]; // Write data if (stream && stream.onblob) stream.onblob(data); }, "body" : function handleBody(parameters) { // Get object var objectIndex = parseInt(parameters[0]); var object = objects[objectIndex]; var streamIndex = parseInt(parameters[1]); var mimetype = parameters[2]; var name = parameters[3]; // Create stream if handler defined if (object && object.onbody) { var stream = streams[streamIndex] = new Guacamole.InputStream(guac_client, streamIndex); object.onbody(stream, mimetype, name); } // Otherwise, unsupported else guac_client.sendAck(streamIndex, "Receipt of body unsupported", 0x0100); }, "cfill": function(parameters) { var channelMask = parseInt(parameters[0]); var layer = getLayer(parseInt(parameters[1])); var r = parseInt(parameters[2]); var g = parseInt(parameters[3]); var b = parseInt(parameters[4]); var a = parseInt(parameters[5]); display.setChannelMask(layer, channelMask); display.fillColor(layer, r, g, b, a); }, "clip": function(parameters) { var layer = getLayer(parseInt(parameters[0])); display.clip(layer); }, "clipboard": function(parameters) { var stream_index = parseInt(parameters[0]); var mimetype = parameters[1]; // Create stream if (guac_client.onclipboard) { var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); guac_client.onclipboard(stream, mimetype); } // Otherwise, unsupported else guac_client.sendAck(stream_index, "Clipboard unsupported", 0x0100); }, "close": function(parameters) { var layer = getLayer(parseInt(parameters[0])); display.close(layer); }, "copy": function(parameters) { var srcL = getLayer(parseInt(parameters[0])); var srcX = parseInt(parameters[1]); var srcY = parseInt(parameters[2]); var srcWidth = parseInt(parameters[3]); var srcHeight = parseInt(parameters[4]); var channelMask = parseInt(parameters[5]); var dstL = getLayer(parseInt(parameters[6])); var dstX = parseInt(parameters[7]); var dstY = parseInt(parameters[8]); display.setChannelMask(dstL, channelMask); display.copy(srcL, srcX, srcY, srcWidth, srcHeight, dstL, dstX, dstY); }, "cstroke": function(parameters) { var channelMask = parseInt(parameters[0]); var layer = getLayer(parseInt(parameters[1])); var cap = lineCap[parseInt(parameters[2])]; var join = lineJoin[parseInt(parameters[3])]; var thickness = parseInt(parameters[4]); var r = parseInt(parameters[5]); var g = parseInt(parameters[6]); var b = parseInt(parameters[7]); var a = parseInt(parameters[8]); display.setChannelMask(layer, channelMask); display.strokeColor(layer, cap, join, thickness, r, g, b, a); }, "cursor": function(parameters) { var cursorHotspotX = parseInt(parameters[0]); var cursorHotspotY = parseInt(parameters[1]); var srcL = getLayer(parseInt(parameters[2])); var srcX = parseInt(parameters[3]); var srcY = parseInt(parameters[4]); var srcWidth = parseInt(parameters[5]); var srcHeight = parseInt(parameters[6]); display.setCursor(cursorHotspotX, cursorHotspotY, srcL, srcX, srcY, srcWidth, srcHeight); }, "curve": function(parameters) { var layer = getLayer(parseInt(parameters[0])); var cp1x = parseInt(parameters[1]); var cp1y = parseInt(parameters[2]); var cp2x = parseInt(parameters[3]); var cp2y = parseInt(parameters[4]); var x = parseInt(parameters[5]); var y = parseInt(parameters[6]); display.curveTo(layer, cp1x, cp1y, cp2x, cp2y, x, y); }, "dispose": function(parameters) { var layer_index = parseInt(parameters[0]); // If visible layer, remove from parent if (layer_index > 0) { // Remove from parent var layer = getLayer(layer_index); layer.dispose(); // Delete reference delete layers[layer_index]; } // If buffer, just delete reference else if (layer_index < 0) delete layers[layer_index]; // Attempting to dispose the root layer currently has no effect. }, "distort": function(parameters) { var layer_index = parseInt(parameters[0]); var a = parseFloat(parameters[1]); var b = parseFloat(parameters[2]); var c = parseFloat(parameters[3]); var d = parseFloat(parameters[4]); var e = parseFloat(parameters[5]); var f = parseFloat(parameters[6]); // Only valid for visible layers (not buffers) if (layer_index >= 0) { var layer = getLayer(layer_index); layer.distort(a, b, c, d, e, f); } }, "error": function(parameters) { var reason = parameters[0]; var code = parseInt(parameters[1]); // Call handler if defined if (guac_client.onerror) guac_client.onerror(new Guacamole.Status(code, reason)); guac_client.disconnect(); }, "end": function(parameters) { // Get stream var stream_index = parseInt(parameters[0]); var stream = streams[stream_index]; // Signal end of stream if (stream && stream.onend) stream.onend(); }, "file": function(parameters) { var stream_index = parseInt(parameters[0]); var mimetype = parameters[1]; var filename = parameters[2]; // Create stream if (guac_client.onfile) { var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); guac_client.onfile(stream, mimetype, filename); } // Otherwise, unsupported else guac_client.sendAck(stream_index, "File transfer unsupported", 0x0100); }, "filesystem" : function handleFilesystem(parameters) { var objectIndex = parseInt(parameters[0]); var name = parameters[1]; // Create object, if supported if (guac_client.onfilesystem) { var object = objects[objectIndex] = new Guacamole.Object(guac_client, objectIndex); guac_client.onfilesystem(object, name); } // If unsupported, simply ignore the availability of the filesystem }, "identity": function(parameters) { var layer = getLayer(parseInt(parameters[0])); display.setTransform(layer, 1, 0, 0, 1, 0, 0); }, "img": function(parameters) { var stream_index = parseInt(parameters[0]); var channelMask = parseInt(parameters[1]); var layer = getLayer(parseInt(parameters[2])); var mimetype = parameters[3]; var x = parseInt(parameters[4]); var y = parseInt(parameters[5]); // Create stream var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); var reader = new Guacamole.DataURIReader(stream, mimetype); // Draw image when stream is complete reader.onend = function drawImageBlob() { display.setChannelMask(layer, channelMask); display.draw(layer, x, y, reader.getURI()); }; }, "jpeg": function(parameters) { var channelMask = parseInt(parameters[0]); var layer = getLayer(parseInt(parameters[1])); var x = parseInt(parameters[2]); var y = parseInt(parameters[3]); var data = parameters[4]; display.setChannelMask(layer, channelMask); display.draw(layer, x, y, "data:image/jpeg;base64," + data); }, "lfill": function(parameters) { var channelMask = parseInt(parameters[0]); var layer = getLayer(parseInt(parameters[1])); var srcLayer = getLayer(parseInt(parameters[2])); display.setChannelMask(layer, channelMask); display.fillLayer(layer, srcLayer); }, "line": function(parameters) { var layer = getLayer(parseInt(parameters[0])); var x = parseInt(parameters[1]); var y = parseInt(parameters[2]); display.lineTo(layer, x, y); }, "lstroke": function(parameters) { var channelMask = parseInt(parameters[0]); var layer = getLayer(parseInt(parameters[1])); var srcLayer = getLayer(parseInt(parameters[2])); display.setChannelMask(layer, channelMask); display.strokeLayer(layer, srcLayer); }, "move": function(parameters) { var layer_index = parseInt(parameters[0]); var parent_index = parseInt(parameters[1]); var x = parseInt(parameters[2]); var y = parseInt(parameters[3]); var z = parseInt(parameters[4]); // Only valid for non-default layers if (layer_index > 0 && parent_index >= 0) { var layer = getLayer(layer_index); var parent = getLayer(parent_index); layer.move(parent, x, y, z); } }, "name": function(parameters) { if (guac_client.onname) guac_client.onname(parameters[0]); }, "nest": function(parameters) { var parser = getParser(parseInt(parameters[0])); parser.receive(parameters[1]); }, "pipe": function(parameters) { var stream_index = parseInt(parameters[0]); var mimetype = parameters[1]; var name = parameters[2]; // Create stream if (guac_client.onpipe) { var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); guac_client.onpipe(stream, mimetype, name); } // Otherwise, unsupported else guac_client.sendAck(stream_index, "Named pipes unsupported", 0x0100); }, "png": function(parameters) { var channelMask = parseInt(parameters[0]); var layer = getLayer(parseInt(parameters[1])); var x = parseInt(parameters[2]); var y = parseInt(parameters[3]); var data = parameters[4]; display.setChannelMask(layer, channelMask); display.draw(layer, x, y, "data:image/png;base64," + data); }, "pop": function(parameters) { var layer = getLayer(parseInt(parameters[0])); display.pop(layer); }, "push": function(parameters) { var layer = getLayer(parseInt(parameters[0])); display.push(layer); }, "rect": function(parameters) { var layer = getLayer(parseInt(parameters[0])); var x = parseInt(parameters[1]); var y = parseInt(parameters[2]); var w = parseInt(parameters[3]); var h = parseInt(parameters[4]); display.rect(layer, x, y, w, h); }, "reset": function(parameters) { var layer = getLayer(parseInt(parameters[0])); display.reset(layer); }, "set": function(parameters) { var layer = getLayer(parseInt(parameters[0])); var name = parameters[1]; var value = parameters[2]; // Call property handler if defined var handler = layerPropertyHandlers[name]; if (handler) handler(layer, value); }, "shade": function(parameters) { var layer_index = parseInt(parameters[0]); var a = parseInt(parameters[1]); // Only valid for visible layers (not buffers) if (layer_index >= 0) { var layer = getLayer(layer_index); layer.shade(a); } }, "size": function(parameters) { var layer_index = parseInt(parameters[0]); var layer = getLayer(layer_index); var width = parseInt(parameters[1]); var height = parseInt(parameters[2]); display.resize(layer, width, height); }, "start": function(parameters) { var layer = getLayer(parseInt(parameters[0])); var x = parseInt(parameters[1]); var y = parseInt(parameters[2]); display.moveTo(layer, x, y); }, "sync": function(parameters) { var timestamp = parseInt(parameters[0]); // Flush display, send sync when done display.flush(function displaySyncComplete() { // Synchronize all audio players for (var index in audioPlayers) { var audioPlayer = audioPlayers[index]; if (audioPlayer) audioPlayer.sync(); } // Send sync response to server if (timestamp !== currentTimestamp) { tunnel.sendMessage("sync", timestamp); currentTimestamp = timestamp; } }); // If received first update, no longer waiting. if (currentState === STATE_WAITING) setState(STATE_CONNECTED); // Call sync handler if defined if (guac_client.onsync) guac_client.onsync(timestamp); }, "transfer": function(parameters) { var srcL = getLayer(parseInt(parameters[0])); var srcX = parseInt(parameters[1]); var srcY = parseInt(parameters[2]); var srcWidth = parseInt(parameters[3]); var srcHeight = parseInt(parameters[4]); var function_index = parseInt(parameters[5]); var dstL = getLayer(parseInt(parameters[6])); var dstX = parseInt(parameters[7]); var dstY = parseInt(parameters[8]); /* SRC */ if (function_index === 0x3) display.put(srcL, srcX, srcY, srcWidth, srcHeight, dstL, dstX, dstY); /* Anything else that isn't a NO-OP */ else if (function_index !== 0x5) display.transfer(srcL, srcX, srcY, srcWidth, srcHeight, dstL, dstX, dstY, Guacamole.Client.DefaultTransferFunction[function_index]); }, "transform": function(parameters) { var layer = getLayer(parseInt(parameters[0])); var a = parseFloat(parameters[1]); var b = parseFloat(parameters[2]); var c = parseFloat(parameters[3]); var d = parseFloat(parameters[4]); var e = parseFloat(parameters[5]); var f = parseFloat(parameters[6]); display.transform(layer, a, b, c, d, e, f); }, "undefine" : function handleUndefine(parameters) { // Get object var objectIndex = parseInt(parameters[0]); var object = objects[objectIndex]; // Signal end of object definition if (object && object.onundefine) object.onundefine(); }, "video": function(parameters) { var stream_index = parseInt(parameters[0]); var layer = getLayer(parseInt(parameters[1])); var mimetype = parameters[2]; // Create stream var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); // Get player instance via callback var videoPlayer = null; if (guac_client.onvideo) videoPlayer = guac_client.onvideo(stream, layer, mimetype); // If unsuccessful, try to use a default implementation if (!videoPlayer) videoPlayer = Guacamole.VideoPlayer.getInstance(stream, layer, mimetype); // If we have successfully retrieved an video player, send success response if (videoPlayer) { videoPlayers[stream_index] = videoPlayer; guac_client.sendAck(stream_index, "OK", 0x0000); } // Otherwise, mimetype must be unsupported else guac_client.sendAck(stream_index, "BAD TYPE", 0x030F); } }; tunnel.oninstruction = function(opcode, parameters) { var handler = instructionHandlers[opcode]; if (handler) handler(parameters); }; /** * Sends a disconnect instruction to the server and closes the tunnel. */ this.disconnect = function() { // Only attempt disconnection not disconnected. if (currentState != STATE_DISCONNECTED && currentState != STATE_DISCONNECTING) { setState(STATE_DISCONNECTING); // Stop ping if (pingInterval) window.clearInterval(pingInterval); // Send disconnect message and disconnect tunnel.sendMessage("disconnect"); tunnel.disconnect(); setState(STATE_DISCONNECTED); } }; /** * Connects the underlying tunnel of this Guacamole.Client, passing the * given arbitrary data to the tunnel during the connection process. * * @param data Arbitrary connection data to be sent to the underlying * tunnel during the connection process. * @throws {Guacamole.Status} If an error occurs during connection. */ this.connect = function(data) { setState(STATE_CONNECTING); try { tunnel.connect(data); } catch (status) { setState(STATE_IDLE); throw status; } // Ping every 5 seconds (ensure connection alive) pingInterval = window.setInterval(function() { tunnel.sendMessage("sync", currentTimestamp); }, 5000); setState(STATE_WAITING); }; }; /** * Map of all Guacamole binary raster operations to transfer functions. * @private */ Guacamole.Client.DefaultTransferFunction = { /* BLACK */ 0x0: function (src, dst) { dst.red = dst.green = dst.blue = 0x00; }, /* WHITE */ 0xF: function (src, dst) { dst.red = dst.green = dst.blue = 0xFF; }, /* SRC */ 0x3: function (src, dst) { dst.red = src.red; dst.green = src.green; dst.blue = src.blue; dst.alpha = src.alpha; }, /* DEST (no-op) */ 0x5: function (src, dst) { // Do nothing }, /* Invert SRC */ 0xC: function (src, dst) { dst.red = 0xFF & ~src.red; dst.green = 0xFF & ~src.green; dst.blue = 0xFF & ~src.blue; dst.alpha = src.alpha; }, /* Invert DEST */ 0xA: function (src, dst) { dst.red = 0xFF & ~dst.red; dst.green = 0xFF & ~dst.green; dst.blue = 0xFF & ~dst.blue; }, /* AND */ 0x1: function (src, dst) { dst.red = ( src.red & dst.red); dst.green = ( src.green & dst.green); dst.blue = ( src.blue & dst.blue); }, /* NAND */ 0xE: function (src, dst) { dst.red = 0xFF & ~( src.red & dst.red); dst.green = 0xFF & ~( src.green & dst.green); dst.blue = 0xFF & ~( src.blue & dst.blue); }, /* OR */ 0x7: function (src, dst) { dst.red = ( src.red | dst.red); dst.green = ( src.green | dst.green); dst.blue = ( src.blue | dst.blue); }, /* NOR */ 0x8: function (src, dst) { dst.red = 0xFF & ~( src.red | dst.red); dst.green = 0xFF & ~( src.green | dst.green); dst.blue = 0xFF & ~( src.blue | dst.blue); }, /* XOR */ 0x6: function (src, dst) { dst.red = ( src.red ^ dst.red); dst.green = ( src.green ^ dst.green); dst.blue = ( src.blue ^ dst.blue); }, /* XNOR */ 0x9: function (src, dst) { dst.red = 0xFF & ~( src.red ^ dst.red); dst.green = 0xFF & ~( src.green ^ dst.green); dst.blue = 0xFF & ~( src.blue ^ dst.blue); }, /* AND inverted source */ 0x4: function (src, dst) { dst.red = 0xFF & (~src.red & dst.red); dst.green = 0xFF & (~src.green & dst.green); dst.blue = 0xFF & (~src.blue & dst.blue); }, /* OR inverted source */ 0xD: function (src, dst) { dst.red = 0xFF & (~src.red | dst.red); dst.green = 0xFF & (~src.green | dst.green); dst.blue = 0xFF & (~src.blue | dst.blue); }, /* AND inverted destination */ 0x2: function (src, dst) { dst.red = 0xFF & ( src.red & ~dst.red); dst.green = 0xFF & ( src.green & ~dst.green); dst.blue = 0xFF & ( src.blue & ~dst.blue); }, /* OR inverted destination */ 0xB: function (src, dst) { dst.red = 0xFF & ( src.red | ~dst.red); dst.green = 0xFF & ( src.green | ~dst.green); dst.blue = 0xFF & ( src.blue | ~dst.blue); } }; guacamole-client-0.9.9/guacamole-common-js/src/main/webapp/modules/Display.js0100664 0001751 0001751 00000135374 12645510255 031012 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ var Guacamole = Guacamole || {}; /** * The Guacamole display. The display does not deal with the Guacamole * protocol, and instead implements a set of graphical operations which * embody the set of operations present in the protocol. The order operations * are executed is guaranteed to be in the same order as their corresponding * functions are called. * * @constructor */ Guacamole.Display = function() { /** * Reference to this Guacamole.Display. * @private */ var guac_display = this; var displayWidth = 0; var displayHeight = 0; var displayScale = 1; // Create display var display = document.createElement("div"); display.style.position = "relative"; display.style.width = displayWidth + "px"; display.style.height = displayHeight + "px"; // Ensure transformations on display originate at 0,0 display.style.transformOrigin = display.style.webkitTransformOrigin = display.style.MozTransformOrigin = display.style.OTransformOrigin = display.style.msTransformOrigin = "0 0"; // Create default layer var default_layer = new Guacamole.Display.VisibleLayer(displayWidth, displayHeight); // Create cursor layer var cursor = new Guacamole.Display.VisibleLayer(0, 0); cursor.setChannelMask(Guacamole.Layer.SRC); // Add default layer and cursor to display display.appendChild(default_layer.getElement()); display.appendChild(cursor.getElement()); // Create bounding div var bounds = document.createElement("div"); bounds.style.position = "relative"; bounds.style.width = (displayWidth*displayScale) + "px"; bounds.style.height = (displayHeight*displayScale) + "px"; // Add display to bounds bounds.appendChild(display); /** * The X coordinate of the hotspot of the mouse cursor. The hotspot is * the relative location within the image of the mouse cursor at which * each click occurs. * * @type {Number} */ this.cursorHotspotX = 0; /** * The Y coordinate of the hotspot of the mouse cursor. The hotspot is * the relative location within the image of the mouse cursor at which * each click occurs. * * @type {Number} */ this.cursorHotspotY = 0; /** * The current X coordinate of the local mouse cursor. This is not * necessarily the location of the actual mouse - it refers only to * the location of the cursor image within the Guacamole display, as * last set by moveCursor(). * * @type {Number} */ this.cursorX = 0; /** * The current X coordinate of the local mouse cursor. This is not * necessarily the location of the actual mouse - it refers only to * the location of the cursor image within the Guacamole display, as * last set by moveCursor(). * * @type {Number} */ this.cursorY = 0; /** * Fired when the default layer (and thus the entire Guacamole display) * is resized. * * @event * @param {Number} width The new width of the Guacamole display. * @param {Number} height The new height of the Guacamole display. */ this.onresize = null; /** * Fired whenever the local cursor image is changed. This can be used to * implement special handling of the client-side cursor, or to override * the default use of a software cursor layer. * * @event * @param {HTMLCanvasElement} canvas The cursor image. * @param {Number} x The X-coordinate of the cursor hotspot. * @param {Number} y The Y-coordinate of the cursor hotspot. */ this.oncursor = null; /** * The queue of all pending Tasks. Tasks will be run in order, with new * tasks added at the end of the queue and old tasks removed from the * front of the queue (FIFO). These tasks will eventually be grouped * into a Frame. * @private * @type {Task[]} */ var tasks = []; /** * The queue of all frames. Each frame is a pairing of an array of tasks * and a callback which must be called when the frame is rendered. * @private * @type {Frame[]} */ var frames = []; /** * Flushes all pending frames. * @private */ function __flush_frames() { var rendered_frames = 0; // Draw all pending frames, if ready while (rendered_frames < frames.length) { var frame = frames[rendered_frames]; if (!frame.isReady()) break; frame.flush(); rendered_frames++; } // Remove rendered frames from array frames.splice(0, rendered_frames); } /** * An ordered list of tasks which must be executed atomically. Once * executed, an associated (and optional) callback will be called. * * @private * @constructor * @param {function} callback The function to call when this frame is * rendered. * @param {Task[]} tasks The set of tasks which must be executed to render * this frame. */ function Frame(callback, tasks) { /** * Returns whether this frame is ready to be rendered. This function * returns true if and only if ALL underlying tasks are unblocked. * * @returns {Boolean} true if all underlying tasks are unblocked, * false otherwise. */ this.isReady = function() { // Search for blocked tasks for (var i=0; i < tasks.length; i++) { if (tasks[i].blocked) return false; } // If no blocked tasks, the frame is ready return true; }; /** * Renders this frame, calling the associated callback, if any, after * the frame is complete. This function MUST only be called when no * blocked tasks exist. Calling this function with blocked tasks * will result in undefined behavior. */ this.flush = function() { // Draw all pending tasks. for (var i=0; i < tasks.length; i++) tasks[i].execute(); // Call callback if (callback) callback(); }; } /** * A container for an task handler. Each operation which must be ordered * is associated with a Task that goes into a task queue. Tasks in this * queue are executed in order once their handlers are set, while Tasks * without handlers block themselves and any following Tasks from running. * * @constructor * @private * @param {function} taskHandler The function to call when this task * runs, if any. * @param {boolean} blocked Whether this task should start blocked. */ function Task(taskHandler, blocked) { var task = this; /** * Whether this Task is blocked. * * @type {boolean} */ this.blocked = blocked; /** * Unblocks this Task, allowing it to run. */ this.unblock = function() { if (task.blocked) { task.blocked = false; __flush_frames(); } }; /** * Calls the handler associated with this task IMMEDIATELY. This * function does not track whether this task is marked as blocked. * Enforcing the blocked status of tasks is up to the caller. */ this.execute = function() { if (taskHandler) taskHandler(); }; } /** * Schedules a task for future execution. The given handler will execute * immediately after all previous tasks upon frame flush, unless this * task is blocked. If any tasks is blocked, the entire frame will not * render (and no tasks within will execute) until all tasks are unblocked. * * @private * @param {function} handler The function to call when possible, if any. * @param {boolean} blocked Whether the task should start blocked. * @returns {Task} The Task created and added to the queue for future * running. */ function scheduleTask(handler, blocked) { var task = new Task(handler, blocked); tasks.push(task); return task; } /** * Returns the element which contains the Guacamole display. * * @return {Element} The element containing the Guacamole display. */ this.getElement = function() { return bounds; }; /** * Returns the width of this display. * * @return {Number} The width of this display; */ this.getWidth = function() { return displayWidth; }; /** * Returns the height of this display. * * @return {Number} The height of this display; */ this.getHeight = function() { return displayHeight; }; /** * Returns the default layer of this display. Each Guacamole display always * has at least one layer. Other layers can optionally be created within * this layer, but the default layer cannot be removed and is the absolute * ancestor of all other layers. * * @return {Guacamole.Display.VisibleLayer} The default layer. */ this.getDefaultLayer = function() { return default_layer; }; /** * Returns the cursor layer of this display. Each Guacamole display contains * a layer for the image of the mouse cursor. This layer is a special case * and exists above all other layers, similar to the hardware mouse cursor. * * @return {Guacamole.Display.VisibleLayer} The cursor layer. */ this.getCursorLayer = function() { return cursor; }; /** * Creates a new layer. The new layer will be a direct child of the default * layer, but can be moved to be a child of any other layer. Layers returned * by this function are visible. * * @return {Guacamole.Display.VisibleLayer} The newly-created layer. */ this.createLayer = function() { var layer = new Guacamole.Display.VisibleLayer(displayWidth, displayHeight); layer.move(default_layer, 0, 0, 0); return layer; }; /** * Creates a new buffer. Buffers are invisible, off-screen surfaces. They * are implemented in the same manner as layers, but do not provide the * same nesting semantics. * * @return {Guacamole.Layer} The newly-created buffer. */ this.createBuffer = function() { var buffer = new Guacamole.Layer(0, 0); buffer.autosize = 1; return buffer; }; /** * Flush all pending draw tasks, if possible, as a new frame. If the entire * frame is not ready, the flush will wait until all required tasks are * unblocked. * * @param {function} callback The function to call when this frame is * flushed. This may happen immediately, or * later when blocked tasks become unblocked. */ this.flush = function(callback) { // Add frame, reset tasks frames.push(new Frame(callback, tasks)); tasks = []; // Attempt flush __flush_frames(); }; /** * Sets the hotspot and image of the mouse cursor displayed within the * Guacamole display. * * @param {Number} hotspotX The X coordinate of the cursor hotspot. * @param {Number} hotspotY The Y coordinate of the cursor hotspot. * @param {Guacamole.Layer} layer The source layer containing the data which * should be used as the mouse cursor image. * @param {Number} srcx The X coordinate of the upper-left corner of the * rectangle within the source layer's coordinate * space to copy data from. * @param {Number} srcy The Y coordinate of the upper-left corner of the * rectangle within the source layer's coordinate * space to copy data from. * @param {Number} srcw The width of the rectangle within the source layer's * coordinate space to copy data from. * @param {Number} srch The height of the rectangle within the source * layer's coordinate space to copy data from. */ this.setCursor = function(hotspotX, hotspotY, layer, srcx, srcy, srcw, srch) { scheduleTask(function __display_set_cursor() { // Set hotspot guac_display.cursorHotspotX = hotspotX; guac_display.cursorHotspotY = hotspotY; // Reset cursor size cursor.resize(srcw, srch); // Draw cursor to cursor layer cursor.copy(layer, srcx, srcy, srcw, srch, 0, 0); guac_display.moveCursor(guac_display.cursorX, guac_display.cursorY); // Fire cursor change event if (guac_display.oncursor) guac_display.oncursor(cursor.getCanvas(), hotspotX, hotspotY); }); }; /** * Sets whether the software-rendered cursor is shown. This cursor differs * from the hardware cursor in that it is built into the Guacamole.Display, * and relies on its own Guacamole layer to render. * * @param {Boolean} [shown=true] Whether to show the software cursor. */ this.showCursor = function(shown) { var element = cursor.getElement(); var parent = element.parentNode; // Remove from DOM if hidden if (shown === false) { if (parent) parent.removeChild(element); } // Otherwise, ensure cursor is child of display else if (parent !== display) display.appendChild(element); }; /** * Sets the location of the local cursor to the given coordinates. For the * sake of responsiveness, this function performs its action immediately. * Cursor motion is not maintained within atomic frames. * * @param {Number} x The X coordinate to move the cursor to. * @param {Number} y The Y coordinate to move the cursor to. */ this.moveCursor = function(x, y) { // Move cursor layer cursor.translate(x - guac_display.cursorHotspotX, y - guac_display.cursorHotspotY); // Update stored position guac_display.cursorX = x; guac_display.cursorY = y; }; /** * Changes the size of the given Layer to the given width and height. * Resizing is only attempted if the new size provided is actually different * from the current size. * * @param {Guacamole.Layer} layer The layer to resize. * @param {Number} width The new width. * @param {Number} height The new height. */ this.resize = function(layer, width, height) { scheduleTask(function __display_resize() { layer.resize(width, height); // Resize display if default layer is resized if (layer === default_layer) { // Update (set) display size displayWidth = width; displayHeight = height; display.style.width = displayWidth + "px"; display.style.height = displayHeight + "px"; // Update bounds size bounds.style.width = (displayWidth*displayScale) + "px"; bounds.style.height = (displayHeight*displayScale) + "px"; // Notify of resize if (guac_display.onresize) guac_display.onresize(width, height); } }); }; /** * Draws the specified image at the given coordinates. The image specified * must already be loaded. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {Number} x The destination X coordinate. * @param {Number} y The destination Y coordinate. * @param {Image} image The image to draw. Note that this is an Image * object - not a URL. */ this.drawImage = function(layer, x, y, image) { scheduleTask(function __display_drawImage() { layer.drawImage(x, y, image); }); }; /** * Draws the image contained within the specified Blob at the given * coordinates. The Blob specified must already be populated with image * data. * * @param {Guacamole.Layer} layer * The layer to draw upon. * * @param {Number} x * The destination X coordinate. * * @param {Number} y * The destination Y coordinate. * * @param {Blob} blob * The Blob containing the image data to draw. */ this.drawBlob = function(layer, x, y, blob) { // Create URL for blob var url = URL.createObjectURL(blob); // Draw and free blob URL when ready var task = scheduleTask(function __display_drawBlob() { layer.drawImage(x, y, image); URL.revokeObjectURL(url); }, true); // Load image from URL var image = new Image(); image.onload = task.unblock; image.src = url; }; /** * Draws the image at the specified URL at the given coordinates. The image * will be loaded automatically, and this and any future operations will * wait for the image to finish loading. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {Number} x The destination X coordinate. * @param {Number} y The destination Y coordinate. * @param {String} url The URL of the image to draw. */ this.draw = function(layer, x, y, url) { var task = scheduleTask(function __display_draw() { layer.drawImage(x, y, image); }, true); var image = new Image(); image.onload = task.unblock; image.src = url; }; /** * Plays the video at the specified URL within this layer. The video * will be loaded automatically, and this and any future operations will * wait for the video to finish loading. Future operations will not be * executed until the video finishes playing. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {String} mimetype The mimetype of the video to play. * @param {Number} duration The duration of the video in milliseconds. * @param {String} url The URL of the video to play. */ this.play = function(layer, mimetype, duration, url) { // Start loading the video var video = document.createElement("video"); video.type = mimetype; video.src = url; // Start copying frames when playing video.addEventListener("play", function() { function render_callback() { layer.drawImage(0, 0, video); if (!video.ended) window.setTimeout(render_callback, 20); } render_callback(); }, false); scheduleTask(video.play); }; /** * Transfer a rectangle of image data from one Layer to this Layer using the * specified transfer function. * * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. * @param {Number} srcx The X coordinate of the upper-left corner of the * rectangle within the source Layer's coordinate * space to copy data from. * @param {Number} srcy The Y coordinate of the upper-left corner of the * rectangle within the source Layer's coordinate * space to copy data from. * @param {Number} srcw The width of the rectangle within the source Layer's * coordinate space to copy data from. * @param {Number} srch The height of the rectangle within the source * Layer's coordinate space to copy data from. * @param {Guacamole.Layer} dstLayer The layer to draw upon. * @param {Number} x The destination X coordinate. * @param {Number} y The destination Y coordinate. * @param {Function} transferFunction The transfer function to use to * transfer data from source to * destination. */ this.transfer = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y, transferFunction) { scheduleTask(function __display_transfer() { dstLayer.transfer(srcLayer, srcx, srcy, srcw, srch, x, y, transferFunction); }); }; /** * Put a rectangle of image data from one Layer to this Layer directly * without performing any alpha blending. Simply copy the data. * * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. * @param {Number} srcx The X coordinate of the upper-left corner of the * rectangle within the source Layer's coordinate * space to copy data from. * @param {Number} srcy The Y coordinate of the upper-left corner of the * rectangle within the source Layer's coordinate * space to copy data from. * @param {Number} srcw The width of the rectangle within the source Layer's * coordinate space to copy data from. * @param {Number} srch The height of the rectangle within the source * Layer's coordinate space to copy data from. * @param {Guacamole.Layer} dstLayer The layer to draw upon. * @param {Number} x The destination X coordinate. * @param {Number} y The destination Y coordinate. */ this.put = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y) { scheduleTask(function __display_put() { dstLayer.put(srcLayer, srcx, srcy, srcw, srch, x, y); }); }; /** * Copy a rectangle of image data from one Layer to this Layer. This * operation will copy exactly the image data that will be drawn once all * operations of the source Layer that were pending at the time this * function was called are complete. This operation will not alter the * size of the source Layer even if its autosize property is set to true. * * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. * @param {Number} srcx The X coordinate of the upper-left corner of the * rectangle within the source Layer's coordinate * space to copy data from. * @param {Number} srcy The Y coordinate of the upper-left corner of the * rectangle within the source Layer's coordinate * space to copy data from. * @param {Number} srcw The width of the rectangle within the source Layer's * coordinate space to copy data from. * @param {Number} srch The height of the rectangle within the source * Layer's coordinate space to copy data from. * @param {Guacamole.Layer} dstLayer The layer to draw upon. * @param {Number} x The destination X coordinate. * @param {Number} y The destination Y coordinate. */ this.copy = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y) { scheduleTask(function __display_copy() { dstLayer.copy(srcLayer, srcx, srcy, srcw, srch, x, y); }); }; /** * Starts a new path at the specified point. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {Number} x The X coordinate of the point to draw. * @param {Number} y The Y coordinate of the point to draw. */ this.moveTo = function(layer, x, y) { scheduleTask(function __display_moveTo() { layer.moveTo(x, y); }); }; /** * Add the specified line to the current path. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {Number} x The X coordinate of the endpoint of the line to draw. * @param {Number} y The Y coordinate of the endpoint of the line to draw. */ this.lineTo = function(layer, x, y) { scheduleTask(function __display_lineTo() { layer.lineTo(x, y); }); }; /** * Add the specified arc to the current path. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {Number} x The X coordinate of the center of the circle which * will contain the arc. * @param {Number} y The Y coordinate of the center of the circle which * will contain the arc. * @param {Number} radius The radius of the circle. * @param {Number} startAngle The starting angle of the arc, in radians. * @param {Number} endAngle The ending angle of the arc, in radians. * @param {Boolean} negative Whether the arc should be drawn in order of * decreasing angle. */ this.arc = function(layer, x, y, radius, startAngle, endAngle, negative) { scheduleTask(function __display_arc() { layer.arc(x, y, radius, startAngle, endAngle, negative); }); }; /** * Starts a new path at the specified point. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {Number} cp1x The X coordinate of the first control point. * @param {Number} cp1y The Y coordinate of the first control point. * @param {Number} cp2x The X coordinate of the second control point. * @param {Number} cp2y The Y coordinate of the second control point. * @param {Number} x The X coordinate of the endpoint of the curve. * @param {Number} y The Y coordinate of the endpoint of the curve. */ this.curveTo = function(layer, cp1x, cp1y, cp2x, cp2y, x, y) { scheduleTask(function __display_curveTo() { layer.curveTo(cp1x, cp1y, cp2x, cp2y, x, y); }); }; /** * Closes the current path by connecting the end point with the start * point (if any) with a straight line. * * @param {Guacamole.Layer} layer The layer to draw upon. */ this.close = function(layer) { scheduleTask(function __display_close() { layer.close(); }); }; /** * Add the specified rectangle to the current path. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {Number} x The X coordinate of the upper-left corner of the * rectangle to draw. * @param {Number} y The Y coordinate of the upper-left corner of the * rectangle to draw. * @param {Number} w The width of the rectangle to draw. * @param {Number} h The height of the rectangle to draw. */ this.rect = function(layer, x, y, w, h) { scheduleTask(function __display_rect() { layer.rect(x, y, w, h); }); }; /** * Clip all future drawing operations by the current path. The current path * is implicitly closed. The current path can continue to be reused * for other operations (such as fillColor()) but a new path will be started * once a path drawing operation (path() or rect()) is used. * * @param {Guacamole.Layer} layer The layer to affect. */ this.clip = function(layer) { scheduleTask(function __display_clip() { layer.clip(); }); }; /** * Stroke the current path with the specified color. The current path * is implicitly closed. The current path can continue to be reused * for other operations (such as clip()) but a new path will be started * once a path drawing operation (path() or rect()) is used. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {String} cap The line cap style. Can be "round", "square", * or "butt". * @param {String} join The line join style. Can be "round", "bevel", * or "miter". * @param {Number} thickness The line thickness in pixels. * @param {Number} r The red component of the color to fill. * @param {Number} g The green component of the color to fill. * @param {Number} b The blue component of the color to fill. * @param {Number} a The alpha component of the color to fill. */ this.strokeColor = function(layer, cap, join, thickness, r, g, b, a) { scheduleTask(function __display_strokeColor() { layer.strokeColor(cap, join, thickness, r, g, b, a); }); }; /** * Fills the current path with the specified color. The current path * is implicitly closed. The current path can continue to be reused * for other operations (such as clip()) but a new path will be started * once a path drawing operation (path() or rect()) is used. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {Number} r The red component of the color to fill. * @param {Number} g The green component of the color to fill. * @param {Number} b The blue component of the color to fill. * @param {Number} a The alpha component of the color to fill. */ this.fillColor = function(layer, r, g, b, a) { scheduleTask(function __display_fillColor() { layer.fillColor(r, g, b, a); }); }; /** * Stroke the current path with the image within the specified layer. The * image data will be tiled infinitely within the stroke. The current path * is implicitly closed. The current path can continue to be reused * for other operations (such as clip()) but a new path will be started * once a path drawing operation (path() or rect()) is used. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {String} cap The line cap style. Can be "round", "square", * or "butt". * @param {String} join The line join style. Can be "round", "bevel", * or "miter". * @param {Number} thickness The line thickness in pixels. * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern * within the stroke. */ this.strokeLayer = function(layer, cap, join, thickness, srcLayer) { scheduleTask(function __display_strokeLayer() { layer.strokeLayer(cap, join, thickness, srcLayer); }); }; /** * Fills the current path with the image within the specified layer. The * image data will be tiled infinitely within the stroke. The current path * is implicitly closed. The current path can continue to be reused * for other operations (such as clip()) but a new path will be started * once a path drawing operation (path() or rect()) is used. * * @param {Guacamole.Layer} layer The layer to draw upon. * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern * within the fill. */ this.fillLayer = function(layer, srcLayer) { scheduleTask(function __display_fillLayer() { layer.fillLayer(srcLayer); }); }; /** * Push current layer state onto stack. * * @param {Guacamole.Layer} layer The layer to draw upon. */ this.push = function(layer) { scheduleTask(function __display_push() { layer.push(); }); }; /** * Pop layer state off stack. * * @param {Guacamole.Layer} layer The layer to draw upon. */ this.pop = function(layer) { scheduleTask(function __display_pop() { layer.pop(); }); }; /** * Reset the layer, clearing the stack, the current path, and any transform * matrix. * * @param {Guacamole.Layer} layer The layer to draw upon. */ this.reset = function(layer) { scheduleTask(function __display_reset() { layer.reset(); }); }; /** * Sets the given affine transform (defined with six values from the * transform's matrix). * * @param {Guacamole.Layer} layer The layer to modify. * @param {Number} a The first value in the affine transform's matrix. * @param {Number} b The second value in the affine transform's matrix. * @param {Number} c The third value in the affine transform's matrix. * @param {Number} d The fourth value in the affine transform's matrix. * @param {Number} e The fifth value in the affine transform's matrix. * @param {Number} f The sixth value in the affine transform's matrix. */ this.setTransform = function(layer, a, b, c, d, e, f) { scheduleTask(function __display_setTransform() { layer.setTransform(a, b, c, d, e, f); }); }; /** * Applies the given affine transform (defined with six values from the * transform's matrix). * * @param {Guacamole.Layer} layer The layer to modify. * @param {Number} a The first value in the affine transform's matrix. * @param {Number} b The second value in the affine transform's matrix. * @param {Number} c The third value in the affine transform's matrix. * @param {Number} d The fourth value in the affine transform's matrix. * @param {Number} e The fifth value in the affine transform's matrix. * @param {Number} f The sixth value in the affine transform's matrix. */ this.transform = function(layer, a, b, c, d, e, f) { scheduleTask(function __display_transform() { layer.transform(a, b, c, d, e, f); }); }; /** * Sets the channel mask for future operations on this Layer. * * The channel mask is a Guacamole-specific compositing operation identifier * with a single bit representing each of four channels (in order): source * image where destination transparent, source where destination opaque, * destination where source transparent, and destination where source * opaque. * * @param {Guacamole.Layer} layer The layer to modify. * @param {Number} mask The channel mask for future operations on this * Layer. */ this.setChannelMask = function(layer, mask) { scheduleTask(function __display_setChannelMask() { layer.setChannelMask(mask); }); }; /** * Sets the miter limit for stroke operations using the miter join. This * limit is the maximum ratio of the size of the miter join to the stroke * width. If this ratio is exceeded, the miter will not be drawn for that * joint of the path. * * @param {Guacamole.Layer} layer The layer to modify. * @param {Number} limit The miter limit for stroke operations using the * miter join. */ this.setMiterLimit = function(layer, limit) { scheduleTask(function __display_setMiterLimit() { layer.setMiterLimit(limit); }); }; /** * Sets the scale of the client display element such that it renders at * a relatively smaller or larger size, without affecting the true * resolution of the display. * * @param {Number} scale The scale to resize to, where 1.0 is normal * size (1:1 scale). */ this.scale = function(scale) { display.style.transform = display.style.WebkitTransform = display.style.MozTransform = display.style.OTransform = display.style.msTransform = "scale(" + scale + "," + scale + ")"; displayScale = scale; // Update bounds size bounds.style.width = (displayWidth*displayScale) + "px"; bounds.style.height = (displayHeight*displayScale) + "px"; }; /** * Returns the scale of the display. * * @return {Number} The scale of the display. */ this.getScale = function() { return displayScale; }; /** * Returns a canvas element containing the entire display, with all child * layers composited within. * * @return {HTMLCanvasElement} A new canvas element containing a copy of * the display. */ this.flatten = function() { // Get destination canvas var canvas = document.createElement("canvas"); canvas.width = default_layer.width; canvas.height = default_layer.height; var context = canvas.getContext("2d"); // Returns sorted array of children function get_children(layer) { // Build array of children var children = []; for (var index in layer.children) children.push(layer.children[index]); // Sort children.sort(function children_comparator(a, b) { // Compare based on Z order var diff = a.z - b.z; if (diff !== 0) return diff; // If Z order identical, use document order var a_element = a.getElement(); var b_element = b.getElement(); var position = b_element.compareDocumentPosition(a_element); if (position & Node.DOCUMENT_POSITION_PRECEDING) return -1; if (position & Node.DOCUMENT_POSITION_FOLLOWING) return 1; // Otherwise, assume same return 0; }); // Done return children; } // Draws the contents of the given layer at the given coordinates function draw_layer(layer, x, y) { // Draw layer if (layer.width > 0 && layer.height > 0) { // Save and update alpha var initial_alpha = context.globalAlpha; context.globalAlpha *= layer.alpha / 255.0; // Copy data context.drawImage(layer.getCanvas(), x, y); // Draw all children var children = get_children(layer); for (var i=0; i layer.width) resizeWidth = opBoundX; else resizeWidth = layer.width; // Determine max height var resizeHeight; if (opBoundY > layer.height) resizeHeight = opBoundY; else resizeHeight = layer.height; // Resize if necessary layer.resize(resizeWidth, resizeHeight); } /** * Set to true if this Layer should resize itself to accomodate the * dimensions of any drawing operation, and false (the default) otherwise. * * Note that setting this property takes effect immediately, and thus may * take effect on operations that were started in the past but have not * yet completed. If you wish the setting of this flag to only modify * future operations, you will need to make the setting of this flag an * operation with sync(). * * @example * // Set autosize to true for all future operations * layer.sync(function() { * layer.autosize = true; * }); * * @type {Boolean} * @default false */ this.autosize = false; /** * The current width of this layer. * @type {Number} */ this.width = width; /** * The current height of this layer. * @type {Number} */ this.height = height; /** * Returns the canvas element backing this Layer. * @returns {Element} The canvas element backing this Layer. */ this.getCanvas = function() { return canvas; }; /** * Changes the size of this Layer to the given width and height. Resizing * is only attempted if the new size provided is actually different from * the current size. * * @param {Number} newWidth The new width to assign to this Layer. * @param {Number} newHeight The new height to assign to this Layer. */ this.resize = function(newWidth, newHeight) { if (newWidth !== layer.width || newHeight !== layer.height) resize(newWidth, newHeight); }; /** * Draws the specified image at the given coordinates. The image specified * must already be loaded. * * @param {Number} x The destination X coordinate. * @param {Number} y The destination Y coordinate. * @param {Image} image The image to draw. Note that this is an Image * object - not a URL. */ this.drawImage = function(x, y, image) { if (layer.autosize) fitRect(x, y, image.width, image.height); context.drawImage(image, x, y); }; /** * Transfer a rectangle of image data from one Layer to this Layer using the * specified transfer function. * * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. * @param {Number} srcx The X coordinate of the upper-left corner of the * rectangle within the source Layer's coordinate * space to copy data from. * @param {Number} srcy The Y coordinate of the upper-left corner of the * rectangle within the source Layer's coordinate * space to copy data from. * @param {Number} srcw The width of the rectangle within the source Layer's * coordinate space to copy data from. * @param {Number} srch The height of the rectangle within the source * Layer's coordinate space to copy data from. * @param {Number} x The destination X coordinate. * @param {Number} y The destination Y coordinate. * @param {Function} transferFunction The transfer function to use to * transfer data from source to * destination. */ this.transfer = function(srcLayer, srcx, srcy, srcw, srch, x, y, transferFunction) { var srcCanvas = srcLayer.getCanvas(); // If entire rectangle outside source canvas, stop if (srcx >= srcCanvas.width || srcy >= srcCanvas.height) return; // Otherwise, clip rectangle to area if (srcx + srcw > srcCanvas.width) srcw = srcCanvas.width - srcx; if (srcy + srch > srcCanvas.height) srch = srcCanvas.height - srcy; // Stop if nothing to draw. if (srcw === 0 || srch === 0) return; if (layer.autosize) fitRect(x, y, srcw, srch); // Get image data from src and dst var src = srcLayer.getCanvas().getContext("2d").getImageData(srcx, srcy, srcw, srch); var dst = context.getImageData(x , y, srcw, srch); // Apply transfer for each pixel for (var i=0; i= srcCanvas.width || srcy >= srcCanvas.height) return; // Otherwise, clip rectangle to area if (srcx + srcw > srcCanvas.width) srcw = srcCanvas.width - srcx; if (srcy + srch > srcCanvas.height) srch = srcCanvas.height - srcy; // Stop if nothing to draw. if (srcw === 0 || srch === 0) return; if (layer.autosize) fitRect(x, y, srcw, srch); // Get image data from src and dst var src = srcLayer.getCanvas().getContext("2d").getImageData(srcx, srcy, srcw, srch); context.putImageData(src, x, y); }; /** * Copy a rectangle of image data from one Layer to this Layer. This * operation will copy exactly the image data that will be drawn once all * operations of the source Layer that were pending at the time this * function was called are complete. This operation will not alter the * size of the source Layer even if its autosize property is set to true. * * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. * @param {Number} srcx The X coordinate of the upper-left corner of the * rectangle within the source Layer's coordinate * space to copy data from. * @param {Number} srcy The Y coordinate of the upper-left corner of the * rectangle within the source Layer's coordinate * space to copy data from. * @param {Number} srcw The width of the rectangle within the source Layer's * coordinate space to copy data from. * @param {Number} srch The height of the rectangle within the source * Layer's coordinate space to copy data from. * @param {Number} x The destination X coordinate. * @param {Number} y The destination Y coordinate. */ this.copy = function(srcLayer, srcx, srcy, srcw, srch, x, y) { var srcCanvas = srcLayer.getCanvas(); // If entire rectangle outside source canvas, stop if (srcx >= srcCanvas.width || srcy >= srcCanvas.height) return; // Otherwise, clip rectangle to area if (srcx + srcw > srcCanvas.width) srcw = srcCanvas.width - srcx; if (srcy + srch > srcCanvas.height) srch = srcCanvas.height - srcy; // Stop if nothing to draw. if (srcw === 0 || srch === 0) return; if (layer.autosize) fitRect(x, y, srcw, srch); context.drawImage(srcCanvas, srcx, srcy, srcw, srch, x, y, srcw, srch); }; /** * Starts a new path at the specified point. * * @param {Number} x The X coordinate of the point to draw. * @param {Number} y The Y coordinate of the point to draw. */ this.moveTo = function(x, y) { // Start a new path if current path is closed if (pathClosed) { context.beginPath(); pathClosed = false; } if (layer.autosize) fitRect(x, y, 0, 0); context.moveTo(x, y); }; /** * Add the specified line to the current path. * * @param {Number} x The X coordinate of the endpoint of the line to draw. * @param {Number} y The Y coordinate of the endpoint of the line to draw. */ this.lineTo = function(x, y) { // Start a new path if current path is closed if (pathClosed) { context.beginPath(); pathClosed = false; } if (layer.autosize) fitRect(x, y, 0, 0); context.lineTo(x, y); }; /** * Add the specified arc to the current path. * * @param {Number} x The X coordinate of the center of the circle which * will contain the arc. * @param {Number} y The Y coordinate of the center of the circle which * will contain the arc. * @param {Number} radius The radius of the circle. * @param {Number} startAngle The starting angle of the arc, in radians. * @param {Number} endAngle The ending angle of the arc, in radians. * @param {Boolean} negative Whether the arc should be drawn in order of * decreasing angle. */ this.arc = function(x, y, radius, startAngle, endAngle, negative) { // Start a new path if current path is closed if (pathClosed) { context.beginPath(); pathClosed = false; } if (layer.autosize) fitRect(x, y, 0, 0); context.arc(x, y, radius, startAngle, endAngle, negative); }; /** * Starts a new path at the specified point. * * @param {Number} cp1x The X coordinate of the first control point. * @param {Number} cp1y The Y coordinate of the first control point. * @param {Number} cp2x The X coordinate of the second control point. * @param {Number} cp2y The Y coordinate of the second control point. * @param {Number} x The X coordinate of the endpoint of the curve. * @param {Number} y The Y coordinate of the endpoint of the curve. */ this.curveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) { // Start a new path if current path is closed if (pathClosed) { context.beginPath(); pathClosed = false; } if (layer.autosize) fitRect(x, y, 0, 0); context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); }; /** * Closes the current path by connecting the end point with the start * point (if any) with a straight line. */ this.close = function() { context.closePath(); pathClosed = true; }; /** * Add the specified rectangle to the current path. * * @param {Number} x The X coordinate of the upper-left corner of the * rectangle to draw. * @param {Number} y The Y coordinate of the upper-left corner of the * rectangle to draw. * @param {Number} w The width of the rectangle to draw. * @param {Number} h The height of the rectangle to draw. */ this.rect = function(x, y, w, h) { // Start a new path if current path is closed if (pathClosed) { context.beginPath(); pathClosed = false; } if (layer.autosize) fitRect(x, y, w, h); context.rect(x, y, w, h); }; /** * Clip all future drawing operations by the current path. The current path * is implicitly closed. The current path can continue to be reused * for other operations (such as fillColor()) but a new path will be started * once a path drawing operation (path() or rect()) is used. */ this.clip = function() { // Set new clipping region context.clip(); // Path now implicitly closed pathClosed = true; }; /** * Stroke the current path with the specified color. The current path * is implicitly closed. The current path can continue to be reused * for other operations (such as clip()) but a new path will be started * once a path drawing operation (path() or rect()) is used. * * @param {String} cap The line cap style. Can be "round", "square", * or "butt". * @param {String} join The line join style. Can be "round", "bevel", * or "miter". * @param {Number} thickness The line thickness in pixels. * @param {Number} r The red component of the color to fill. * @param {Number} g The green component of the color to fill. * @param {Number} b The blue component of the color to fill. * @param {Number} a The alpha component of the color to fill. */ this.strokeColor = function(cap, join, thickness, r, g, b, a) { // Stroke with color context.lineCap = cap; context.lineJoin = join; context.lineWidth = thickness; context.strokeStyle = "rgba(" + r + "," + g + "," + b + "," + a/255.0 + ")"; context.stroke(); // Path now implicitly closed pathClosed = true; }; /** * Fills the current path with the specified color. The current path * is implicitly closed. The current path can continue to be reused * for other operations (such as clip()) but a new path will be started * once a path drawing operation (path() or rect()) is used. * * @param {Number} r The red component of the color to fill. * @param {Number} g The green component of the color to fill. * @param {Number} b The blue component of the color to fill. * @param {Number} a The alpha component of the color to fill. */ this.fillColor = function(r, g, b, a) { // Fill with color context.fillStyle = "rgba(" + r + "," + g + "," + b + "," + a/255.0 + ")"; context.fill(); // Path now implicitly closed pathClosed = true; }; /** * Stroke the current path with the image within the specified layer. The * image data will be tiled infinitely within the stroke. The current path * is implicitly closed. The current path can continue to be reused * for other operations (such as clip()) but a new path will be started * once a path drawing operation (path() or rect()) is used. * * @param {String} cap The line cap style. Can be "round", "square", * or "butt". * @param {String} join The line join style. Can be "round", "bevel", * or "miter". * @param {Number} thickness The line thickness in pixels. * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern * within the stroke. */ this.strokeLayer = function(cap, join, thickness, srcLayer) { // Stroke with image data context.lineCap = cap; context.lineJoin = join; context.lineWidth = thickness; context.strokeStyle = context.createPattern( srcLayer.getCanvas(), "repeat" ); context.stroke(); // Path now implicitly closed pathClosed = true; }; /** * Fills the current path with the image within the specified layer. The * image data will be tiled infinitely within the stroke. The current path * is implicitly closed. The current path can continue to be reused * for other operations (such as clip()) but a new path will be started * once a path drawing operation (path() or rect()) is used. * * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern * within the fill. */ this.fillLayer = function(srcLayer) { // Fill with image data context.fillStyle = context.createPattern( srcLayer.getCanvas(), "repeat" ); context.fill(); // Path now implicitly closed pathClosed = true; }; /** * Push current layer state onto stack. */ this.push = function() { // Save current state onto stack context.save(); stackSize++; }; /** * Pop layer state off stack. */ this.pop = function() { // Restore current state from stack if (stackSize > 0) { context.restore(); stackSize--; } }; /** * Reset the layer, clearing the stack, the current path, and any transform * matrix. */ this.reset = function() { // Clear stack while (stackSize > 0) { context.restore(); stackSize--; } // Restore to initial state context.restore(); context.save(); // Clear path context.beginPath(); pathClosed = false; }; /** * Sets the given affine transform (defined with six values from the * transform's matrix). * * @param {Number} a The first value in the affine transform's matrix. * @param {Number} b The second value in the affine transform's matrix. * @param {Number} c The third value in the affine transform's matrix. * @param {Number} d The fourth value in the affine transform's matrix. * @param {Number} e The fifth value in the affine transform's matrix. * @param {Number} f The sixth value in the affine transform's matrix. */ this.setTransform = function(a, b, c, d, e, f) { context.setTransform( a, b, c, d, e, f /*0, 0, 1*/ ); }; /** * Applies the given affine transform (defined with six values from the * transform's matrix). * * @param {Number} a The first value in the affine transform's matrix. * @param {Number} b The second value in the affine transform's matrix. * @param {Number} c The third value in the affine transform's matrix. * @param {Number} d The fourth value in the affine transform's matrix. * @param {Number} e The fifth value in the affine transform's matrix. * @param {Number} f The sixth value in the affine transform's matrix. */ this.transform = function(a, b, c, d, e, f) { context.transform( a, b, c, d, e, f /*0, 0, 1*/ ); }; /** * Sets the channel mask for future operations on this Layer. * * The channel mask is a Guacamole-specific compositing operation identifier * with a single bit representing each of four channels (in order): source * image where destination transparent, source where destination opaque, * destination where source transparent, and destination where source * opaque. * * @param {Number} mask The channel mask for future operations on this * Layer. */ this.setChannelMask = function(mask) { context.globalCompositeOperation = compositeOperation[mask]; }; /** * Sets the miter limit for stroke operations using the miter join. This * limit is the maximum ratio of the size of the miter join to the stroke * width. If this ratio is exceeded, the miter will not be drawn for that * joint of the path. * * @param {Number} limit The miter limit for stroke operations using the * miter join. */ this.setMiterLimit = function(limit) { context.miterLimit = limit; }; // Initialize canvas dimensions canvas.width = width; canvas.height = height; // Explicitly render canvas below other elements in the layer (such as // child layers). Chrome and others may fail to render layers properly // without this. canvas.style.zIndex = -1; }; /** * Channel mask for the composite operation "rout". */ Guacamole.Layer.ROUT = 0x2; /** * Channel mask for the composite operation "atop". */ Guacamole.Layer.ATOP = 0x6; /** * Channel mask for the composite operation "xor". */ Guacamole.Layer.XOR = 0xA; /** * Channel mask for the composite operation "rover". */ Guacamole.Layer.ROVER = 0xB; /** * Channel mask for the composite operation "over". */ Guacamole.Layer.OVER = 0xE; /** * Channel mask for the composite operation "plus". */ Guacamole.Layer.PLUS = 0xF; /** * Channel mask for the composite operation "rin". * Beware that WebKit-based browsers may leave the contents of the destionation * layer where the source layer is transparent, despite the definition of this * operation. */ Guacamole.Layer.RIN = 0x1; /** * Channel mask for the composite operation "in". * Beware that WebKit-based browsers may leave the contents of the destionation * layer where the source layer is transparent, despite the definition of this * operation. */ Guacamole.Layer.IN = 0x4; /** * Channel mask for the composite operation "out". * Beware that WebKit-based browsers may leave the contents of the destionation * layer where the source layer is transparent, despite the definition of this * operation. */ Guacamole.Layer.OUT = 0x8; /** * Channel mask for the composite operation "ratop". * Beware that WebKit-based browsers may leave the contents of the destionation * layer where the source layer is transparent, despite the definition of this * operation. */ Guacamole.Layer.RATOP = 0x9; /** * Channel mask for the composite operation "src". * Beware that WebKit-based browsers may leave the contents of the destionation * layer where the source layer is transparent, despite the definition of this * operation. */ Guacamole.Layer.SRC = 0xC; /** * Represents a single pixel of image data. All components have a minimum value * of 0 and a maximum value of 255. * * @constructor * * @param {Number} r The red component of this pixel. * @param {Number} g The green component of this pixel. * @param {Number} b The blue component of this pixel. * @param {Number} a The alpha component of this pixel. */ Guacamole.Layer.Pixel = function(r, g, b, a) { /** * The red component of this pixel, where 0 is the minimum value, * and 255 is the maximum. */ this.red = r; /** * The green component of this pixel, where 0 is the minimum value, * and 255 is the maximum. */ this.green = g; /** * The blue component of this pixel, where 0 is the minimum value, * and 255 is the maximum. */ this.blue = b; /** * The alpha component of this pixel, where 0 is the minimum value, * and 255 is the maximum. */ this.alpha = a; }; guacamole-client-0.9.9/guacamole-common-js/src/main/webapp/modules/Parser.js0100664 0001751 0001751 00000012066 12471273272 030633 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ var Guacamole = Guacamole || {}; /** * Simple Guacamole protocol parser that invokes an oninstruction event when * full instructions are available from data received via receive(). * * @constructor */ Guacamole.Parser = function() { /** * Reference to this parser. * @private */ var parser = this; /** * Current buffer of received data. This buffer grows until a full * element is available. After a full element is available, that element * is flushed into the element buffer. * * @private */ var buffer = ""; /** * Buffer of all received, complete elements. After an entire instruction * is read, this buffer is flushed, and a new instruction begins. * * @private */ var element_buffer = []; // The location of the last element's terminator var element_end = -1; // Where to start the next length search or the next element var start_index = 0; /** * Appends the given instruction data packet to the internal buffer of * this Guacamole.Parser, executing all completed instructions at * the beginning of this buffer, if any. * * @param {String} packet The instruction data to receive. */ this.receive = function(packet) { // Truncate buffer as necessary if (start_index > 4096 && element_end >= start_index) { buffer = buffer.substring(start_index); // Reset parse relative to truncation element_end -= start_index; start_index = 0; } // Append data to buffer buffer += packet; // While search is within currently received data while (element_end < buffer.length) { // If we are waiting for element data if (element_end >= start_index) { // We now have enough data for the element. Parse. var element = buffer.substring(start_index, element_end); var terminator = buffer.substring(element_end, element_end+1); // Add element to array element_buffer.push(element); // If last element, handle instruction if (terminator == ";") { // Get opcode var opcode = element_buffer.shift(); // Call instruction handler. if (parser.oninstruction != null) parser.oninstruction(opcode, element_buffer); // Clear elements element_buffer.length = 0; } else if (terminator != ',') throw new Error("Illegal terminator."); // Start searching for length at character after // element terminator start_index = element_end + 1; } // Search for end of length var length_end = buffer.indexOf(".", start_index); if (length_end != -1) { // Parse length var length = parseInt(buffer.substring(element_end+1, length_end)); if (length == NaN) throw new Error("Non-numeric character in element length."); // Calculate start of element start_index = length_end + 1; // Calculate location of element terminator element_end = start_index + length; } // If no period yet, continue search when more data // is received else { start_index = buffer.length; break; } } // end parse loop }; /** * Fired once for every complete Guacamole instruction received, in order. * * @event * @param {String} opcode The Guacamole instruction opcode. * @param {Array} parameters The parameters provided for the instruction, * if any. */ this.oninstruction = null; }; guacamole-client-0.9.9/guacamole-common-js/src/main/webapp/modules/ArrayBufferWriter.js0100664 0001751 0001751 00000006167 12556046101 033002 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ var Guacamole = Guacamole || {}; /** * A writer which automatically writes to the given output stream with arbitrary * binary data, supplied as ArrayBuffers. * * @constructor * @param {Guacamole.OutputStream} stream The stream that data will be written * to. */ Guacamole.ArrayBufferWriter = function(stream) { /** * Reference to this Guacamole.StringWriter. * @private */ var guac_writer = this; // Simply call onack for acknowledgements stream.onack = function(status) { if (guac_writer.onack) guac_writer.onack(status); }; /** * Encodes the given data as base64, sending it as a blob. The data must * be small enough to fit into a single blob instruction. * * @private * @param {Uint8Array} bytes The data to send. */ function __send_blob(bytes) { var binary = ""; // Produce binary string from bytes in buffer for (var i=0; i= buffer.length) { var new_buffer = new Uint8Array((length+bytes)*2); new_buffer.set(buffer); buffer = new_buffer; } length += bytes; } /** * Appends a single Unicode character to the current buffer, resizing the * buffer if necessary. The character will be encoded as UTF-8. * * @private * @param {Number} codepoint The codepoint of the Unicode character to * append. */ function __append_utf8(codepoint) { var mask; var bytes; // 1 byte if (codepoint <= 0x7F) { mask = 0x00; bytes = 1; } // 2 byte else if (codepoint <= 0x7FF) { mask = 0xC0; bytes = 2; } // 3 byte else if (codepoint <= 0xFFFF) { mask = 0xE0; bytes = 3; } // 4 byte else if (codepoint <= 0x1FFFFF) { mask = 0xF0; bytes = 4; } // If invalid codepoint, append replacement character else { __append_utf8(0xFFFD); return; } // Offset buffer by size __expand(bytes); var offset = length - 1; // Add trailing bytes, if any for (var i=1; i>= 6; } // Set initial byte buffer[offset] = mask | codepoint; } /** * Encodes the given string as UTF-8, returning an ArrayBuffer containing * the resulting bytes. * * @private * @param {String} text The string to encode as UTF-8. * @return {Uint8Array} The encoded UTF-8 data. */ function __encode_utf8(text) { // Fill buffer with UTF-8 for (var i=0; i 0) { var out_buffer = buffer.subarray(0, length); length = 0; return out_buffer; } } /** * Sends the given text. * * @param {String} text The text to send. */ this.sendText = function(text) { array_writer.sendData(__encode_utf8(text)); }; /** * Signals that no further text will be sent, effectively closing the * stream. */ this.sendEnd = function() { array_writer.sendEnd(); }; /** * Fired for received data, if acknowledged by the server. * @event * @param {Guacamole.Status} status The status of the operation. */ this.onack = null; };guacamole-client-0.9.9/guacamole-common-js/src/main/webapp/modules/Namespace.js0100664 0001751 0001751 00000002461 12645510255 031267 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The namespace used by the Guacamole JavaScript API. Absolutely all classes * defined by the Guacamole JavaScript API will be within this namespace. * * @namespace */ var Guacamole = Guacamole || {}; guacamole-client-0.9.9/guacamole-common-js/src/main/webapp/common/license.js0100664 0001751 0001751 00000002231 12471273272 030632 0ustar00mjumpermjumper0000000 0000000 /*! (C) 2014 Glyptodon LLC - glyptodon.org/MIT-LICENSE */ /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ guacamole-client-0.9.9/guacamole-common-js/jsdoc-conf.json0100664 0001751 0001751 00000000221 12645510255 025304 0ustar00mjumpermjumper0000000 0000000 { "source" : { "include" : "src" }, "opts" : { "recurse" : true, "destination" : "target/site/jsdoc" } } guacamole-client-0.9.9/guacamole-common-js/pom.xml0100664 0001751 0001751 00000007334 12645510255 023715 0ustar00mjumpermjumper0000000 0000000 4.0.0 org.glyptodon.guacamole guacamole-common-js pom 0.9.9 guacamole-common-js http://guac-dev.org/ The base JavaScript API of the Guacamole project, providing JavaScript support for the Guacamole stack, including a full client implementation for the Guacamole protocol. The MIT License http://www.opensource.org/licenses/mit-license.php repo https://github.com/glyptodon/guacamole-client scm:git:git://github.com/glyptodon/guacamole-client.git mike.jumper Michael Jumper mike.jumper@guac-dev.org james.muehlner James Muehlner james.muehlner@guac-dev.org UTF-8 maven-assembly-plugin 2.5.3 false static.xml make-zip package attached com.samaxes.maven minify-maven-plugin 1.6.1 default-minify UTF-8 CLOSURE / / all.js common/license.js modules/**/*.js minify guacamole-client-0.9.9/guacamole-common-js/ChangeLog0100664 0001751 0001751 00000003173 12424302752 024143 0ustar00mjumpermjumper0000000 0000000 2013-07-01 Michael Jumper * Add support for "file", "blob", and "end" instructions 2013-05-29 Michael Jumper * Fix support for AltGr and modifiers * Handle bad source rect sizes in copy 2012-10-24 Michael Jumper * Implement base audio support * Implement audio instruction * Add width/height getters to client * Add onresize event to client 2012-10-16 Michael Jumper * Fix stream inefficiency bug (ticket #201) 2012-09-27 Michael Jumper * Fix variable naming conflict (ticket #191) 2012-08-11 Michael Jumper * Improve documentation 2012-08-02 Michael Jumper * Fix keyboard event handling 2012-08-01 Michael Jumper * Implement absolute touch device emulation 2012-05-04 Michael Jumper * Improved iPad and touch device support * Improved touch support * Implemented new drawing instructions * Nestable layers 2011-12-11 Michael Jumper * Implemented improved instruction format * Fixed errors in IE in on-screen keyboard * Relicensed as Mozilla/LGPL/GPL * Touch support (emulates touchpad control of mouse pointer) * "rect" and "clip" instructions 2011-07-13 Michael Jumper * "sync" instruction * Channel masks (alpha compositing) * Multiple tunnel support 2011-03-02 Michael Jumper * Initial release guacamole-client-0.9.9/guacamole-common-js/LICENSE0100664 0001751 0001751 00000002041 12471273272 023375 0ustar00mjumpermjumper0000000 0000000 Copyright (C) 2013 Glyptodon LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. guacamole-client-0.9.9/guacamole/README0100664 0001751 0001751 00000004735 12424302752 021356 0ustar00mjumpermjumper0000000 0000000 ------------------------------------------------------------ About this README ------------------------------------------------------------ This README is intended to provide quick and to-the-point documentation for technical users intending to compile parts of Guacamole themselves. Distribution-specific packages are available from the files section of the main project page: http://sourceforge.net/projects/guacamole/files/ Distribution-specific documentation is provided on the Guacamole wiki: http://guac-dev.org/ ------------------------------------------------------------ What is Guacamole? ------------------------------------------------------------ Guacamole is an HTML5 web application that provides access to your desktop using remote desktop protocols. A centralized server acts as a tunnel and proxy, allowing access to multiple desktops through a web browser; no plugins needed. The client requires nothing more than a web browser supporting HTML5 and AJAX. The Guacamole project maintains this web application and the Java and C libraries and programs it depends on. These libraries and programs are separate in order to enable others to implement other applications using the same underlying technology. All components and dependencies of Guacamole are free and open source. ------------------------------------------------------------ Compiling and installing Guacamole ------------------------------------------------------------ Guacamole is built using Maven. Building Guacamole compiles all classes and packages them into a deployable .war file. This .war file can be installed and deployed under servlet containers like Apache Tomcat or Jetty. 1) Run mvn package $ mvn package Maven will download any needed dependencies for building the .jar file. Once all dependencies have been downloaded, the .war file will be created in the target/ subdirectory of the current directory. 2) Copy the .war file as directed in the instructions provided with your servlet container. Apache Tomcat, Jetty, and other servlet containers have specific and varying locations that .war files must be placed for the web application to be deployed. You will likely need to do this as root. ------------------------------------------------------------ Reporting problems ------------------------------------------------------------ Please report any bugs encountered by opening a new ticket at the Trac system hosted at: http://guac-dev.org/trac/ guacamole-client-0.9.9/guacamole/doc/example/user-mapping.xml0100664 0001751 0001751 00000002232 12424302752 026015 0ustar00mjumpermjumper0000000 0000000 vnc localhost 5900 VNCPASS vnc localhost 5901 VNCPASS vnc otherhost 5900 VNCPASS guacamole-client-0.9.9/guacamole/nb-configuration.xml0100664 0001751 0001751 00000002066 12640675105 024464 0ustar00mjumpermjumper0000000 0000000 ide guacamole-client-0.9.9/guacamole/src/main/resources/logback.xml0100664 0001751 0001751 00000003015 12612226251 026333 0ustar00mjumpermjumper0000000 0000000 %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/directives/locationChooser.js0100664 0001751 0001751 00000013122 12612226251 033315 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive for choosing the location of a connection or connection group. */ angular.module('manage').directive('locationChooser', [function locationChooser() { return { // Element only restrict: 'E', replace: true, scope: { /** * The identifier of the data source from which the given root * connection group was retrieved. * * @type String */ dataSource : '=', /** * The root connection group of the connection group hierarchy to * display. * * @type ConnectionGroup */ rootGroup : '=', /** * The unique identifier of the currently-selected connection * group. If not specified, the root group will be used. * * @type String */ value : '=' }, templateUrl: 'app/manage/templates/locationChooser.html', controller: ['$scope', function locationChooserController($scope) { /** * Map of unique identifiers to their corresponding connection * groups. * * @type Object. */ var connectionGroups = {}; /** * Recursively traverses the given connection group and all * children, storing each encountered connection group within the * connectionGroups map by its identifier. * * @param {GroupListItem} group * The connection group to traverse. */ var mapConnectionGroups = function mapConnectionGroups(group) { // Map given group connectionGroups[group.identifier] = group; // Map all child groups if (group.childConnectionGroups) group.childConnectionGroups.forEach(mapConnectionGroups); }; /** * Whether the group list menu is currently open. * * @type Boolean */ $scope.menuOpen = false; /** * The human-readable name of the currently-chosen connection * group. * * @type String */ $scope.chosenConnectionGroupName = null; /** * Toggle the current state of the menu listing connection groups. * If the menu is currently open, it will be closed. If currently * closed, it will be opened. */ $scope.toggleMenu = function toggleMenu() { $scope.menuOpen = !$scope.menuOpen; }; // Update the root group map when data source or root group change $scope.$watchGroup(['dataSource', 'rootGroup'], function updateRootGroups() { // Abort if the root group is not set if (!$scope.dataSource || !$scope.rootGroup) return null; // Wrap root group in map $scope.rootGroups = {}; $scope.rootGroups[$scope.dataSource] = $scope.rootGroup; }); // Expose selection function to group list template $scope.groupListContext = { /** * Selects the given group item. * * @param {GroupListItem} item * The chosen item. */ chooseGroup : function chooseGroup(item) { // Record new parent $scope.value = item.identifier; $scope.chosenConnectionGroupName = item.name; // Close menu $scope.menuOpen = false; } }; $scope.$watch('rootGroup', function setRootGroup(rootGroup) { connectionGroups = {}; if (!rootGroup) return; // Map all known groups mapConnectionGroups(rootGroup); // If no value is specified, default to the root identifier if (!$scope.value || !($scope.value in connectionGroups)) $scope.value = rootGroup.identifier; $scope.chosenConnectionGroupName = connectionGroups[$scope.value].name; }); }] }; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/manageModule.js0100664 0001751 0001751 00000002442 12612226251 030422 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The module for the administration functionality. */ angular.module('manage', [ 'form', 'groupList', 'list', 'locale', 'navigation', 'notification', 'rest' ]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/styles/attributes.css0100664 0001751 0001751 00000003726 12612226251 031717 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /* Do not stretch attributes to fit available area */ .attributes input[type=text], .attributes input[type=password], .attributes input[type=number] { width: auto; } .attributes .form .fields { display: table; margin: 1em; } .attributes .form .fields .labeled-field { display: table-row; } .attributes .form .fields .field-header, .attributes .form .fields .form-field { display: table-cell; padding: 0.125em; vertical-align: top; } .attributes .form .fields .field-header { padding-right: 1em; } .attributes .form h3 { font-size: 1.25em; font-weight: bold; text-transform: uppercase; padding: 0.75em 0.5em; margin: 1em 0; border-bottom: 1px solid rgba(0, 0, 0, 0.125); border-top: 1px solid rgba(0, 0, 0, 0.125); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.125); background: rgba(0, 0, 0, 0.04); width: 100%; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/styles/forms.css0100664 0001751 0001751 00000002423 12612226251 030650 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .manage table.properties th { text-align: left; font-weight: normal; padding-right: 1em; } .manage .action-buttons { text-align: center; margin-bottom: 1em; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/styles/connection-parameter.css0100664 0001751 0001751 00000003414 12612226251 033640 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /* Do not stretch connection parameters to fit available area */ .connection-parameters input[type=text], .connection-parameters input[type=password], .connection-parameters input[type=number] { width: auto; } .connection-parameters .form .fields { display: table; padding-left: .5em; border-left: 3px solid rgba(0,0,0,0.125); } .connection-parameters .form .fields .labeled-field { display: table-row; } .connection-parameters .form .fields .field-header, .connection-parameters .form .fields .form-field { display: table-cell; padding: 0.125em; vertical-align: top; } .connection-parameters .form .fields .field-header { padding-right: 1em; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/styles/locationChooser.css0100664 0001751 0001751 00000002521 12612226251 032654 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .location-chooser .dropdown { position: absolute; z-index: 2; margin-top: -1px; width: 3in; max-height: 2in; overflow: auto; border: 1px solid rgba(0, 0, 0, 0.5); background: white; font-size: 10pt; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/styles/manage-user.css0100664 0001751 0001751 00000004745 12645510255 031745 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .manage-user .username.header { margin-bottom: 0; } .manage-user .page-tabs .page-list li.read-only a[href], .manage-user .page-tabs .page-list li.unlinked a[href], .manage-user .page-tabs .page-list li.linked a[href] { padding-right: 2.5em; position: relative; } .manage-user .page-tabs .page-list li.read-only a[href]:before, .manage-user .page-tabs .page-list li.unlinked a[href]:before, .manage-user .page-tabs .page-list li.linked a[href]:before { content: ' '; position: absolute; right: 0; bottom: 0; top: 0; width: 2.5em; background-size: 1.25em; background-repeat: no-repeat; background-position: center; } .manage-user .page-tabs .page-list li.read-only a[href]:before { background-image: url('images/lock.png'); } .manage-user .page-tabs .page-list li.unlinked a[href]:before { background-image: url('images/plus.png'); } .manage-user .page-tabs .page-list li.unlinked a[href] { opacity: 0.5; } .manage-user .page-tabs .page-list li.unlinked a[href]:hover, .manage-user .page-tabs .page-list li.unlinked a[href].current { opacity: 1; } .manage-user .page-tabs .page-list li.linked a[href]:before { background-image: url('images/checkmark.png'); } .manage-user .notice.read-only { background: #FDA; border: 1px solid rgba(0, 0, 0, 0.125); border-radius: 0.25em; text-align: center; padding: 1em; }././@LongLink0100644 0000000 0000000 00000000153 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.jsguacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupControl0100664 0001751 0001751 00000024460 12612226251 035611 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The controller for editing or creating connection groups. */ angular.module('manage').controller('manageConnectionGroupController', ['$scope', '$injector', function manageConnectionGroupController($scope, $injector) { // Required types var ConnectionGroup = $injector.get('ConnectionGroup'); var PermissionSet = $injector.get('PermissionSet'); // Required services var $location = $injector.get('$location'); var $routeParams = $injector.get('$routeParams'); var authenticationService = $injector.get('authenticationService'); var connectionGroupService = $injector.get('connectionGroupService'); var guacNotification = $injector.get('guacNotification'); var permissionService = $injector.get('permissionService'); var schemaService = $injector.get('schemaService'); /** * An action to be provided along with the object sent to showStatus which * closes the currently-shown status dialog. */ var ACKNOWLEDGE_ACTION = { name : "MANAGE_CONNECTION_GROUP.ACTION_ACKNOWLEDGE", // Handle action callback : function acknowledgeCallback() { guacNotification.showStatus(false); } }; /** * The unique identifier of the data source containing the connection group * being edited. * * @type String */ $scope.selectedDataSource = $routeParams.dataSource; /** * The identifier of the connection group being edited. If a new connection * group is being created, this will not be defined. * * @type String */ var identifier = $routeParams.id; /** * The root connection group of the connection group hierarchy. * * @type ConnectionGroup */ $scope.rootGroup = null; /** * The connection group being modified. * * @type ConnectionGroup */ $scope.connectionGroup = null; /** * Whether the user has UPDATE permission for the current connection group. * * @type Boolean */ $scope.hasUpdatePermission = null; /** * Whether the user has DELETE permission for the current connection group. * * @type Boolean */ $scope.hasDeletePermission = null; /** * All permissions associated with the current user, or null if the user's * permissions have not yet been loaded. * * @type PermissionSet */ $scope.permissions = null; /** * All available connection group attributes. This is only the set of * attribute definitions, organized as logical groupings of attributes, not * attribute values. * * @type Form[] */ $scope.attributes = null; /** * Returns whether critical data has completed being loaded. * * @returns {Boolean} * true if enough data has been loaded for the user interface to be * useful, false otherwise. */ $scope.isLoaded = function isLoaded() { return $scope.rootGroup !== null && $scope.connectionGroup !== null && $scope.permissions !== null && $scope.attributes !== null && $scope.canSaveConnectionGroup !== null && $scope.canDeleteConnectionGroup !== null; }; // Pull connection group attribute schema schemaService.getConnectionGroupAttributes($scope.selectedDataSource) .success(function attributesReceived(attributes) { $scope.attributes = attributes; }); // Query the user's permissions for the current connection group permissionService.getPermissions($scope.selectedDataSource, authenticationService.getCurrentUsername()) .success(function permissionsReceived(permissions) { $scope.permissions = permissions; // Check if the connection group is new or if the user has UPDATE permission $scope.canSaveConnectionGroup = !identifier || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, identifier); // Check if connection group is not new and the user has DELETE permission $scope.canDeleteConnectionGroup = !!identifier && ( PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.DELETE, identifier) ); }); // Pull connection group hierarchy connectionGroupService.getConnectionGroupTree( $scope.selectedDataSource, ConnectionGroup.ROOT_IDENTIFIER, [PermissionSet.ObjectPermissionType.ADMINISTER] ) .success(function connectionGroupReceived(rootGroup) { $scope.rootGroup = rootGroup; }); // If we are editing an existing connection group, pull its data if (identifier) { connectionGroupService.getConnectionGroup($scope.selectedDataSource, identifier) .success(function connectionGroupReceived(connectionGroup) { $scope.connectionGroup = connectionGroup; }); } // If we are creating a new connection group, populate skeleton connection group data else $scope.connectionGroup = new ConnectionGroup(); /** * Available connection group types, as translation string / internal value * pairs. * * @type Object[] */ $scope.types = [ { label: "MANAGE_CONNECTION_GROUP.NAME_TYPE_ORGANIZATIONAL", value: ConnectionGroup.Type.ORGANIZATIONAL }, { label: "MANAGE_CONNECTION_GROUP.NAME_TYPE_BALANCING", value : ConnectionGroup.Type.BALANCING } ]; /** * Cancels all pending edits, returning to the management page. */ $scope.cancel = function cancel() { $location.path('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); }; /** * Saves the connection group, creating a new connection group or updating * the existing connection group. */ $scope.saveConnectionGroup = function saveConnectionGroup() { // Save the connection connectionGroupService.saveConnectionGroup($scope.selectedDataSource, $scope.connectionGroup) .success(function savedConnectionGroup() { $location.path('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); }) // Notify of any errors .error(function connectionGroupSaveFailed(error) { guacNotification.showStatus({ 'className' : 'error', 'title' : 'MANAGE_CONNECTION_GROUP.DIALOG_HEADER_ERROR', 'text' : error.message, 'actions' : [ ACKNOWLEDGE_ACTION ] }); }); }; /** * An action to be provided along with the object sent to showStatus which * immediately deletes the current connection group. */ var DELETE_ACTION = { name : "MANAGE_CONNECTION_GROUP.ACTION_DELETE", className : "danger", // Handle action callback : function deleteCallback() { deleteConnectionGroupImmediately(); guacNotification.showStatus(false); } }; /** * An action to be provided along with the object sent to showStatus which * closes the currently-shown status dialog. */ var CANCEL_ACTION = { name : "MANAGE_CONNECTION_GROUP.ACTION_CANCEL", // Handle action callback : function cancelCallback() { guacNotification.showStatus(false); } }; /** * Immediately deletes the current connection group, without prompting the * user for confirmation. */ var deleteConnectionGroupImmediately = function deleteConnectionGroupImmediately() { // Delete the connection group connectionGroupService.deleteConnectionGroup($scope.selectedDataSource, $scope.connectionGroup) .success(function deletedConnectionGroup() { $location.path('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); }) // Notify of any errors .error(function connectionGroupDeletionFailed(error) { guacNotification.showStatus({ 'className' : 'error', 'title' : 'MANAGE_CONNECTION_GROUP.DIALOG_HEADER_ERROR', 'text' : error.message, 'actions' : [ ACKNOWLEDGE_ACTION ] }); }); }; /** * Deletes the connection group, prompting the user first to confirm that * deletion is desired. */ $scope.deleteConnectionGroup = function deleteConnectionGroup() { // Confirm deletion request guacNotification.showStatus({ 'title' : 'MANAGE_CONNECTION_GROUP.DIALOG_HEADER_CONFIRM_DELETE', 'text' : 'MANAGE_CONNECTION_GROUP.TEXT_CONFIRM_DELETE', 'actions' : [ DELETE_ACTION, CANCEL_ACTION] }); }; }]); ././@LongLink0100644 0000000 0000000 00000000146 12645506003 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.jsguacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.j0100664 0001751 0001751 00000037356 12645506003 035541 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The controller for editing or creating connections. */ angular.module('manage').controller('manageConnectionController', ['$scope', '$injector', function manageConnectionController($scope, $injector) { // Required types var Connection = $injector.get('Connection'); var ConnectionGroup = $injector.get('ConnectionGroup'); var HistoryEntryWrapper = $injector.get('HistoryEntryWrapper'); var PermissionSet = $injector.get('PermissionSet'); // Required services var $location = $injector.get('$location'); var $routeParams = $injector.get('$routeParams'); var $translate = $injector.get('$translate'); var authenticationService = $injector.get('authenticationService'); var guacNotification = $injector.get('guacNotification'); var connectionService = $injector.get('connectionService'); var connectionGroupService = $injector.get('connectionGroupService'); var permissionService = $injector.get('permissionService'); var schemaService = $injector.get('schemaService'); var translationStringService = $injector.get('translationStringService'); /** * An action to be provided along with the object sent to showStatus which * closes the currently-shown status dialog. */ var ACKNOWLEDGE_ACTION = { name : "MANAGE_CONNECTION.ACTION_ACKNOWLEDGE", // Handle action callback : function acknowledgeCallback() { guacNotification.showStatus(false); } }; /** * The unique identifier of the data source containing the connection being * edited. * * @type String */ $scope.selectedDataSource = $routeParams.dataSource; /** * The identifier of the original connection from which this connection is * being cloned. Only valid if this is a new connection. * * @type String */ var cloneSourceIdentifier = $location.search().clone; /** * The identifier of the connection being edited. If a new connection is * being created, this will not be defined. * * @type String */ var identifier = $routeParams.id; /** * All known protocols. * * @type Object. */ $scope.protocols = null; /** * The root connection group of the connection group hierarchy. * * @type ConnectionGroup */ $scope.rootGroup = null; /** * The connection being modified. * * @type Connection */ $scope.connection = null; /** * The parameter name/value pairs associated with the connection being * modified. * * @type Object. */ $scope.parameters = null; /** * The date format for use within the connection history. * * @type String */ $scope.historyDateFormat = null; /** * The usage history of the connection being modified. * * @type HistoryEntryWrapper[] */ $scope.historyEntryWrappers = null; /** * Whether the user can save the connection being edited. This could be * updating an existing connection, or creating a new connection. * * @type Boolean */ $scope.canSaveConnection = null; /** * Whether the user can delete the connection being edited. * * @type Boolean */ $scope.canDeleteConnection = null; /** * Whether the user can clone the connection being edited. * * @type Boolean */ $scope.canCloneConnection = null; /** * All permissions associated with the current user, or null if the user's * permissions have not yet been loaded. * * @type PermissionSet */ $scope.permissions = null; /** * All available connection attributes. This is only the set of attribute * definitions, organized as logical groupings of attributes, not attribute * values. * * @type Form[] */ $scope.attributes = null; /** * Returns whether critical data has completed being loaded. * * @returns {Boolean} * true if enough data has been loaded for the user interface to be * useful, false otherwise. */ $scope.isLoaded = function isLoaded() { return $scope.protocols !== null && $scope.rootGroup !== null && $scope.connection !== null && $scope.parameters !== null && $scope.historyDateFormat !== null && $scope.historyEntryWrappers !== null && $scope.permissions !== null && $scope.attributes !== null && $scope.canSaveConnection !== null && $scope.canDeleteConnection !== null && $scope.canCloneConnection !== null; }; // Pull connection attribute schema schemaService.getConnectionAttributes($scope.selectedDataSource) .success(function attributesReceived(attributes) { $scope.attributes = attributes; }); // Pull connection group hierarchy connectionGroupService.getConnectionGroupTree( $scope.selectedDataSource, ConnectionGroup.ROOT_IDENTIFIER, [PermissionSet.ObjectPermissionType.ADMINISTER] ) .success(function connectionGroupReceived(rootGroup) { $scope.rootGroup = rootGroup; }); // Query the user's permissions for the current connection permissionService.getPermissions($scope.selectedDataSource, authenticationService.getCurrentUsername()) .success(function permissionsReceived(permissions) { $scope.permissions = permissions; // Check if the connection is new or if the user has UPDATE permission $scope.canSaveConnection = !identifier || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, identifier); // Check if connection is not new and the user has DELETE permission $scope.canDeleteConnection = !!identifier && ( PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.DELETE, identifier) ); // Check if the connection is not new and the user has UPDATE and CREATE_CONNECTION permissions $scope.canCloneConnection = !!identifier && ( PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) || ( PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, identifier) && PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION) ) ); }); // Get protocol metadata schemaService.getProtocols($scope.selectedDataSource) .success(function protocolsReceived(protocols) { $scope.protocols = protocols; }); // Get history date format $translate('MANAGE_CONNECTION.FORMAT_HISTORY_START').then(function historyDateFormatReceived(historyDateFormat) { $scope.historyDateFormat = historyDateFormat; }); // If we are editing an existing connection, pull its data if (identifier) { // Pull data from existing connection connectionService.getConnection($scope.selectedDataSource, identifier) .success(function connectionRetrieved(connection) { $scope.connection = connection; }); // Pull connection history connectionService.getConnectionHistory($scope.selectedDataSource, identifier) .success(function historyReceived(historyEntries) { // Wrap all history entries for sake of display $scope.historyEntryWrappers = []; historyEntries.forEach(function wrapHistoryEntry(historyEntry) { $scope.historyEntryWrappers.push(new HistoryEntryWrapper(historyEntry)); }); }); // Pull connection parameters connectionService.getConnectionParameters($scope.selectedDataSource, identifier) .success(function parametersReceived(parameters) { $scope.parameters = parameters; }); } // If we are cloning an existing connection, pull its data instead else if (cloneSourceIdentifier) { // Pull data from cloned connection connectionService.getConnection($scope.selectedDataSource, cloneSourceIdentifier) .success(function connectionRetrieved(connection) { $scope.connection = connection; // Clear the identifier field because this connection is new delete $scope.connection.identifier; }); // Do not pull connection history $scope.historyEntryWrappers = []; // Pull connection parameters from cloned connection connectionService.getConnectionParameters($scope.selectedDataSource, cloneSourceIdentifier) .success(function parametersReceived(parameters) { $scope.parameters = parameters; }); } // If we are creating a new connection, populate skeleton connection data else { $scope.connection = new Connection({ protocol: 'vnc' }); $scope.historyEntryWrappers = []; $scope.parameters = {}; } /** * Returns the translation string namespace for the protocol having the * given name. The namespace will be of the form: * * PROTOCOL_NAME * * where NAME is the protocol name transformed via * translationStringService.canonicalize(). * * @param {String} protocolName * The name of the protocol. * * @returns {String} * The translation namespace for the protocol specified, or null if no * namespace could be generated. */ $scope.getNamespace = function getNamespace(protocolName) { // Do not generate a namespace if no protocol is selected if (!protocolName) return null; return 'PROTOCOL_' + translationStringService.canonicalize(protocolName); }; /** * Given the internal name of a protocol, produces the translation string * for the localized version of that protocol's name. The translation * string will be of the form: * * NAMESPACE.NAME * * where NAMESPACE is the namespace generated from * $scope.getNamespace(). * * @param {String} protocolName * The name of the protocol. * * @returns {String} * The translation string which produces the localized name of the * protocol specified. */ $scope.getProtocolName = function getProtocolName(protocolName) { return $scope.getNamespace(protocolName) + '.NAME'; }; /** * Cancels all pending edits, returning to the management page. */ $scope.cancel = function cancel() { $location.path('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); }; /** * Cancels all pending edits, opening an edit page for a new connection * which is prepopulated with the data from the connection currently being edited. */ $scope.cloneConnection = function cloneConnection() { $location.path('/manage/' + encodeURIComponent($scope.selectedDataSource) + '/connections').search('clone', identifier); }; /** * Saves the connection, creating a new connection or updating the existing * connection. */ $scope.saveConnection = function saveConnection() { $scope.connection.parameters = $scope.parameters; // Save the connection connectionService.saveConnection($scope.selectedDataSource, $scope.connection) .success(function savedConnection() { $location.path('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); }) // Notify of any errors .error(function connectionSaveFailed(error) { guacNotification.showStatus({ 'className' : 'error', 'title' : 'MANAGE_CONNECTION.DIALOG_HEADER_ERROR', 'text' : error.message, 'actions' : [ ACKNOWLEDGE_ACTION ] }); }); }; /** * An action to be provided along with the object sent to showStatus which * immediately deletes the current connection. */ var DELETE_ACTION = { name : "MANAGE_CONNECTION.ACTION_DELETE", className : "danger", // Handle action callback : function deleteCallback() { deleteConnectionImmediately(); guacNotification.showStatus(false); } }; /** * An action to be provided along with the object sent to showStatus which * closes the currently-shown status dialog. */ var CANCEL_ACTION = { name : "MANAGE_CONNECTION.ACTION_CANCEL", // Handle action callback : function cancelCallback() { guacNotification.showStatus(false); } }; /** * Immediately deletes the current connection, without prompting the user * for confirmation. */ var deleteConnectionImmediately = function deleteConnectionImmediately() { // Delete the connection connectionService.deleteConnection($scope.selectedDataSource, $scope.connection) .success(function deletedConnection() { $location.path('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); }) // Notify of any errors .error(function connectionDeletionFailed(error) { guacNotification.showStatus({ 'className' : 'error', 'title' : 'MANAGE_CONNECTION.DIALOG_HEADER_ERROR', 'text' : error.message, 'actions' : [ ACKNOWLEDGE_ACTION ] }); }); }; /** * Deletes the connection, prompting the user first to confirm that * deletion is desired. */ $scope.deleteConnection = function deleteConnection() { // Confirm deletion request guacNotification.showStatus({ 'title' : 'MANAGE_CONNECTION.DIALOG_HEADER_CONFIRM_DELETE', 'text' : 'MANAGE_CONNECTION.TEXT_CONFIRM_DELETE', 'actions' : [ DELETE_ACTION, CANCEL_ACTION] }); }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js0100664 0001751 0001751 00000103563 12645510255 034541 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The controller for editing users. */ angular.module('manage').controller('manageUserController', ['$scope', '$injector', function manageUserController($scope, $injector) { // Required types var ConnectionGroup = $injector.get('ConnectionGroup'); var PageDefinition = $injector.get('PageDefinition'); var PermissionFlagSet = $injector.get('PermissionFlagSet'); var PermissionSet = $injector.get('PermissionSet'); var User = $injector.get('User'); // Required services var $location = $injector.get('$location'); var $routeParams = $injector.get('$routeParams'); var authenticationService = $injector.get('authenticationService'); var connectionGroupService = $injector.get('connectionGroupService'); var dataSourceService = $injector.get('dataSourceService'); var guacNotification = $injector.get('guacNotification'); var permissionService = $injector.get('permissionService'); var schemaService = $injector.get('schemaService'); var translationStringService = $injector.get('translationStringService'); var userService = $injector.get('userService'); /** * An action to be provided along with the object sent to showStatus which * closes the currently-shown status dialog. */ var ACKNOWLEDGE_ACTION = { name : "MANAGE_USER.ACTION_ACKNOWLEDGE", // Handle action callback : function acknowledgeCallback() { guacNotification.showStatus(false); } }; /** * The identifiers of all data sources currently available to the * authenticated user. * * @type String[] */ var dataSources = authenticationService.getAvailableDataSources(); /** * The username of the current, authenticated user. * * @type String */ var currentUsername = authenticationService.getCurrentUsername(); /** * The unique identifier of the data source containing the user being * edited. * * @type String */ var selectedDataSource = $routeParams.dataSource; /** * The username of the user being edited. If a new user is * being created, this will not be defined. * * @type String */ var username = $routeParams.id; /** * All user accounts associated with the same username as the account being * created or edited, as a map of data source identifier to the User object * within that data source. * * @type Object. */ $scope.users = null; /** * The user being modified. * * @type User */ $scope.user = null; /** * All permissions associated with the user being modified. * * @type PermissionFlagSet */ $scope.permissionFlags = null; /** * A map of data source identifiers to the root connection groups within * thost data sources. As only one data source is applicable to any one * user being edited/created, this will only contain a single key. * * @type Object. */ $scope.rootGroups = null; /** * Array of all connection properties that are filterable. * * @type String[] */ $scope.filteredConnectionProperties = [ 'name', 'protocol' ]; /** * Array of all connection group properties that are filterable. * * @type String[] */ $scope.filteredConnectionGroupProperties = [ 'name' ]; /** * A map of data source identifiers to the set of all permissions * associated with the current user under that data source, or null if the * user's permissions have not yet been loaded. * * @type Object. */ $scope.permissions = null; /** * All available user attributes. This is only the set of attribute * definitions, organized as logical groupings of attributes, not attribute * values. * * @type Form[] */ $scope.attributes = null; /** * The pages associated with each user account having the given username. * Each user account will be associated with a particular data source. * * @type PageDefinition[] */ $scope.accountPages = []; /** * Returns whether critical data has completed being loaded. * * @returns {Boolean} * true if enough data has been loaded for the user interface to be * useful, false otherwise. */ $scope.isLoaded = function isLoaded() { return $scope.users !== null && $scope.permissionFlags !== null && $scope.rootGroups !== null && $scope.permissions !== null && $scope.attributes !== null; }; /** * Returns whether the user being edited already exists within the data * source specified. * * @param {String} [dataSource] * The identifier of the data source to check. If omitted, this will * default to the currently-selected data source. * * @returns {Boolean} * true if the user being edited already exists, false otherwise. */ $scope.userExists = function userExists(dataSource) { // Do not check if users are not yet loaded if (!$scope.users) return false; // Use currently-selected data source if unspecified dataSource = dataSource || selectedDataSource; // Account exists only if it was successfully retrieved return (dataSource in $scope.users); }; /** * Returns whether the current user can change attributes associated with * the user being edited within the given data source. * * @param {String} [dataSource] * The identifier of the data source to check. If omitted, this will * default to the currently-selected data source. * * @returns {Boolean} * true if the current user can change attributes associated with the * user being edited, false otherwise. */ $scope.canChangeAttributes = function canChangeAttributes(dataSource) { // Do not check if permissions are not yet loaded if (!$scope.permissions) return false; // Use currently-selected data source if unspecified dataSource = dataSource || selectedDataSource; // Attributes can always be set if we are creating the user if (!$scope.userExists(dataSource)) return true; // The administrator can always change attributes if (PermissionSet.hasSystemPermission($scope.permissions[dataSource], PermissionSet.SystemPermissionType.ADMINISTER)) return true; // Otherwise, can change attributes if we have permission to update this user return PermissionSet.hasUserPermission($scope.permissions[dataSource], PermissionSet.ObjectPermissionType.UPDATE, username); }; /** * Returns whether the current user can change permissions of any kind * which are associated with the user being edited within the given data * source. * * @param {String} [dataSource] * The identifier of the data source to check. If omitted, this will * default to the currently-selected data source. * * @returns {Boolean} * true if the current user can grant or revoke permissions of any kind * which are associated with the user being edited, false otherwise. */ $scope.canChangePermissions = function canChangePermissions(dataSource) { // Do not check if permissions are not yet loaded if (!$scope.permissions) return false; // Use currently-selected data source if unspecified dataSource = dataSource || selectedDataSource; // Permissions can always be set if we are creating the user if (!$scope.userExists(dataSource)) return true; // The administrator can always modify permissions if (PermissionSet.hasSystemPermission($scope.permissions[dataSource], PermissionSet.SystemPermissionType.ADMINISTER)) return true; // Otherwise, can only modify permissions if we have explicit // ADMINISTER permission return PermissionSet.hasUserPermission($scope.permissions[dataSource], PermissionSet.ObjectPermissionType.ADMINISTER, username); }; /** * Returns whether the current user can change the system permissions * granted to the user being edited within the given data source. * * @param {String} [dataSource] * The identifier of the data source to check. If omitted, this will * default to the currently-selected data source. * * @returns {Boolean} * true if the current user can grant or revoke system permissions to * the user being edited, false otherwise. */ $scope.canChangeSystemPermissions = function canChangeSystemPermissions(dataSource) { // Do not check if permissions are not yet loaded if (!$scope.permissions) return false; // Use currently-selected data source if unspecified dataSource = dataSource || selectedDataSource; // Only the administrator can modify system permissions return PermissionSet.hasSystemPermission($scope.permissions[dataSource], PermissionSet.SystemPermissionType.ADMINISTER); }; /** * Returns whether the current user can edit the username of the user being * edited within the given data source. * * @param {String} [dataSource] * The identifier of the data source to check. If omitted, this will * default to the currently-selected data source. * * @returns {Boolean} * true if the current user can edit the username of the user being * edited, false otherwise. */ $scope.canEditUsername = function canEditUsername(dataSource) { return !username; }; /** * Returns whether the current user can save the user being edited within * the given data source. Saving will create or update that user depending * on whether the user already exists. * * @param {String} [dataSource] * The identifier of the data source to check. If omitted, this will * default to the currently-selected data source. * * @returns {Boolean} * true if the current user can save changes to the user being edited, * false otherwise. */ $scope.canSaveUser = function canSaveUser(dataSource) { // Do not check if permissions are not yet loaded if (!$scope.permissions) return false; // Use currently-selected data source if unspecified dataSource = dataSource || selectedDataSource; // The administrator can always save users if (PermissionSet.hasSystemPermission($scope.permissions[dataSource], PermissionSet.SystemPermissionType.ADMINISTER)) return true; // If user does not exist, can only save if we have permission to create users if (!$scope.userExists(dataSource)) return PermissionSet.hasSystemPermission($scope.permissions[dataSource], PermissionSet.SystemPermissionType.CREATE_USER); // Otherwise, can only save if we have permission to update this user return PermissionSet.hasUserPermission($scope.permissions[dataSource], PermissionSet.ObjectPermissionType.UPDATE, username); }; /** * Returns whether the current user can delete the user being edited from * the given data source. * * @param {String} [dataSource] * The identifier of the data source to check. If omitted, this will * default to the currently-selected data source. * * @returns {Boolean} * true if the current user can delete the user being edited, false * otherwise. */ $scope.canDeleteUser = function canDeleteUser(dataSource) { // Do not check if permissions are not yet loaded if (!$scope.permissions) return false; // Use currently-selected data source if unspecified dataSource = dataSource || selectedDataSource; // Can't delete what doesn't exist if (!$scope.userExists(dataSource)) return false; // The administrator can always delete users if (PermissionSet.hasSystemPermission($scope.permissions[dataSource], PermissionSet.SystemPermissionType.ADMINISTER)) return true; // Otherwise, require explicit DELETE permission on the user return PermissionSet.hasUserPermission($scope.permissions[dataSource], PermissionSet.ObjectPermissionType.DELETE, username); }; /** * Returns whether the user being edited within the given data source is * read-only, and thus cannot be modified by the current user. * * @param {String} [dataSource] * The identifier of the data source to check. If omitted, this will * default to the currently-selected data source. * * @returns {Boolean} * true if the user being edited is actually read-only and cannot be * edited at all, false otherwise. */ $scope.isReadOnly = function isReadOnly(dataSource) { // Use currently-selected data source if unspecified dataSource = dataSource || selectedDataSource; // User is read-only if they cannot be saved return !$scope.canSaveUser(dataSource); }; // Update visible account pages whenever available users/permissions changes $scope.$watchGroup(['users', 'permissions'], function updateAccountPages() { // Generate pages for each applicable data source $scope.accountPages = []; angular.forEach(dataSources, function addAccountPage(dataSource) { // Determine whether data source contains this user var linked = $scope.userExists(dataSource); var readOnly = $scope.isReadOnly(dataSource); // Account is not relevant if it does not exist and cannot be // created if (!linked && readOnly) return; // Determine class name based on read-only / linked status var className; if (readOnly) className = 'read-only'; else if (linked) className = 'linked'; else className = 'unlinked'; // Add page entry $scope.accountPages.push(new PageDefinition({ name : translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME', url : '/manage/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username || ''), className : className })); }); }); // Pull user attribute schema schemaService.getUserAttributes(selectedDataSource).success(function attributesReceived(attributes) { $scope.attributes = attributes; }); // Pull user data and permissions if we are editing an existing user if (username) { // Pull user data dataSourceService.apply(userService.getUser, dataSources, username) .then(function usersReceived(users) { // Get user for currently-selected data source $scope.users = users; $scope.user = users[selectedDataSource]; // Create skeleton user if user does not exist if (!$scope.user) $scope.user = new User({ 'username' : username }); }); // Pull user permissions permissionService.getPermissions(selectedDataSource, username).success(function gotPermissions(permissions) { $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(permissions); }) // If permissions cannot be retrieved, use empty permissions .error(function permissionRetrievalFailed() { $scope.permissionFlags = new PermissionFlagSet(); }); } // Use skeleton data if we are creating a new user else { // No users exist regardless of data source if there is no username $scope.users = {}; // Use skeleton user object with no associated permissions $scope.user = new User(); $scope.permissionFlags = new PermissionFlagSet(); } // Retrieve all connections for which we have ADMINISTER permission dataSourceService.apply( connectionGroupService.getConnectionGroupTree, [selectedDataSource], ConnectionGroup.ROOT_IDENTIFIER, [PermissionSet.ObjectPermissionType.ADMINISTER] ) .then(function connectionGroupReceived(rootGroups) { $scope.rootGroups = rootGroups; }); // Query the user's permissions for the current user dataSourceService.apply( permissionService.getPermissions, dataSources, currentUsername ) .then(function permissionsReceived(permissions) { $scope.permissions = permissions; }); /** * Available system permission types, as translation string / internal * value pairs. * * @type Object[] */ $scope.systemPermissionTypes = [ { label: "MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM", value: PermissionSet.SystemPermissionType.ADMINISTER }, { label: "MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS", value: PermissionSet.SystemPermissionType.CREATE_USER }, { label: "MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS", value: PermissionSet.SystemPermissionType.CREATE_CONNECTION }, { label: "MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS", value: PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP } ]; /** * The set of permissions that will be added to the user when the user is * saved. Permissions will only be present in this set if they are * manually added, and not later manually removed before saving. * * @type PermissionSet */ var permissionsAdded = new PermissionSet(); /** * The set of permissions that will be removed from the user when the user * is saved. Permissions will only be present in this set if they are * manually removed, and not later manually added before saving. * * @type PermissionSet */ var permissionsRemoved = new PermissionSet(); /** * Updates the permissionsAdded and permissionsRemoved permission sets to * reflect the addition of the given system permission. * * @param {String} type * The system permission to add, as defined by * PermissionSet.SystemPermissionType. */ var addSystemPermission = function addSystemPermission(type) { // If permission was previously removed, simply un-remove it if (PermissionSet.hasSystemPermission(permissionsRemoved, type)) PermissionSet.removeSystemPermission(permissionsRemoved, type); // Otherwise, explicitly add the permission else PermissionSet.addSystemPermission(permissionsAdded, type); }; /** * Updates the permissionsAdded and permissionsRemoved permission sets to * reflect the removal of the given system permission. * * @param {String} type * The system permission to remove, as defined by * PermissionSet.SystemPermissionType. */ var removeSystemPermission = function removeSystemPermission(type) { // If permission was previously added, simply un-add it if (PermissionSet.hasSystemPermission(permissionsAdded, type)) PermissionSet.removeSystemPermission(permissionsAdded, type); // Otherwise, explicitly remove the permission else PermissionSet.addSystemPermission(permissionsRemoved, type); }; /** * Notifies the controller that a change has been made to the given * system permission for the user being edited. * * @param {String} type * The system permission that was changed, as defined by * PermissionSet.SystemPermissionType. */ $scope.systemPermissionChanged = function systemPermissionChanged(type) { // Determine current permission setting var value = $scope.permissionFlags.systemPermissions[type]; // Add/remove permission depending on flag state if (value) addSystemPermission(type); else removeSystemPermission(type); }; /** * Updates the permissionsAdded and permissionsRemoved permission sets to * reflect the addition of the given user permission. * * @param {String} type * The user permission to add, as defined by * PermissionSet.ObjectPermissionType. * * @param {String} identifier * The identifier of the user affected by the permission being added. */ var addUserPermission = function addUserPermission(type, identifier) { // If permission was previously removed, simply un-remove it if (PermissionSet.hasUserPermission(permissionsRemoved, type, identifier)) PermissionSet.removeUserPermission(permissionsRemoved, type, identifier); // Otherwise, explicitly add the permission else PermissionSet.addUserPermission(permissionsAdded, type, identifier); }; /** * Updates the permissionsAdded and permissionsRemoved permission sets to * reflect the removal of the given user permission. * * @param {String} type * The user permission to remove, as defined by * PermissionSet.ObjectPermissionType. * * @param {String} identifier * The identifier of the user affected by the permission being removed. */ var removeUserPermission = function removeUserPermission(type, identifier) { // If permission was previously added, simply un-add it if (PermissionSet.hasUserPermission(permissionsAdded, type, identifier)) PermissionSet.removeUserPermission(permissionsAdded, type, identifier); // Otherwise, explicitly remove the permission else PermissionSet.addUserPermission(permissionsRemoved, type, identifier); }; /** * Notifies the controller that a change has been made to the given user * permission for the user being edited. * * @param {String} type * The user permission that was changed, as defined by * PermissionSet.ObjectPermissionType. * * @param {String} identifier * The identifier of the user affected by the changed permission. */ $scope.userPermissionChanged = function userPermissionChanged(type, identifier) { // Determine current permission setting var value = $scope.permissionFlags.userPermissions[type][identifier]; // Add/remove permission depending on flag state if (value) addUserPermission(type, identifier); else removeUserPermission(type, identifier); }; /** * Updates the permissionsAdded and permissionsRemoved permission sets to * reflect the addition of the given connection permission. * * @param {String} identifier * The identifier of the connection to add READ permission for. */ var addConnectionPermission = function addConnectionPermission(identifier) { // If permission was previously removed, simply un-remove it if (PermissionSet.hasConnectionPermission(permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier)) PermissionSet.removeConnectionPermission(permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); // Otherwise, explicitly add the permission else PermissionSet.addConnectionPermission(permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); }; /** * Updates the permissionsAdded and permissionsRemoved permission sets to * reflect the removal of the given connection permission. * * @param {String} identifier * The identifier of the connection to remove READ permission for. */ var removeConnectionPermission = function removeConnectionPermission(identifier) { // If permission was previously added, simply un-add it if (PermissionSet.hasConnectionPermission(permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier)) PermissionSet.removeConnectionPermission(permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); // Otherwise, explicitly remove the permission else PermissionSet.addConnectionPermission(permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); }; /** * Updates the permissionsAdded and permissionsRemoved permission sets to * reflect the addition of the given connection group permission. * * @param {String} identifier * The identifier of the connection group to add READ permission for. */ var addConnectionGroupPermission = function addConnectionGroupPermission(identifier) { // If permission was previously removed, simply un-remove it if (PermissionSet.hasConnectionGroupPermission(permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier)) PermissionSet.removeConnectionGroupPermission(permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); // Otherwise, explicitly add the permission else PermissionSet.addConnectionGroupPermission(permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); }; /** * Updates the permissionsAdded and permissionsRemoved permission sets to * reflect the removal of the given connection permission. * * @param {String} identifier * The identifier of the connection to remove READ permission for. */ var removeConnectionGroupPermission = function removeConnectionGroupPermission(identifier) { // If permission was previously added, simply un-add it if (PermissionSet.hasConnectionGroupPermission(permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier)) PermissionSet.removeConnectionGroupPermission(permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); // Otherwise, explicitly remove the permission else PermissionSet.addConnectionGroupPermission(permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); }; // Expose permission query and modification functions to group list template $scope.groupListContext = { /** * Returns the PermissionFlagSet that contains the current state of * granted permissions. * * @returns {PermissionFlagSet} * The PermissionFlagSet describing the current state of granted * permissions for the user being edited. */ getPermissionFlags : function getPermissionFlags() { return $scope.permissionFlags; }, /** * Notifies the controller that a change has been made to the given * connection permission for the user being edited. This only applies * to READ permissions. * * @param {String} identifier * The identifier of the connection affected by the changed * permission. */ connectionPermissionChanged : function connectionPermissionChanged(identifier) { // Determine current permission setting var value = $scope.permissionFlags.connectionPermissions.READ[identifier]; // Add/remove permission depending on flag state if (value) addConnectionPermission(identifier); else removeConnectionPermission(identifier); }, /** * Notifies the controller that a change has been made to the given * connection group permission for the user being edited. This only * applies to READ permissions. * * @param {String} identifier * The identifier of the connection group affected by the changed * permission. */ connectionGroupPermissionChanged : function connectionGroupPermissionChanged(identifier) { // Determine current permission setting var value = $scope.permissionFlags.connectionGroupPermissions.READ[identifier]; // Add/remove permission depending on flag state if (value) addConnectionGroupPermission(identifier); else removeConnectionGroupPermission(identifier); } }; /** * Cancels all pending edits, returning to the management page. */ $scope.cancel = function cancel() { $location.path('/settings/users'); }; /** * Saves the user, updating the existing user only. */ $scope.saveUser = function saveUser() { // Verify passwords match if ($scope.passwordMatch !== $scope.user.password) { guacNotification.showStatus({ 'className' : 'error', 'title' : 'MANAGE_USER.DIALOG_HEADER_ERROR', 'text' : 'MANAGE_USER.ERROR_PASSWORD_MISMATCH', 'actions' : [ ACKNOWLEDGE_ACTION ] }); return; } // Save or create the user, depending on whether the user exists var saveUserPromise; if ($scope.userExists(selectedDataSource)) saveUserPromise = userService.saveUser(selectedDataSource, $scope.user); else saveUserPromise = userService.createUser(selectedDataSource, $scope.user); saveUserPromise.success(function savedUser() { // Upon success, save any changed permissions permissionService.patchPermissions(selectedDataSource, $scope.user.username, permissionsAdded, permissionsRemoved) .success(function patchedUserPermissions() { $location.path('/settings/users'); }) // Notify of any errors .error(function userPermissionsPatchFailed(error) { guacNotification.showStatus({ 'className' : 'error', 'title' : 'MANAGE_USER.DIALOG_HEADER_ERROR', 'text' : error.message, 'actions' : [ ACKNOWLEDGE_ACTION ] }); }); }) // Notify of any errors .error(function userSaveFailed(error) { guacNotification.showStatus({ 'className' : 'error', 'title' : 'MANAGE_USER.DIALOG_HEADER_ERROR', 'text' : error.message, 'actions' : [ ACKNOWLEDGE_ACTION ] }); }); }; /** * An action to be provided along with the object sent to showStatus which * immediately deletes the current user. */ var DELETE_ACTION = { name : "MANAGE_USER.ACTION_DELETE", className : "danger", // Handle action callback : function deleteCallback() { deleteUserImmediately(); guacNotification.showStatus(false); } }; /** * An action to be provided along with the object sent to showStatus which * closes the currently-shown status dialog. */ var CANCEL_ACTION = { name : "MANAGE_USER.ACTION_CANCEL", // Handle action callback : function cancelCallback() { guacNotification.showStatus(false); } }; /** * Immediately deletes the current user, without prompting the user for * confirmation. */ var deleteUserImmediately = function deleteUserImmediately() { // Delete the user userService.deleteUser(selectedDataSource, $scope.user) .success(function deletedUser() { $location.path('/settings/users'); }) // Notify of any errors .error(function userDeletionFailed(error) { guacNotification.showStatus({ 'className' : 'error', 'title' : 'MANAGE_USER.DIALOG_HEADER_ERROR', 'text' : error.message, 'actions' : [ ACKNOWLEDGE_ACTION ] }); }); }; /** * Deletes the user, prompting the user first to confirm that deletion is * desired. */ $scope.deleteUser = function deleteUser() { // Confirm deletion request guacNotification.showStatus({ 'title' : 'MANAGE_USER.DIALOG_HEADER_CONFIRM_DELETE', 'text' : 'MANAGE_USER.TEXT_CONFIRM_DELETE', 'actions' : [ DELETE_ACTION, CANCEL_ACTION] }); }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/types/HistoryEntryWrapper.js0100664 0001751 0001751 00000006150 12645510255 033222 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for defining the HistoryEntryWrapper class. */ angular.module('manage').factory('HistoryEntryWrapper', ['$injector', function defineHistoryEntryWrapper($injector) { // Required types var ConnectionHistoryEntry = $injector.get('ConnectionHistoryEntry'); /** * Wrapper for ConnectionHistoryEntry which adds display-specific * properties, such as the connection duration. * * @constructor * @param {ConnectionHistoryEntry} historyEntry * The history entry to wrap. */ var HistoryEntryWrapper = function HistoryEntryWrapper(historyEntry) { /** * The wrapped ConnectionHistoryEntry. * * @type ConnectionHistoryEntry */ this.entry = historyEntry; /** * An object providing value and unit properties, denoting the duration * and its corresponding units. * * @type ConnectionHistoryEntry.Duration */ this.duration = null; /** * The string to display as the duration of this history entry. If a * duration is available, its value and unit will be exposed to any * given translation string as the VALUE and UNIT substitution * variables respectively. * * @type String */ this.durationText = 'MANAGE_CONNECTION.TEXT_HISTORY_DURATION'; // Notify if connection is active right now if (historyEntry.active) this.durationText = 'MANAGE_CONNECTION.INFO_CONNECTION_ACTIVE_NOW'; // If connection is not active, inform user if end date is not known else if (!historyEntry.endDate) this.durationText = 'MANAGE_CONNECTION.INFO_CONNECTION_DURATION_UNKNOWN'; // Set the duration if the necessary information is present if (historyEntry.endDate && historyEntry.startDate) this.duration = new ConnectionHistoryEntry.Duration(historyEntry.endDate - historyEntry.startDate); }; return HistoryEntryWrapper; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/types/ManageableUser.js0100664 0001751 0001751 00000003646 12612226251 032052 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for defining the ManageableUser class. */ angular.module('manage').factory('ManageableUser', [function defineManageableUser() { /** * A pairing of an @link{User} with the identifier of its corresponding * data source. * * @constructor * @param {Object|ManageableUser} template */ var ManageableUser = function ManageableUser(template) { /** * The unique identifier of the data source containing this user. * * @type String */ this.dataSource = template.dataSource; /** * The @link{User} object represented by this ManageableUser and * contained within the associated data source. * * @type User */ this.user = template.user; }; return ManageableUser; }]); ././@LongLink0100644 0000000 0000000 00000000152 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/templates/locationChooserConnectionGroup.htmlguacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/templates/locationChooserConnectionGroup0100664 0001751 0001751 00000002402 12612226251 035573 0ustar00mjumpermjumper0000000 0000000 {{item.name}} guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/templates/manageConnection.html0100664 0001751 0001751 00000012065 12612226251 033624 0ustar00mjumpermjumper0000000 0000000

{{'MANAGE_CONNECTION.SECTION_HEADER_EDIT_CONNECTION' | translate}}

{{'MANAGE_CONNECTION.FIELD_HEADER_NAME' | translate}}
{{'MANAGE_CONNECTION.FIELD_HEADER_LOCATION' | translate}}
{{'MANAGE_CONNECTION.FIELD_HEADER_PROTOCOL' | translate}}

{{'MANAGE_CONNECTION.SECTION_HEADER_PARAMETERS' | translate}}

{{'MANAGE_CONNECTION.SECTION_HEADER_HISTORY' | translate}}

{{'MANAGE_CONNECTION.INFO_CONNECTION_NOT_USED' | translate}}

{{'MANAGE_CONNECTION.TABLE_HEADER_HISTORY_USERNAME' | translate}} {{'MANAGE_CONNECTION.TABLE_HEADER_HISTORY_START' | translate}} {{'MANAGE_CONNECTION.TABLE_HEADER_HISTORY_DURATION' | translate}}
{{wrapper.entry.username}} {{wrapper.entry.startDate | date:historyDateFormat}}
guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/templates/manageUser.html0100664 0001751 0001751 00000013017 12645510255 032447 0ustar00mjumpermjumper0000000 0000000

{{'MANAGE_USER.SECTION_HEADER_EDIT_USER' | translate}}

{{'MANAGE_USER.INFO_READ_ONLY' | translate}}

{{'MANAGE_USER.FIELD_HEADER_USERNAME' | translate}} {{user.username}}
{{'MANAGE_USER.FIELD_HEADER_PASSWORD' | translate}}
{{'MANAGE_USER.FIELD_HEADER_PASSWORD_AGAIN' | translate}}

{{'MANAGE_USER.SECTION_HEADER_PERMISSIONS' | translate}}

{{systemPermissionType.label | translate}}
{{'MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD' | translate}}

{{'MANAGE_USER.SECTION_HEADER_CONNECTIONS' | translate}}

././@LongLink0100644 0000000 0000000 00000000145 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/templates/connectionGroupPermission.htmlguacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/templates/connectionGroupPermission.html0100664 0001751 0001751 00000002703 12612226251 035577 0ustar00mjumpermjumper0000000 0000000
{{item.name}}
guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/templates/connectionPermission.html0100664 0001751 0001751 00000003160 12612226251 034560 0ustar00mjumpermjumper0000000 0000000
{{item.name}}
guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/templates/locationChooser.html0100664 0001751 0001751 00000003153 12612226251 033505 0ustar00mjumpermjumper0000000 0000000
{{chosenConnectionGroupName}}
guacamole-client-0.9.9/guacamole/src/main/webapp/app/manage/templates/manageConnectionGroup.html0100664 0001751 0001751 00000006444 12612226251 034645 0ustar00mjumpermjumper0000000 0000000

{{'MANAGE_CONNECTION_GROUP.SECTION_HEADER_EDIT_CONNECTION_GROUP' | translate}}

{{'MANAGE_CONNECTION_GROUP.FIELD_HEADER_NAME' | translate}}
{{'MANAGE_CONNECTION_GROUP.FIELD_HEADER_LOCATION' | translate}}
{{'MANAGE_CONNECTION_GROUP.FIELD_HEADER_TYPE' | translate}}
guacamole-client-0.9.9/guacamole/src/main/webapp/app/history/services/guacHistory.js0100664 0001751 0001751 00000006662 12612226251 032431 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for reading and manipulating the Guacamole connection history. */ angular.module('history').factory('guacHistory', ['HistoryEntry', function guacHistory(HistoryEntry) { var service = {}; // The parameter name for getting the history from local storage var GUAC_HISTORY_STORAGE_KEY = "GUAC_HISTORY"; /** * The number of entries to allow before removing old entries based on the * cutoff. */ var IDEAL_LENGTH = 6; /** * The top few recent connections, sorted in order of most recent access. * * @type HistoryEntry[] */ service.recentConnections = []; /** * Updates the thumbnail and access time of the history entry for the * connection with the given ID. * * @param {String} id * The ID of the connection whose history entry should be updated. * * @param {String} thumbnail * The URL of the thumbnail image to associate with the history entry. */ service.updateThumbnail = function(id, thumbnail) { var i; // Remove any existing entry for this connection for (i=0; i < service.recentConnections.length; i++) { if (service.recentConnections[i].id === id) { service.recentConnections.splice(i, 1); break; } } // Store new entry in history service.recentConnections.unshift(new HistoryEntry( id, thumbnail, new Date().getTime() )); // Truncate history to ideal length if (service.recentConnections.length > IDEAL_LENGTH) service.recentConnections.length = IDEAL_LENGTH; // Save updated history, ignore inability to use localStorage try { if (localStorage) localStorage.setItem(GUAC_HISTORY_STORAGE_KEY, JSON.stringify(service.recentConnections)); } catch (ignore) {} }; // Get stored connection history, ignore inability to use localStorage try { if (localStorage) { var storedHistory = JSON.parse(localStorage.getItem(GUAC_HISTORY_STORAGE_KEY) || "[]"); if (storedHistory instanceof Array) service.recentConnections = storedHistory; } } catch (ignore) {} return service; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/history/historyModule.js0100664 0001751 0001751 00000002275 12612226251 031150 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The module for code relating to connection history. */ angular.module('history', []); guacamole-client-0.9.9/guacamole/src/main/webapp/app/history/types/HistoryEntry.js0100664 0001751 0001751 00000003644 12612226251 032131 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the HistoryEntry class used by the guacHistory service. */ angular.module('history').factory('HistoryEntry', [function defineHistoryEntry() { /** * A single entry in the connection history. * * @constructor * @param {String} id The ID of the connection. * * @param {String} thumbnail * The URL of the thumbnail to use to represent the connection. */ var HistoryEntry = function HistoryEntry(id, thumbnail) { /** * The ID of the connection associated with this history entry, * including type prefix. */ this.id = id; /** * The thumbnail associated with the connection associated with this * history entry. */ this.thumbnail = thumbnail; }; return HistoryEntry; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/osk/directives/guacOsk.js0100664 0001751 0001751 00000010366 12612226251 031131 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which displays the Guacamole on-screen keyboard. */ angular.module('osk').directive('guacOsk', [function guacOsk() { return { restrict: 'E', replace: true, scope: { /** * The URL for the Guacamole on-screen keyboard layout to use. * * @type String */ layout : '=' }, templateUrl: 'app/osk/templates/guacOsk.html', controller: ['$scope', '$injector', '$element', function guacOsk($scope, $injector, $element) { // Required services var $http = $injector.get('$http'); var $rootScope = $injector.get('$rootScope'); var cacheService = $injector.get('cacheService'); /** * The current on-screen keyboard, if any. * * @type Guacamole.OnScreenKeyboard */ var keyboard = null; /** * The main containing element for the entire directive. * * @type Element */ var main = $element[0]; // Size keyboard to same size as main element $scope.keyboardResized = function keyboardResized() { // Resize keyboard, if defined if (keyboard) keyboard.resize(main.offsetWidth); }; // Set layout whenever URL changes $scope.$watch("layout", function setLayout(url) { // Remove current keyboard if (keyboard) { main.removeChild(keyboard.getElement()); keyboard = null; } // Load new keyboard if (url) { // Retrieve layout JSON $http({ cache : cacheService.languages, method : 'GET', url : url }) // Build OSK with retrieved layout .success(function layoutRetrieved(layout) { // Abort if the layout changed while we were waiting for a response if ($scope.layout !== url) return; // Add OSK element keyboard = new Guacamole.OnScreenKeyboard(layout); main.appendChild(keyboard.getElement()); // Init size keyboard.resize(main.offsetWidth); // Broadcast keydown for each key pressed keyboard.onkeydown = function(keysym) { $rootScope.$broadcast('guacSyntheticKeydown', keysym); }; // Broadcast keydown for each key released keyboard.onkeyup = function(keysym) { $rootScope.$broadcast('guacSyntheticKeyup', keysym); }; }); } }); // end layout scope watch }] }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/osk/oskModule.js0100664 0001751 0001751 00000002275 12612226251 027336 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Module for displaying the Guacamole on-screen keyboard. */ angular.module('osk', []); guacamole-client-0.9.9/guacamole/src/main/webapp/app/osk/styles/osk.css0100664 0001751 0001751 00000013613 12612226251 027665 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .osk { position: relative; } .guac-keyboard { display: inline-block; width: 100%; margin: 0; padding: 0; cursor: default; text-align: left; vertical-align: middle; } .guac-keyboard, .guac-keyboard * { overflow: hidden; white-space: nowrap; } .guac-keyboard .guac-keyboard-key-container { display: inline-block; margin: 0.05em; position: relative; } .guac-keyboard .guac-keyboard-key { position: absolute; left: 0; right: 0; top: 0; bottom: 0; background: #444; border: 0.125em solid #666; -moz-border-radius: 0.25em; -webkit-border-radius: 0.25em; -khtml-border-radius: 0.25em; border-radius: 0.25em; color: white; font-size: 40%; font-weight: lighter; text-align: center; white-space: pre; text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25), 1px -1px 0 rgba(0, 0, 0, 0.25), -1px 1px 0 rgba(0, 0, 0, 0.25), -1px -1px 0 rgba(0, 0, 0, 0.25); } .guac-keyboard .guac-keyboard-key:hover { cursor: pointer; } .guac-keyboard .guac-keyboard-key.highlight { background: #666; border-color: #666; } /* Align some keys to the left */ .guac-keyboard .guac-keyboard-key-caps, .guac-keyboard .guac-keyboard-key-enter, .guac-keyboard .guac-keyboard-key-tab, .guac-keyboard .guac-keyboard-key-lalt, .guac-keyboard .guac-keyboard-key-ralt, .guac-keyboard .guac-keyboard-key-alt-gr, .guac-keyboard .guac-keyboard-key-lctrl, .guac-keyboard .guac-keyboard-key-rctrl, .guac-keyboard .guac-keyboard-key-lshift, .guac-keyboard .guac-keyboard-key-rshift { text-align: left; padding-left: 0.75em; } /* Active shift */ .guac-keyboard.guac-keyboard-modifier-shift .guac-keyboard-key-rshift, .guac-keyboard.guac-keyboard-modifier-shift .guac-keyboard-key-lshift, /* Active ctrl */ .guac-keyboard.guac-keyboard-modifier-control .guac-keyboard-key-rctrl, .guac-keyboard.guac-keyboard-modifier-control .guac-keyboard-key-lctrl, /* Active alt */ .guac-keyboard.guac-keyboard-modifier-alt .guac-keyboard-key-ralt, .guac-keyboard.guac-keyboard-modifier-alt .guac-keyboard-key-lalt, /* Active alt-gr */ .guac-keyboard.guac-keyboard-modifier-alt-gr .guac-keyboard-key-alt-gr, /* Active caps */ .guac-keyboard.guac-keyboard-modifier-caps .guac-keyboard-key-caps, /* Active super */ .guac-keyboard.guac-keyboard-modifier-super .guac-keyboard-key-super { background: #882; border-color: #DD4; } .guac-keyboard .guac-keyboard-key.guac-keyboard-pressed { background: #822; border-color: #D44; } .guac-keyboard .guac-keyboard-group { line-height: 0; } .guac-keyboard .guac-keyboard-group.guac-keyboard-alpha, .guac-keyboard .guac-keyboard-group.guac-keyboard-movement { display: inline-block; text-align: center; vertical-align: top; } .guac-keyboard .guac-keyboard-group.guac-keyboard-main { /* IE10 */ display: -ms-flexbox; -ms-flex-align: stretch; -ms-flex-direction: row; /* Ancient Mozilla */ display: -moz-box; -moz-box-align: stretch; -moz-box-orient: horizontal; /* Ancient WebKit */ display: -webkit-box; -webkit-box-align: stretch; -webkit-box-orient: horizontal; /* Old WebKit */ display: -webkit-flex; -webkit-align-items: stretch; -webkit-flex-direction: row; /* W3C */ display: flex; align-items: stretch; flex-direction: row; } .guac-keyboard .guac-keyboard-group.guac-keyboard-movement { -ms-flex: 1 1 auto; -moz-box-flex: 1; -webkit-box-flex: 1; -webkit-flex: 1 1 auto; flex: 1 1 auto; } .guac-keyboard .guac-keyboard-gap { display: inline-block; } /* Hide keycaps requiring modifiers which are NOT currently active. */ .guac-keyboard:not(.guac-keyboard-modifier-caps) .guac-keyboard-cap.guac-keyboard-requires-caps, .guac-keyboard:not(.guac-keyboard-modifier-shift) .guac-keyboard-cap.guac-keyboard-requires-shift, .guac-keyboard:not(.guac-keyboard-modifier-alt-gr) .guac-keyboard-cap.guac-keyboard-requires-alt-gr, /* Hide keycaps NOT requiring modifiers which ARE currently active, where that modifier is used to determine which cap is displayed for the current key. */ .guac-keyboard.guac-keyboard-modifier-shift .guac-keyboard-key.guac-keyboard-uses-shift .guac-keyboard-cap:not(.guac-keyboard-requires-shift), .guac-keyboard.guac-keyboard-modifier-caps .guac-keyboard-key.guac-keyboard-uses-caps .guac-keyboard-cap:not(.guac-keyboard-requires-caps), .guac-keyboard.guac-keyboard-modifier-alt-gr .guac-keyboard-key.guac-keyboard-uses-alt-gr .guac-keyboard-cap:not(.guac-keyboard-requires-alt-gr) { display: none; } /* Fade out keys which do not use AltGr if AltGr is active */ .guac-keyboard.guac-keyboard-modifier-alt-gr .guac-keyboard-key:not(.guac-keyboard-uses-alt-gr):not(.guac-keyboard-key-alt-gr) { opacity: 0.5; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/osk/templates/guacOsk.html0100664 0001751 0001751 00000002331 12612226251 031307 0ustar00mjumpermjumper0000000 0000000
guacamole-client-0.9.9/guacamole/src/main/webapp/app/login/directives/login.js0100664 0001751 0001751 00000014322 12612226251 031155 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive for displaying an arbitrary login form. */ angular.module('login').directive('guacLogin', [function guacLogin() { // Login directive var directive = { restrict : 'E', replace : true, templateUrl : 'app/login/templates/login.html' }; // Login directive scope directive.scope = { /** * An optional instructional message to display within the login * dialog. * * @type String */ helpText : '=', /** * The login form or set of fields. This will be displayed to the user * to capture their credentials. * * @type Field[] */ form : '=', /** * A map of all field name/value pairs that have already been provided. * If not null, the user will be prompted to continue their login * attempt using only the fields which remain. */ values : '=' }; // Controller for login directive directive.controller = ['$scope', '$injector', function loginController($scope, $injector) { // Required types var Error = $injector.get('Error'); var Field = $injector.get('Field'); // Required services var $route = $injector.get('$route'); var authenticationService = $injector.get('authenticationService'); /** * A description of the error that occurred during login, if any. * * @type String */ $scope.loginError = null; /** * All form values entered by the user, as parameter name/value pairs. * * @type Object. */ $scope.enteredValues = {}; /** * All form fields which have not yet been filled by the user. * * @type Field[] */ $scope.remainingFields = []; /** * Returns whether a previous login attempt is continuing. * * @return {Boolean} * true if a previous login attempt is continuing, false otherwise. */ $scope.isContinuation = function isContinuation() { // The login is continuing if any parameter values are provided for (var name in $scope.values) return true; return false; }; // Ensure provided values are included within entered values, even if // they have no corresponding input fields $scope.$watch('values', function resetEnteredValues(values) { angular.extend($scope.enteredValues, values || {}); }); // Update field information when form is changed $scope.$watch('form', function resetRemainingFields(fields) { // If no fields are provided, then no fields remain if (!fields) { $scope.remainingFields = []; return; } // Filter provided fields against provided values $scope.remainingFields = fields.filter(function isRemaining(field) { return !(field.name in $scope.values); }); // Set default values for all unset fields angular.forEach($scope.remainingFields, function setDefault(field) { if (!$scope.enteredValues[field.name]) $scope.enteredValues[field.name] = ''; }); }); /** * Submits the currently-specified username and password to the * authentication service, redirecting to the main view if successful. */ $scope.login = function login() { // Start with cleared status $scope.loginError = null; // Attempt login once existing session is destroyed authenticationService.authenticate($scope.enteredValues) // Clear and reload upon success .then(function loginSuccessful() { $scope.enteredValues = {}; $route.reload(); }) // Reset upon failure ['catch'](function loginFailed(error) { // Clear out passwords if the credentials were rejected for any reason if (error.type !== Error.Type.INSUFFICIENT_CREDENTIALS) { // Flag generic error for invalid login if (error.type === Error.Type.INVALID_CREDENTIALS) $scope.loginError = 'LOGIN.ERROR_INVALID_LOGIN'; // Display error if anything else goes wrong else $scope.loginError = error.message; // Clear all visible password fields angular.forEach($scope.remainingFields, function clearEnteredValueIfPassword(field) { // Remove entered value only if field is a password field if (field.type === Field.Type.PASSWORD && field.name in $scope.enteredValues) $scope.enteredValues[field.name] = ''; }); } }); }; }]; return directive; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/login/loginModule.js0100664 0001751 0001751 00000002334 12612226251 030162 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The module for the login functionality. */ angular.module('login', [ 'element', 'form', 'navigation' ]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/login/styles/dialog.css0100664 0001751 0001751 00000005607 12612226251 030650 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .login-ui.error .login-dialog { animation-name: shake-head; animation-duration: 0.25s; animation-timing-function: linear; -webkit-animation-name: shake-head; -webkit-animation-duration: 0.25s; -webkit-animation-timing-function: linear; } .login-ui div.login-dialog-middle { width: 100%; display: table-cell; vertical-align: middle; text-align: center; } .login-ui div.login-dialog { animation: fadein 0.125s linear; -moz-animation: fadein 0.125s linear; -webkit-animation: fadein 0.125s linear; width: 100%; max-width: 3in; text-align: left; padding: 1em; border: 1px solid rgba(0, 0, 0, 0.25); box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.25); font-size: 1.25em; display: inline-block; } .login-ui .login-dialog h1 { margin-top: 0; margin-bottom: 0em; text-align: center; } .login-ui .login-dialog .buttons { text-align: right; margin: 0; margin-top: 1em; } .login-ui .login-dialog .login-fields { vertical-align: middle; } .login-ui .login-dialog th { text-shadow: 1px 1px white; } .login-ui .login-dialog .version { font-size: 1.25em; font-weight: bold; padding: 0.5em 0; text-transform: uppercase; text-align: center; } .login-ui .login-dialog .logo { display: block; margin: 0.5em auto; width: 3em; height: 3em; background-size: 3em 3em; -moz-background-size: 3em 3em; -webkit-background-size: 3em 3em; -khtml-background-size: 3em 3em; background-image: url("images/guac-tricolor.png"); } .login-ui.continuation div.login-dialog { border-right: none; border-left: none; box-shadow: none; max-width: 6in; } .login-ui.continuation .login-dialog .logo, .login-ui.continuation .login-dialog .version { display: none; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/login/styles/input.css0100664 0001751 0001751 00000003451 12612226251 030543 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .login-ui .login-dialog .login-fields input { display: block; border: 1px solid rgba(0, 0, 0, 0.25); -moz-border-radius: 0.2em; -webkit-border-radius: 0.2em; -khtml-border-radius: 0.2em; font-family: Carlito, FreeSans, Helvetica, Arial, sans-serif; border-radius: 0.2em; width: 100%; margin: 0; margin-bottom: 0.5em; padding: 0.5em 0.75em; max-width: none; } .login-ui .login-dialog .buttons input[type="submit"] { width: 100%; margin: 0; } .login-ui.continuation .login-dialog .buttons input[type="submit"] { width: auto; } .login-ui.initial .login-dialog input.continue-login, .login-ui.continuation .login-dialog input.login { display: none; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/login/styles/animation.css0100664 0001751 0001751 00000003340 12612226251 031360 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ @keyframes shake-head { 0% { margin-left: 0.25em; margin-right: -0.25em; } 25% { margin-left: -0.25em; margin-right: 0.25em; } 50% { margin-left: 0.25em; margin-right: -0.25em; } 75% { margin-left: -0.25em; margin-right: 0.25em; } 100% { margin-left: 0.00em; margin-right: 0.00em; } } @-webkit-keyframes shake-head { 0% { margin-left: 0.25em; margin-right: -0.25em; } 25% { margin-left: -0.25em; margin-right: 0.25em; } 50% { margin-left: 0.25em; margin-right: -0.25em; } 75% { margin-left: -0.25em; margin-right: 0.25em; } 100% { margin-left: 0.00em; margin-right: 0.00em; } } guacamole-client-0.9.9/guacamole/src/main/webapp/app/login/styles/login.css0100664 0001751 0001751 00000004313 12612226251 030512 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ div.login-ui { height: 100%; width: 100%; position: fixed; left: 0; top: 0; display: table; background: white; z-index: 20; } .login-ui p.login-error { display: none; } .login-ui.error p.login-error { display: block; position: fixed; left: 0; right: 0; top: 0; padding: 1em; margin: 0.2em; background: #FDD; border: 1px solid #964040; -moz-border-radius: 0.25em; -webkit-border-radius: 0.25em; -khtml-border-radius: 0.25em; text-align: center; color: #964040; } .login-ui .login-fields .form-field .password-field .toggle-password { display: none; } .login-ui .login-fields .labeled-field { display: block; position: relative; z-index: 1; } .login-ui .login-fields .labeled-field .field-header { display: block; position: absolute; left: 0; right: 0; overflow: hidden; z-index: -1; margin: 0.5em; font-size: 0.9em; opacity: 0.5; } .login-ui .login-fields .labeled-field.empty input { background: transparent; } .login-ui .login-fields .labeled-field input:focus { background: white; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/login/templates/login.html0100664 0001751 0001751 00000004500 12612226251 031337 0ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/locale/services/translationStringService.js0100664 0001751 0001751 00000003733 12612226251 034730 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service for manipulating translation strings and translation table * identifiers. */ angular.module('locale').factory('translationStringService', [function translationStringService() { var service = {}; /** * Given an arbitrary identifier, returns the corresponding translation * table identifier. Translation table identifiers are uppercase strings, * word components separated by single underscores. For example, the * string "Swap red/blue" would become "SWAP_RED_BLUE". * * @param {String} identifier * The identifier to transform into a translation table identifier. * * @returns {String} * The translation table identifier. */ service.canonicalize = function canonicalize(identifier) { return identifier.replace(/[^a-zA-Z0-9]+/g, '_').toUpperCase(); }; return service; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/locale/services/translationLoader.js0100664 0001751 0001751 00000013646 12645510255 033361 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service for loading translation definition files, conforming to the * angular-translate documentation for custom translation loaders: * * https://github.com/angular-translate/angular-translate/wiki/Asynchronous-loading#using-custom-loader-service */ angular.module('locale').factory('translationLoader', ['$injector', function translationLoader($injector) { // Required services var $http = $injector.get('$http'); var $q = $injector.get('$q'); var cacheService = $injector.get('cacheService'); var languageService = $injector.get('languageService'); /** * Satisfies a translation request for the given key by searching for the * translation files for each key in the given array, in order. The request * fails only if none of the files can be found. * * @param {Deferred} deferred * The Deferred object to resolve or reject depending on whether at * least one translation file can be successfully loaded. * * @param {String} requestedKey * The originally-requested language key. * * @param {String[]} remainingKeys * The keys of the languages to attempt to load, in order, where the * first key in this array is the language to try within this function * call. The first key in the array is not necessarily the originally- * requested language key. */ var satisfyTranslation = function satisfyTranslation(deferred, requestedKey, remainingKeys) { // Get current language key var currentKey = remainingKeys.shift(); // If no languages to try, "succeed" with an empty translation (force fallback) if (!currentKey) { deferred.resolve('{}'); return; } /** * Continues trying possible translation files until no possibilities * exist. * * @private */ var tryNextTranslation = function tryNextTranslation() { satisfyTranslation(deferred, requestedKey, remainingKeys); }; // Retrieve list of supported languages languageService.getLanguages() // Attempt to retrieve translation if language is supported .success(function retrievedLanguages(languages) { // Skip retrieval if language is not supported if (!(currentKey in languages)) { tryNextTranslation(); return; } // Attempt to retrieve language $http({ cache : cacheService.languages, method : 'GET', url : 'translations/' + encodeURIComponent(currentKey) + '.json' }) // Resolve promise if translation retrieved successfully .success(function translationFileRetrieved(translation) { deferred.resolve(translation); }) // Retry with remaining languages if translation file could not be // retrieved .error(tryNextTranslation); }) // Retry with remaining languages if translation does not exist .error(tryNextTranslation); }; /** * Given a valid language key, returns all possible legal variations of * that key. Currently, this will be the given key and the given key * without the country code. If the key has no country code, only the * given key will be included in the returned array. * * @param {String} key * The language key to generate variations of. * * @returns {String[]} * All possible variations of the given language key. */ var getKeyVariations = function getKeyVariations(key) { var underscore = key.indexOf('_'); // If no underscore, only one possibility if (underscore === -1) return [key]; // Otherwise, include the lack of country code as an option return [key, key.substr(0, underscore)]; }; /** * Custom loader function for angular-translate which loads the desired * language file dynamically via HTTP. If the language file cannot be * found, the fallback language is used instead. * * @param {Object} options * Arbitrary options, containing at least a "key" property which * contains the requested language key. * * @returns {Promise.} * A promise which resolves to the requested translation string object. */ return function loadTranslationFile(options) { var translation = $q.defer(); // Satisfy the translation request using possible variations of the given key satisfyTranslation(translation, options.key, getKeyVariations(options.key)); // Return promise which is resolved only after the translation file is loaded return translation.promise; }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/locale/localeModule.js0100664 0001751 0001751 00000002277 12612226251 030446 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Module for handling common localization-related tasks. */ angular.module('locale', []); guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/settingsModule.js0100664 0001751 0001751 00000002565 12612226251 031450 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The module for manipulation of general settings. This is distinct from the * "manage" module, which deals only with administrator-level system management. */ angular.module('settings', [ 'groupList', 'list', 'navigation', 'notification', 'rest' ]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/services/preferenceService.js0100664 0001751 0001751 00000013633 12612226251 033722 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for setting and retrieving browser-local preferences. Preferences * may be any JSON-serializable type. */ angular.module('settings').provider('preferenceService', function preferenceServiceProvider() { /** * Reference to the provider itself. * * @type preferenceServiceProvider */ var provider = this; /** * The storage key of Guacamole preferences within local storage. * * @type String */ var GUAC_PREFERENCES_STORAGE_KEY = "GUAC_PREFERENCES"; /** * All valid input method type names. * * @type Object. */ var inputMethods = { /** * No input method is used. Keyboard events are generated from a * physical keyboard. * * @constant * @type String */ NONE : 'none', /** * Keyboard events will be generated from the Guacamole on-screen * keyboard. * * @constant * @type String */ OSK : 'osk', /** * Keyboard events will be generated by inferring the keys necessary to * produce typed text from an IME (Input Method Editor) such as the * native on-screen keyboard of a mobile device. * * @constant * @type String */ TEXT : 'text' }; /** * Returns the key of the language currently in use within the browser. * This is not necessarily the user's desired language, but is rather the * language user by the browser's interface. * * @returns {String} * The key of the language currently in use within the browser. */ var getDefaultLanguageKey = function getDefaultLanguageKey() { // Pull browser language, falling back to US English var language = (navigator.languages && navigator.languages[0]) || navigator.language || navigator.browserLanguage || 'en'; // Convert to format used internally return language.replace(/-/g, '_'); }; /** * All currently-set preferences, as name/value pairs. Each property name * corresponds to the name of a preference. * * @type Object. */ this.preferences = { /** * Whether translation of touch to mouse events should emulate an * absolute pointer device, or a relative pointer device. * * @type Boolean */ emulateAbsoluteMouse : true, /** * The default input method. This may be any of the values defined * within preferenceService.inputMethods. * * @type String */ inputMethod : inputMethods.NONE, /** * The key of the desired display language. * * @type String */ language : getDefaultLanguageKey() }; // Get stored preferences, ignore inability to use localStorage try { if (localStorage) { var preferencesJSON = localStorage.getItem(GUAC_PREFERENCES_STORAGE_KEY); if (preferencesJSON) angular.extend(provider.preferences, JSON.parse(preferencesJSON)); } } catch (ignore) {} // Factory method required by provider this.$get = ['$injector', function preferenceServiceFactory($injector) { // Required services var $rootScope = $injector.get('$rootScope'); var $window = $injector.get('$window'); var service = {}; /** * All valid input method type names. * * @type Object. */ service.inputMethods = inputMethods; /** * All currently-set preferences, as name/value pairs. Each property name * corresponds to the name of a preference. * * @type Object. */ service.preferences = provider.preferences; /** * Persists the current values of all preferences, if possible. */ service.save = function save() { // Save updated preferences, ignore inability to use localStorage try { if (localStorage) localStorage.setItem(GUAC_PREFERENCES_STORAGE_KEY, JSON.stringify(service.preferences)); } catch (ignore) {} }; // Persist settings when window is unloaded $window.addEventListener('unload', service.save); // Persist settings upon navigation $rootScope.$on('$routeChangeSuccess', function handleNavigate() { service.save(); }); // Persist settings upon logout $rootScope.$on('guacLogout', function handleLogout() { service.save(); }); return service; }]; }); ././@LongLink0100644 0000000 0000000 00000000152 12645510255 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnectionHistory.jsguacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnectionHisto0100664 0001751 0001751 00000015702 12645510255 035662 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive for viewing connection history records. */ angular.module('settings').directive('guacSettingsConnectionHistory', [function guacSettingsConnectionHistory() { return { // Element only restrict: 'E', replace: true, scope: { }, templateUrl: 'app/settings/templates/settingsConnectionHistory.html', controller: ['$scope', '$injector', function settingsConnectionHistoryController($scope, $injector) { // Get required types var ConnectionHistoryEntryWrapper = $injector.get('ConnectionHistoryEntryWrapper'); var FilterToken = $injector.get('FilterToken'); var SortOrder = $injector.get('SortOrder'); // Get required services var $routeParams = $injector.get('$routeParams'); var $translate = $injector.get('$translate'); var historyService = $injector.get('historyService'); /** * The identifier of the currently-selected data source. * * @type String */ $scope.dataSource = $routeParams.dataSource; /** * All wrapped matching connection history entries, or null if these * entries have not yet been retrieved. * * @type ConnectionHistoryEntryWrapper[] */ $scope.historyEntryWrappers = null; /** * The search terms to use when filtering the history records. * * @type String */ $scope.searchString = ''; /** * The date format for use for start/end dates. * * @type String */ $scope.dateFormat = null; /** * SortOrder instance which stores the sort order of the history * records. * * @type SortOrder */ $scope.order = new SortOrder([ '-startDate', '-duration', 'username', 'connectionName' ]); // Get session date format $translate('SETTINGS_CONNECTION_HISTORY.FORMAT_DATE') .then(function dateFormatReceived(retrievedDateFormat) { // Store received date format $scope.dateFormat = retrievedDateFormat; }); /** * Returns true if the connection history records have been loaded, * indicating that information needed to render the page is fully * loaded. * * @returns {Boolean} * true if the history records have been loaded, false * otherwise. * */ $scope.isLoaded = function isLoaded() { return $scope.historyEntryWrappers !== null && $scope.dateFormat !== null; }; /** * Returns whether the search has completed but contains no history * records. This function will return false if there are history * records in the results OR if the search has not yet completed. * * @returns {Boolean} * true if the search results have been loaded but no history * records are present, false otherwise. */ $scope.isHistoryEmpty = function isHistoryEmpty() { return $scope.isLoaded() && $scope.historyEntryWrappers.length === 0; }; /** * Query the API for the connection record history, filtered by * searchString, and ordered by order. */ $scope.search = function search() { // Clear current results $scope.historyEntryWrappers = null; // Tokenize search string var tokens = FilterToken.tokenize($scope.searchString); // Transform tokens into list of required string contents var requiredContents = []; angular.forEach(tokens, function addRequiredContents(token) { // Transform depending on token type switch (token.type) { // For string literals, use parsed token value case 'LITERAL': requiredContents.push(token.value); // Ignore whitespace case 'WHITESPACE': break; // For all other token types, use the relevant portion // of the original search string default: requiredContents.push(token.consumed); } }); // Fetch history records historyService.getConnectionHistory( $scope.dataSource, requiredContents, $scope.order.predicate.filter(function isSupportedPredicate(predicate) { return predicate === 'startDate' || predicate === '-startDate'; }) ) .success(function historyRetrieved(historyEntries) { // Wrap all history entries for sake of display $scope.historyEntryWrappers = []; angular.forEach(historyEntries, function wrapHistoryEntry(historyEntry) { $scope.historyEntryWrappers.push(new ConnectionHistoryEntryWrapper(historyEntry)); }); }); }; // Initialize search results $scope.search(); }] }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js0100664 0001751 0001751 00000037337 12612226251 034777 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive for managing all active Guacamole sessions. */ angular.module('settings').directive('guacSettingsSessions', [function guacSettingsSessions() { return { // Element only restrict: 'E', replace: true, scope: { }, templateUrl: 'app/settings/templates/settingsSessions.html', controller: ['$scope', '$injector', function settingsSessionsController($scope, $injector) { // Required types var ActiveConnectionWrapper = $injector.get('ActiveConnectionWrapper'); var ConnectionGroup = $injector.get('ConnectionGroup'); var SortOrder = $injector.get('SortOrder'); // Required services var $filter = $injector.get('$filter'); var $translate = $injector.get('$translate'); var $q = $injector.get('$q'); var activeConnectionService = $injector.get('activeConnectionService'); var authenticationService = $injector.get('authenticationService'); var connectionGroupService = $injector.get('connectionGroupService'); var dataSourceService = $injector.get('dataSourceService'); var guacNotification = $injector.get('guacNotification'); /** * The identifiers of all data sources accessible by the current * user. * * @type String[] */ var dataSources = authenticationService.getAvailableDataSources(); /** * The ActiveConnectionWrappers of all active sessions accessible * by the current user, or null if the active sessions have not yet * been loaded. * * @type ActiveConnectionWrapper[] */ $scope.wrappers = null; /** * SortOrder instance which maintains the sort order of the visible * connection wrappers. * * @type SortOrder */ $scope.wrapperOrder = new SortOrder([ 'activeConnection.username', 'startDate', 'activeConnection.remoteHost', 'name' ]); /** * Array of all wrapper properties that are filterable. * * @type String[] */ $scope.filteredWrapperProperties = [ 'activeConnection.username', 'startDate', 'activeConnection.remoteHost', 'name' ]; /** * All active connections, if known, grouped by corresponding data * source identifier, or null if active connections have not yet * been loaded. * * @type Object.> */ var allActiveConnections = null; /** * Map of all visible connections by data source identifier and * object identifier, or null if visible connections have not yet * been loaded. * * @type Object.> */ var allConnections = null; /** * The date format for use for session-related dates. * * @type String */ var sessionDateFormat = null; /** * Map of all currently-selected active connection wrappers by * data source and identifier. * * @type Object.> */ var allSelectedWrappers = {}; /** * Adds the given connection to the internal set of visible * connections. * * @param {String} dataSource * The identifier of the data source associated with the given * connection. * * @param {Connection} connection * The connection to add to the internal set of visible * connections. */ var addConnection = function addConnection(dataSource, connection) { // Add given connection to set of visible connections allConnections[dataSource][connection.identifier] = connection; }; /** * Adds all descendant connections of the given connection group to * the internal set of connections. * * @param {String} dataSource * The identifier of the data source associated with the given * connection group. * * @param {ConnectionGroup} connectionGroup * The connection group whose descendant connections should be * added to the internal set of connections. */ var addDescendantConnections = function addDescendantConnections(dataSource, connectionGroup) { // Add all child connections angular.forEach(connectionGroup.childConnections, function addConnectionForDataSource(connection) { addConnection(dataSource, connection); }); // Add all child connection groups angular.forEach(connectionGroup.childConnectionGroups, function addConnectionGroupForDataSource(connectionGroup) { addDescendantConnections(dataSource, connectionGroup); }); }; /** * Wraps all loaded active connections, storing the resulting array * within the scope. If required data has not yet finished loading, * this function has no effect. */ var wrapAllActiveConnections = function wrapAllActiveConnections() { // Abort if not all required data is available if (!allActiveConnections || !allConnections || !sessionDateFormat) return; // Wrap all active connections for sake of display $scope.wrappers = []; angular.forEach(allActiveConnections, function wrapActiveConnections(activeConnections, dataSource) { angular.forEach(activeConnections, function wrapActiveConnection(activeConnection, identifier) { // Retrieve corresponding connection var connection = allConnections[dataSource][activeConnection.connectionIdentifier]; // Add wrapper $scope.wrappers.push(new ActiveConnectionWrapper({ dataSource : dataSource, name : connection.name, startDate : $filter('date')(activeConnection.startDate, sessionDateFormat), activeConnection : activeConnection })); }); }); }; // Retrieve all connections dataSourceService.apply( connectionGroupService.getConnectionGroupTree, dataSources, ConnectionGroup.ROOT_IDENTIFIER ) .then(function connectionGroupsReceived(rootGroups) { allConnections = {}; // Load connections from each received root group angular.forEach(rootGroups, function connectionGroupReceived(rootGroup, dataSource) { allConnections[dataSource] = {}; addDescendantConnections(dataSource, rootGroup); }); // Attempt to produce wrapped list of active connections wrapAllActiveConnections(); }); // Query active sessions dataSourceService.apply( activeConnectionService.getActiveConnections, dataSources ) .then(function sessionsRetrieved(retrievedActiveConnections) { // Store received map of active connections allActiveConnections = retrievedActiveConnections; // Attempt to produce wrapped list of active connections wrapAllActiveConnections(); }); // Get session date format $translate('SETTINGS_SESSIONS.FORMAT_STARTDATE').then(function sessionDateFormatReceived(retrievedSessionDateFormat) { // Store received date format sessionDateFormat = retrievedSessionDateFormat; // Attempt to produce wrapped list of active connections wrapAllActiveConnections(); }); /** * Returns whether critical data has completed being loaded. * * @returns {Boolean} * true if enough data has been loaded for the user interface * to be useful, false otherwise. */ $scope.isLoaded = function isLoaded() { return $scope.wrappers !== null; }; /** * An action to be provided along with the object sent to * showStatus which closes the currently-shown status dialog. */ var ACKNOWLEDGE_ACTION = { name : "SETTINGS_SESSIONS.ACTION_ACKNOWLEDGE", // Handle action callback : function acknowledgeCallback() { guacNotification.showStatus(false); } }; /** * An action to be provided along with the object sent to * showStatus which closes the currently-shown status dialog. */ var CANCEL_ACTION = { name : "SETTINGS_SESSIONS.ACTION_CANCEL", // Handle action callback : function cancelCallback() { guacNotification.showStatus(false); } }; /** * An action to be provided along with the object sent to * showStatus which immediately deletes the currently selected * sessions. */ var DELETE_ACTION = { name : "SETTINGS_SESSIONS.ACTION_DELETE", className : "danger", // Handle action callback : function deleteCallback() { deleteAllSessionsImmediately(); guacNotification.showStatus(false); } }; /** * Immediately deletes the selected sessions, without prompting the * user for confirmation. */ var deleteAllSessionsImmediately = function deleteAllSessionsImmediately() { var deletionRequests = []; // Perform deletion for each relevant data source angular.forEach(allSelectedWrappers, function deleteSessionsImmediately(selectedWrappers, dataSource) { // Delete sessions, if any are selected var identifiers = Object.keys(selectedWrappers); if (identifiers.length) deletionRequests.push(activeConnectionService.deleteActiveConnections(dataSource, identifiers)); }); // Update interface $q.all(deletionRequests) .then(function activeConnectionsDeleted() { // Remove deleted connections from wrapper array $scope.wrappers = $scope.wrappers.filter(function activeConnectionStillExists(wrapper) { return !(wrapper.activeConnection.identifier in (allSelectedWrappers[wrapper.dataSource] || {})); }); // Clear selection allSelectedWrappers = {}; }, // Notify of any errors function activeConnectionDeletionFailed(error) { guacNotification.showStatus({ 'className' : 'error', 'title' : 'SETTINGS_SESSIONS.DIALOG_HEADER_ERROR', 'text' : error.message, 'actions' : [ ACKNOWLEDGE_ACTION ] }); }); }; /** * Delete all selected sessions, prompting the user first to * confirm that deletion is desired. */ $scope.deleteSessions = function deleteSessions() { // Confirm deletion request guacNotification.showStatus({ 'title' : 'SETTINGS_SESSIONS.DIALOG_HEADER_CONFIRM_DELETE', 'text' : 'SETTINGS_SESSIONS.TEXT_CONFIRM_DELETE', 'actions' : [ DELETE_ACTION, CANCEL_ACTION] }); }; /** * Returns whether the selected sessions can be deleted. * * @returns {Boolean} * true if selected sessions can be deleted, false otherwise. */ $scope.canDeleteSessions = function canDeleteSessions() { // We can delete sessions if at least one is selected for (var dataSource in allSelectedWrappers) { for (var identifier in allSelectedWrappers[dataSource]) return true; } return false; }; /** * Called whenever an active connection wrapper changes selected * status. * * @param {ActiveConnectionWrapper} wrapper * The wrapper whose selected status has changed. */ $scope.wrapperSelectionChange = function wrapperSelectionChange(wrapper) { // Get selection map for associated data source, creating if necessary var selectedWrappers = allSelectedWrappers[wrapper.dataSource]; if (!selectedWrappers) selectedWrappers = allSelectedWrappers[wrapper.dataSource] = {}; // Add wrapper to map if selected if (wrapper.checked) selectedWrappers[wrapper.activeConnection.identifier] = wrapper; // Otherwise, remove wrapper from map else delete selectedWrappers[wrapper.activeConnection.identifier]; }; }] }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnections.js0100664 0001751 0001751 00000023277 12645510255 035457 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive for managing all connections and connection groups in the system. */ angular.module('settings').directive('guacSettingsConnections', [function guacSettingsConnections() { return { // Element only restrict: 'E', replace: true, scope: { }, templateUrl: 'app/settings/templates/settingsConnections.html', controller: ['$scope', '$injector', function settingsConnectionsController($scope, $injector) { // Required types var ConnectionGroup = $injector.get('ConnectionGroup'); var PermissionSet = $injector.get('PermissionSet'); // Required services var $location = $injector.get('$location'); var $routeParams = $injector.get('$routeParams'); var authenticationService = $injector.get('authenticationService'); var connectionGroupService = $injector.get('connectionGroupService'); var dataSourceService = $injector.get('dataSourceService'); var guacNotification = $injector.get('guacNotification'); var permissionService = $injector.get('permissionService'); /** * The identifier of the current user. * * @type String */ var currentUsername = authenticationService.getCurrentUsername(); /** * An action to be provided along with the object sent to * showStatus which closes the currently-shown status dialog. */ var ACKNOWLEDGE_ACTION = { name : "SETTINGS_CONNECTIONS.ACTION_ACKNOWLEDGE", // Handle action callback : function acknowledgeCallback() { guacNotification.showStatus(false); } }; /** * The identifier of the currently-selected data source. * * @type String */ $scope.dataSource = $routeParams.dataSource; /** * The root connection group of the connection group hierarchy. * * @type Object. */ $scope.rootGroups = null; /** * All permissions associated with the current user, or null if the * user's permissions have not yet been loaded. * * @type PermissionSet */ $scope.permissions = null; /** * Array of all connection properties that are filterable. * * @type String[] */ $scope.filteredConnectionProperties = [ 'name', 'protocol' ]; /** * Array of all connection group properties that are filterable. * * @type String[] */ $scope.filteredConnectionGroupProperties = [ 'name' ]; /** * Returns whether critical data has completed being loaded. * * @returns {Boolean} * true if enough data has been loaded for the user interface * to be useful, false otherwise. */ $scope.isLoaded = function isLoaded() { return $scope.rootGroup !== null && $scope.permissions !== null; }; /** * Returns whether the current user can create new connections * within the current data source. * * @return {Boolean} * true if the current user can create new connections within * the current data source, false otherwise. */ $scope.canCreateConnections = function canCreateConnections() { // Abort if permissions have not yet loaded if (!$scope.permissions) return false; // Can create connections if adminstrator or have explicit permission if (PermissionSet.hasSystemPermission($scope.permissions, PermissionSet.SystemPermissionType.ADMINISTER) || PermissionSet.hasSystemPermission($scope.permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION)) return true; // No data sources allow connection creation return false; }; /** * Returns whether the current user can create new connection * groups within the current data source. * * @return {Boolean} * true if the current user can create new connection groups * within the current data source, false otherwise. */ $scope.canCreateConnectionGroups = function canCreateConnectionGroups() { // Abort if permissions have not yet loaded if (!$scope.permissions) return false; // Can create connections groups if adminstrator or have explicit permission if (PermissionSet.hasSystemPermission($scope.permissions, PermissionSet.SystemPermissionType.ADMINISTER) || PermissionSet.hasSystemPermission($scope.permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP)) return true; // No data sources allow connection group creation return false; }; /** * Returns whether the current user can create new connections or * connection groups or make changes to existing connections or * connection groups within the current data source. The * connection management interface as a whole is useless if this * function returns false. * * @return {Boolean} * true if the current user can create new connections/groups * or make changes to existing connections/groups within the * current data source, false otherwise. */ $scope.canManageConnections = function canManageConnections() { // Abort if permissions have not yet loaded if (!$scope.permissions) return false; // Creating connections/groups counts as management if ($scope.canCreateConnections() || $scope.canCreateConnectionGroups()) return true; // Can manage connections if granted explicit update or delete if (PermissionSet.hasConnectionPermission($scope.permissions, PermissionSet.ObjectPermissionType.UPDATE) || PermissionSet.hasConnectionPermission($scope.permissions, PermissionSet.ObjectPermissionType.DELETE)) return true; // Can manage connections groups if granted explicit update or delete if (PermissionSet.hasConnectionGroupPermission($scope.permissions, PermissionSet.ObjectPermissionType.UPDATE) || PermissionSet.hasConnectionGroupPermission($scope.permissions, PermissionSet.ObjectPermissionType.DELETE)) return true; // No data sources allow management of connections or groups return false; }; // Retrieve current permissions permissionService.getPermissions($scope.dataSource, currentUsername) .success(function permissionsRetrieved(permissions) { // Store retrieved permissions $scope.permissions = permissions; // Ignore permission to update root group PermissionSet.removeConnectionGroupPermission($scope.permissions, PermissionSet.ObjectPermissionType.UPDATE, ConnectionGroup.ROOT_IDENTIFIER); // Return to home if there's nothing to do here if (!$scope.canManageConnections()) $location.path('/'); }); // Retrieve all connections for which we have UPDATE or DELETE permission dataSourceService.apply( connectionGroupService.getConnectionGroupTree, [$scope.dataSource], ConnectionGroup.ROOT_IDENTIFIER, [PermissionSet.ObjectPermissionType.UPDATE, PermissionSet.ObjectPermissionType.DELETE] ) .then(function connectionGroupsReceived(rootGroups) { $scope.rootGroups = rootGroups; }); }] }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js0100664 0001751 0001751 00000020024 12612226251 035413 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive for managing preferences local to the current user. */ angular.module('settings').directive('guacSettingsPreferences', [function guacSettingsPreferences() { return { // Element only restrict: 'E', replace: true, scope: {}, templateUrl: 'app/settings/templates/settingsPreferences.html', controller: ['$scope', '$injector', function settingsPreferencesController($scope, $injector) { // Get required types var PermissionSet = $injector.get('PermissionSet'); // Required services var $translate = $injector.get('$translate'); var authenticationService = $injector.get('authenticationService'); var guacNotification = $injector.get('guacNotification'); var languageService = $injector.get('languageService'); var permissionService = $injector.get('permissionService'); var preferenceService = $injector.get('preferenceService'); var userService = $injector.get('userService'); /** * An action to be provided along with the object sent to * showStatus which closes the currently-shown status dialog. */ var ACKNOWLEDGE_ACTION = { name : 'SETTINGS_PREFERENCES.ACTION_ACKNOWLEDGE', // Handle action callback : function acknowledgeCallback() { guacNotification.showStatus(false); } }; /** * The username of the current user. * * @type String */ var username = authenticationService.getCurrentUsername(); /** * The identifier of the data source which authenticated the * current user. * * @type String */ var dataSource = authenticationService.getDataSource(); /** * All currently-set preferences, or their defaults if not yet set. * * @type Object. */ $scope.preferences = preferenceService.preferences; /** * A map of all available language keys to their human-readable * names. * * @type Object. */ $scope.languages = null; /** * Switches the active display langugae to the chosen language. */ $scope.changeLanguage = function changeLanguage() { $translate.use($scope.preferences.language); }; /** * The new password for the user. * * @type String */ $scope.newPassword = null; /** * The password match for the user. The update password action will * fail if $scope.newPassword !== $scope.passwordMatch. * * @type String */ $scope.newPasswordMatch = null; /** * Whether the current user can change their own password, or null * if this is not yet known. * * @type Boolean */ $scope.canChangePassword = null; /** * Update the current user's password to the password currently set within * the password change dialog. */ $scope.updatePassword = function updatePassword() { // Verify passwords match if ($scope.newPasswordMatch !== $scope.newPassword) { guacNotification.showStatus({ className : 'error', title : 'SETTINGS_PREFERENCES.DIALOG_HEADER_ERROR', text : 'SETTINGS_PREFERENCES.ERROR_PASSWORD_MISMATCH', actions : [ ACKNOWLEDGE_ACTION ] }); return; } // Verify that the new password is not blank if (!$scope.newPassword) { guacNotification.showStatus({ className : 'error', title : 'SETTINGS_PREFERENCES.DIALOG_HEADER_ERROR', text : 'SETTINGS_PREFERENCES.ERROR_PASSWORD_BLANK', actions : [ ACKNOWLEDGE_ACTION ] }); return; } // Save the user with the new password userService.updateUserPassword(dataSource, username, $scope.oldPassword, $scope.newPassword) .success(function passwordUpdated() { // Clear the password fields $scope.oldPassword = null; $scope.newPassword = null; $scope.newPasswordMatch = null; // Indicate that the password has been changed guacNotification.showStatus({ text : 'SETTINGS_PREFERENCES.INFO_PASSWORD_CHANGED', actions : [ ACKNOWLEDGE_ACTION ] }); }) // Notify of any errors .error(function passwordUpdateFailed(error) { guacNotification.showStatus({ className : 'error', title : 'SETTINGS_PREFERENCES.DIALOG_HEADER_ERROR', 'text' : error.message, actions : [ ACKNOWLEDGE_ACTION ] }); }); }; // Retrieve defined languages languageService.getLanguages() .success(function languagesRetrieved(languages) { $scope.languages = languages; }); // Retrieve current permissions permissionService.getPermissions(dataSource, username) .success(function permissionsRetrieved(permissions) { // Add action for changing password if permission is granted $scope.canChangePassword = PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, username); }); /** * Returns whether critical data has completed being loaded. * * @returns {Boolean} * true if enough data has been loaded for the user interface to be * useful, false otherwise. */ $scope.isLoaded = function isLoaded() { return $scope.canChangePassword !== null && $scope.languages !== null; }; }] }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js0100664 0001751 0001751 00000024641 12645510255 034272 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive for managing all users in the system. */ angular.module('settings').directive('guacSettingsUsers', [function guacSettingsUsers() { return { // Element only restrict: 'E', replace: true, scope: { }, templateUrl: 'app/settings/templates/settingsUsers.html', controller: ['$scope', '$injector', function settingsUsersController($scope, $injector) { // Required types var ManageableUser = $injector.get('ManageableUser'); var PermissionSet = $injector.get('PermissionSet'); // Required services var $location = $injector.get('$location'); var authenticationService = $injector.get('authenticationService'); var dataSourceService = $injector.get('dataSourceService'); var guacNotification = $injector.get('guacNotification'); var permissionService = $injector.get('permissionService'); var userService = $injector.get('userService'); // Identifier of the current user var currentUsername = authenticationService.getCurrentUsername(); /** * An action to be provided along with the object sent to * showStatus which closes the currently-shown status dialog. */ var ACKNOWLEDGE_ACTION = { name : "SETTINGS_USERS.ACTION_ACKNOWLEDGE", // Handle action callback : function acknowledgeCallback() { guacNotification.showStatus(false); } }; /** * The identifiers of all data sources accessible by the current * user. * * @type String[] */ var dataSources = authenticationService.getAvailableDataSources(); /** * All visible users, along with their corresponding data sources. * * @type ManageableUser[] */ $scope.manageableUsers = null; /** * The name of the new user to create, if any, when user creation * is requested via newUser(). * * @type String */ $scope.newUsername = ""; /** * Map of data source identifiers to all permissions associated * with the current user within that data source, or null if the * user's permissions have not yet been loaded. * * @type Object. */ $scope.permissions = null; /** * Array of all user properties that are filterable. * * @type String[] */ $scope.filteredUserProperties = [ 'user.username' ]; /** * Returns whether critical data has completed being loaded. * * @returns {Boolean} * true if enough data has been loaded for the user interface * to be useful, false otherwise. */ $scope.isLoaded = function isLoaded() { return $scope.manageableUsers !== null && $scope.permissions !== null; }; /** * Returns the identifier of the data source that should be used by * default when creating a new user. * * @return {String} * The identifier of the data source that should be used by * default when creating a new user, or null if user creation * is not allowed. */ $scope.getDefaultDataSource = function getDefaultDataSource() { // Abort if permissions have not yet loaded if (!$scope.permissions) return null; // For each data source for (var dataSource in $scope.permissions) { // Retrieve corresponding permission set var permissionSet = $scope.permissions[dataSource]; // Can create users if adminstrator or have explicit permission if (PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.ADMINISTER) || PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.CREATE_USER)) return dataSource; } // No data sources allow user creation return null; }; /** * Returns whether the current user can create new users within at * least one data source. * * @return {Boolean} * true if the current user can create new users within at * least one data source, false otherwise. */ $scope.canCreateUsers = function canCreateUsers() { return $scope.getDefaultDataSource() !== null; }; /** * Returns whether the current user can create new users or make * changes to existing users within at least one data source. The * user management interface as a whole is useless if this function * returns false. * * @return {Boolean} * true if the current user can create new users or make * changes to existing users within at least one data source, * false otherwise. */ var canManageUsers = function canManageUsers() { // Abort if permissions have not yet loaded if (!$scope.permissions) return false; // Creating users counts as management if ($scope.canCreateUsers()) return true; // For each data source for (var dataSource in $scope.permissions) { // Retrieve corresponding permission set var permissionSet = $scope.permissions[dataSource]; // Can manage users if granted explicit update or delete if (PermissionSet.hasUserPermission(permissionSet, PermissionSet.ObjectPermissionType.UPDATE) || PermissionSet.hasUserPermission(permissionSet, PermissionSet.ObjectPermissionType.DELETE)) return true; } // No data sources allow management of users return false; }; // Retrieve current permissions dataSourceService.apply( permissionService.getPermissions, dataSources, currentUsername ) .then(function permissionsRetrieved(permissions) { // Store retrieved permissions $scope.permissions = permissions; // Return to home if there's nothing to do here if (!canManageUsers()) $location.path('/'); var userPromise; // If users can be created, list all readable users if ($scope.canCreateUsers()) userPromise = dataSourceService.apply(userService.getUsers, dataSources); // Otherwise, list only updateable/deletable users else userPromise = dataSourceService.apply(userService.getUsers, dataSources, [ PermissionSet.ObjectPermissionType.UPDATE, PermissionSet.ObjectPermissionType.DELETE ]); userPromise.then(function usersReceived(userArrays) { var addedUsers = {}; $scope.manageableUsers = []; // For each user in each data source angular.forEach(dataSources, function addUserList(dataSource) { angular.forEach(userArrays[dataSource], function addUser(user) { // Do not add the same user twice if (addedUsers[user.username]) return; // Link to default creation data source if we cannot manage this user if (!PermissionSet.hasSystemPermission(permissions[dataSource], PermissionSet.ObjectPermissionType.ADMINISTER) && !PermissionSet.hasUserPermission(permissions[dataSource], PermissionSet.ObjectPermissionType.UPDATE, user.username) && !PermissionSet.hasUserPermission(permissions[dataSource], PermissionSet.ObjectPermissionType.DELETE, user.username)) dataSource = $scope.getDefaultDataSource(); // Add user to overall list addedUsers[user.username] = user; $scope.manageableUsers.push(new ManageableUser ({ 'dataSource' : dataSource, 'user' : user })); }); }); }); }); }] }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/styles/mouse-mode.css0100664 0001751 0001751 00000003041 12612226251 032201 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .preferences .mouse-mode .choices { text-align: center; } .preferences .mouse-mode .choice { display: inline-block; } .preferences .mouse-mode .choice .figure { display: inline-block; vertical-align: middle; width: 75%; max-width: 320px; } .preferences .mouse-mode .figure img { display: block; width: 100%; max-width: 320px; margin: 1em auto; } .preferences .mouse-mode .caption { text-align: left; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/styles/settings.css0100664 0001751 0001751 00000004102 12645510255 031774 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .settings .header { margin-bottom: 0; } .settings table.properties th { text-align: left; font-weight: normal; padding-right: 1em; } .settings .action-buttons { text-align: center; margin: 1em 0; } .settings .toolbar { /* IE10 */ display: -ms-flexbox; -ms-flex-align: center; -ms-flex-direction: row; /* Ancient Mozilla */ display: -moz-box; -moz-box-align: center; -moz-box-orient: horizontal; /* Ancient WebKit */ display: -webkit-box; -webkit-box-align: center; -webkit-box-orient: horizontal; /* Old WebKit */ display: -webkit-flex; -webkit-align-items: center; -webkit-flex-direction: row; /* W3C */ display: flex; align-items: center; flex-direction: row; } .settings .toolbar .action-buttons { margin-right: 0.25em; } .settings .toolbar .filter { -ms-flex: 1 1 auto; -moz-box-flex: 1; -webkit-box-flex: 1; -webkit-flex: 1 1 auto; flex: 1 1 auto; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/styles/preferences.css0100664 0001751 0001751 00000002357 12612226251 032441 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .preferences .update-password .form, .preferences .language .form { padding-left: 0.5em; border-left: 3px solid rgba(0, 0, 0, 0.125); }guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/styles/sessions.css0100664 0001751 0001751 00000002470 12612226251 032002 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .settings table.session-list { width: 100%; } .settings table.session-list tr.session:hover { background: #CDA; } .settings table.session-list .select-session { min-width: 2em; text-align: center; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/styles/history.css0100664 0001751 0001751 00000003475 12645510255 031651 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .settings.connectionHistory .filter { /* IE10 */ display: -ms-flexbox; -ms-flex-align: stretch; -ms-flex-direction: row; /* Ancient Mozilla */ display: -moz-box; -moz-box-align: stretch; -moz-box-orient: horizontal; /* Ancient WebKit */ display: -webkit-box; -webkit-box-align: stretch; -webkit-box-orient: horizontal; /* Old WebKit */ display: -webkit-flex; -webkit-align-items: stretch; -webkit-flex-direction: row; /* W3C */ display: flex; align-items: stretch; flex-direction: row; } .settings.connectionHistory .filter .search-button { margin-top: 0; margin-bottom: 0; } .settings.connectionHistory .history-list { width: 100%; }guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/styles/buttons.css0100664 0001751 0001751 00000003544 12645510255 031643 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ a.button.add-user, a.button.add-connection, a.button.add-connection-group { font-size: 0.8em; padding-left: 1.8em; position: relative; } a.button.add-user::before, a.button.add-connection::before, a.button.add-connection-group::before { content: ' '; position: absolute; width: 1.8em; top: 0; bottom: 0; left: 0; background-repeat: no-repeat; background-size: 1em; background-position: 0.5em 0.45em; } a.button.add-user::before { background-image: url('images/action-icons/guac-user-add.png'); } a.button.add-connection::before { background-image: url('images/action-icons/guac-monitor-add.png'); } a.button.add-connection-group::before { background-image: url('images/action-icons/guac-group-add.png'); } guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/styles/input-method.css0100664 0001751 0001751 00000002264 12612226251 032552 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .preferences .input-method .caption { margin-left: 2em; margin-right: 2em; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/controllers/settingsController.js0100664 0001751 0001751 00000003714 12612226251 034711 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The controller for the general settings page. */ angular.module('manage').controller('settingsController', ['$scope', '$injector', function settingsController($scope, $injector) { // Required services var $routeParams = $injector.get('$routeParams'); var userPageService = $injector.get('userPageService'); /** * The array of settings pages available to the current user, or null if * not yet known. * * @type Page[] */ $scope.settingsPages = null; /** * The currently-selected settings tab. This may be 'users', 'connections', * or 'sessions'. * * @type String */ $scope.activeTab = $routeParams.tab; // Retrieve settings pages userPageService.getSettingsPages() .then(function settingsPagesRetrieved(pages) { $scope.settingsPages = pages; }); }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/types/ActiveConnectionWrapper.js0100664 0001751 0001751 00000005152 12612226251 034375 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for defining the ActiveConnectionWrapper class. */ angular.module('settings').factory('ActiveConnectionWrapper', [ function defineActiveConnectionWrapper() { /** * Wrapper for ActiveConnection which adds display-specific * properties, such as a checked option. * * @constructor * @param {ActiveConnectionWrapper|Object} template * The object whose properties should be copied within the new * ActiveConnectionWrapper. */ var ActiveConnectionWrapper = function ActiveConnectionWrapper(template) { /** * The identifier of the data source associated with the * ActiveConnection wrapped by this ActiveConnectionWrapper. * * @type String */ this.dataSource = template.dataSource; /** * The display name of this connection. * * @type String */ this.name = template.name; /** * The date and time this session began, pre-formatted for display. * * @type String */ this.startDate = template.startDate; /** * The wrapped ActiveConnection. * * @type ActiveConnection */ this.activeConnection = template.activeConnection; /** * A flag indicating that the active connection has been selected. * * @type Boolean */ this.checked = template.checked || false; }; return ActiveConnectionWrapper; }]);././@LongLink0100644 0000000 0000000 00000000145 12645510255 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/types/ConnectionHistoryEntryWrapper.jsguacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/types/ConnectionHistoryEntryWrapper.js0100664 0001751 0001751 00000010555 12645510255 035656 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for defining the ConnectionHistoryEntryWrapper class. */ angular.module('settings').factory('ConnectionHistoryEntryWrapper', ['$injector', function defineConnectionHistoryEntryWrapper($injector) { // Required types var ConnectionHistoryEntry = $injector.get('ConnectionHistoryEntry'); /** * Wrapper for ConnectionHistoryEntry which adds display-specific * properties, such as a duration. * * @constructor * @param {ConnectionHistoryEntry} historyEntry * The ConnectionHistoryEntry that should be wrapped. */ var ConnectionHistoryEntryWrapper = function ConnectionHistoryEntryWrapper(historyEntry) { /** * The identifier of the connection associated with this history entry. * * @type String */ this.connectionIdentifier = historyEntry.connectionIdentifier; /** * The name of the connection associated with this history entry. * * @type String */ this.connectionName = historyEntry.connectionName; /** * The username of the user associated with this particular usage of * the connection. * * @type String */ this.username = historyEntry.username; /** * The time that usage began, in seconds since 1970-01-01 00:00:00 UTC. * * @type Number */ this.startDate = historyEntry.startDate; /** * The time that usage ended, in seconds since 1970-01-01 00:00:00 UTC. * The absence of an endDate does NOT necessarily indicate that the * connection is still in use, particularly if the server was shutdown * or restarted before the history entry could be updated. To determine * whether a connection is still active, check the active property of * this history entry. * * @type Number */ this.endDate = historyEntry.endDate; /** * The total amount of time the connection associated with the wrapped * history record was open, in seconds. * * @type Number */ this.duration = this.endDate - this.startDate; /** * An object providing value and unit properties, denoting the duration * and its corresponding units. * * @type ConnectionHistoryEntry.Duration */ this.readableDuration = null; // Set the duration if the necessary information is present if (this.endDate && this.startDate) this.readableDuration = new ConnectionHistoryEntry.Duration(this.duration); /** * The string to display as the duration of this history entry. If a * duration is available, its value and unit will be exposed to any * given translation string as the VALUE and UNIT substitution * variables respectively. * * @type String */ this.readableDurationText = 'SETTINGS_CONNECTION_HISTORY.TEXT_HISTORY_DURATION'; // Inform user if end date is not known if (!this.endDate) this.readableDurationText = 'SETTINGS_CONNECTION_HISTORY.INFO_CONNECTION_DURATION_UNKNOWN'; }; return ConnectionHistoryEntryWrapper; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/templates/settings.html0100664 0001751 0001751 00000003563 12645510255 032635 0ustar00mjumpermjumper0000000 0000000

{{'SETTINGS.SECTION_HEADER_SETTINGS' | translate}}

././@LongLink0100644 0000000 0000000 00000000147 12645510255 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/templates/settingsConnectionHistory.htmlguacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/templates/settingsConnectionHistory.ht0100664 0001751 0001751 00000007352 12645510255 035706 0ustar00mjumpermjumper0000000 0000000

{{'SETTINGS_CONNECTION_HISTORY.HELP_CONNECTION_HISTORY' | translate}}

{{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_USERNAME' | translate}} {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_STARTDATE' | translate}} {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_DURATION' | translate}} {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_CONNECTION_NAME' | translate}}
{{historyEntryWrapper.username}} {{historyEntryWrapper.startDate | date : dateFormat}} {{historyEntryWrapper.connectionName}}

{{'SETTINGS_CONNECTION_HISTORY.INFO_NO_HISTORY' | translate}}

guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/templates/settingsConnections.html0100664 0001751 0001751 00000005254 12645510255 035037 0ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/templates/connection.html0100664 0001751 0001751 00000003467 12612226251 033131 0ustar00mjumpermjumper0000000 0000000
{{item.name}}
guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/templates/settingsSessions.html0100664 0001751 0001751 00000007204 12612226251 034352 0ustar00mjumpermjumper0000000 0000000

{{'SETTINGS_SESSIONS.HELP_SESSIONS' | translate}}

{{'SETTINGS_SESSIONS.TABLE_HEADER_SESSION_USERNAME' | translate}} {{'SETTINGS_SESSIONS.TABLE_HEADER_SESSION_STARTDATE' | translate}} {{'SETTINGS_SESSIONS.TABLE_HEADER_SESSION_REMOTEHOST' | translate}} {{'SETTINGS_SESSIONS.TABLE_HEADER_SESSION_CONNECTION_NAME' | translate}}
{{wrapper.activeConnection.username}} {{wrapper.startDate}} {{wrapper.activeConnection.remoteHost}} {{wrapper.name}}

{{'SETTINGS_SESSIONS.INFO_NO_SESSIONS' | translate}}

guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/templates/connectionGroup.html0100664 0001751 0001751 00000002444 12612226251 034140 0ustar00mjumpermjumper0000000 0000000 {{item.name}} guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/templates/settingsUsers.html0100664 0001751 0001751 00000005073 12645510255 033655 0ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/settings/templates/settingsPreferences.html0100664 0001751 0001751 00000014032 12612226251 035002 0ustar00mjumpermjumper0000000 0000000

{{'SETTINGS_PREFERENCES.HELP_LANGUAGE' | translate}}

{{'SETTINGS_PREFERENCES.FIELD_HEADER_LANGUAGE' | translate}}

{{'SETTINGS_PREFERENCES.SECTION_HEADER_UPDATE_PASSWORD' | translate}}

{{'SETTINGS_PREFERENCES.HELP_UPDATE_PASSWORD' | translate}}

{{'SETTINGS_PREFERENCES.FIELD_HEADER_PASSWORD_OLD' | translate}}
{{'SETTINGS_PREFERENCES.FIELD_HEADER_PASSWORD_NEW' | translate}}
{{'SETTINGS_PREFERENCES.FIELD_HEADER_PASSWORD_NEW_AGAIN' | translate}}

{{'SETTINGS_PREFERENCES.SECTION_HEADER_DEFAULT_INPUT_METHOD' | translate}}

{{'SETTINGS_PREFERENCES.HELP_DEFAULT_INPUT_METHOD' | translate}}

{{'SETTINGS_PREFERENCES.SECTION_HEADER_DEFAULT_MOUSE_MODE' | translate}}

{{'SETTINGS_PREFERENCES.HELP_DEFAULT_MOUSE_MODE' | translate}}

guacamole-client-0.9.9/guacamole/src/main/webapp/app/auth/service/authenticationService.js0100664 0001751 0001751 00000030216 12645510255 033543 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for authenticating a user against the REST API. * * This service broadcasts two events on $rootScope depending on the result of * authentication operations: 'guacLogin' if authentication was successful and * a new token was created, and 'guacLogout' if an existing token is being * destroyed or replaced. Both events will be passed the related token as their * sole parameter. * * If a login attempt results in an existing token being replaced, 'guacLogout' * will be broadcast first for the token being replaced, followed by * 'guacLogin' for the new token. * * Failed logins may also result in guacInsufficientCredentials or * guacInvalidCredentials events, if the provided credentials were rejected for * being insufficient or invalid respectively. Both events will be provided * the set of parameters originally given to authenticate() and the error that * rejected the credentials. The Error object provided will contain set of * expected credentials returned by the REST endpoint. This set of credentials * will be in the form of a Field array. */ angular.module('auth').factory('authenticationService', ['$injector', function authenticationService($injector) { // Required types var Error = $injector.get('Error'); // Required services var $cookieStore = $injector.get('$cookieStore'); var $http = $injector.get('$http'); var $q = $injector.get('$q'); var $rootScope = $injector.get('$rootScope'); var service = {}; /** * The unique identifier of the local cookie which stores the user's * current authentication token and username. * * @type String */ var AUTH_COOKIE_ID = "GUAC_AUTH"; /** * Makes a request to authenticate a user using the token REST API endpoint * and given arbitrary parameters, returning a promise that succeeds only * if the authentication operation was successful. The resulting * authentication data can be retrieved later via getCurrentToken() or * getCurrentUsername(). * * The provided parameters can be virtually any object, as each property * will be sent as an HTTP parameter in the authentication request. * Standard parameters include "username" for the user's username, * "password" for the user's associated password, and "token" for the * auth token to check/update. * * If a token is provided, it will be reused if possible. * * @param {Object} parameters * Arbitrary parameters to authenticate with. * * @returns {Promise} * A promise which succeeds only if the login operation was successful. */ service.authenticate = function authenticate(parameters) { var authenticationProcess = $q.defer(); /** * Stores the given authentication data within the browser and marks * the authentication process as completed. * * @param {Object} data * The authentication data returned by the token REST endpoint. */ var completeAuthentication = function completeAuthentication(data) { // Store auth data $cookieStore.put(AUTH_COOKIE_ID, { 'authToken' : data.authToken, 'username' : data.username, 'dataSource' : data.dataSource, 'availableDataSources' : data.availableDataSources }); // Process is complete authenticationProcess.resolve(); }; // Attempt authentication $http({ method: 'POST', url: 'api/tokens', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: $.param(parameters), }) // If authentication succeeds, handle received auth data .success(function authenticationSuccessful(data) { var currentToken = service.getCurrentToken(); // If a new token was received, ensure the old token is invalidated, // if any, and notify listeners of the new token if (data.authToken !== currentToken) { // If an old token existed, explicitly logout first if (currentToken) { service.logout() ['finally'](function logoutComplete() { completeAuthentication(data); $rootScope.$broadcast('guacLogin', data.authToken); }); } // Otherwise, simply complete authentication and notify of login else { completeAuthentication(data); $rootScope.$broadcast('guacLogin', data.authToken); } } // Otherwise, just finish the auth process else completeAuthentication(data); }) // If authentication fails, propogate failure to returned promise .error(function authenticationFailed(error) { // Ensure error object exists, even if the error response is not // coming from the authentication REST endpoint error = new Error(error); // Request credentials if provided credentials were invalid if (error.type === Error.Type.INVALID_CREDENTIALS) $rootScope.$broadcast('guacInvalidCredentials', parameters, error); // Request more credentials if provided credentials were not enough else if (error.type === Error.Type.INSUFFICIENT_CREDENTIALS) $rootScope.$broadcast('guacInsufficientCredentials', parameters, error); authenticationProcess.reject(error); }); return authenticationProcess.promise; }; /** * Makes a request to update the current auth token, if any, using the * token REST API endpoint. If the optional parameters object is provided, * its properties will be included as parameters in the update request. * This function returns a promise that succeeds only if the authentication * operation was successful. The resulting authentication data can be * retrieved later via getCurrentToken() or getCurrentUsername(). * * If there is no current auth token, this function behaves identically to * authenticate(), and makes a general authentication request. * * @param {Object} [parameters] * Arbitrary parameters to authenticate with, if any. * * @returns {Promise} * A promise which succeeds only if the login operation was successful. */ service.updateCurrentToken = function updateCurrentToken(parameters) { // HTTP parameters for the authentication request var httpParameters = {}; // Add token parameter if current token is known var token = service.getCurrentToken(); if (token) httpParameters.token = service.getCurrentToken(); // Add any additional parameters if (parameters) angular.extend(httpParameters, parameters); // Make the request return service.authenticate(httpParameters); }; /** * Makes a request to authenticate a user using the token REST API endpoint * with a username and password, ignoring any currently-stored token, * returning a promise that succeeds only if the login operation was * successful. The resulting authentication data can be retrieved later * via getCurrentToken() or getCurrentUsername(). * * @param {String} username * The username to log in with. * * @param {String} password * The password to log in with. * * @returns {Promise} * A promise which succeeds only if the login operation was successful. */ service.login = function login(username, password) { return service.authenticate({ username: username, password: password }); }; /** * Makes a request to logout a user using the login REST API endpoint, * returning a promise succeeds only if the logout operation was * successful. * * @returns {Promise} * A promise which succeeds only if the logout operation was * successful. */ service.logout = function logout() { // Clear authentication data var token = service.getCurrentToken(); $cookieStore.remove(AUTH_COOKIE_ID); // Notify listeners that a token is being destroyed $rootScope.$broadcast('guacLogout', token); // Delete old token return $http({ method: 'DELETE', url: 'api/tokens/' + token }); }; /** * Returns the username of the current user. If the current user is not * logged in, this value may not be valid. * * @returns {String} * The username of the current user, or null if no authentication data * is present. */ service.getCurrentUsername = function getCurrentUsername() { // Return username, if available var authData = $cookieStore.get(AUTH_COOKIE_ID); if (authData) return authData.username; // No auth data present return null; }; /** * Returns the auth token associated with the current user. If the current * user is not logged in, this token may not be valid. * * @returns {String} * The auth token associated with the current user, or null if no * authentication data is present. */ service.getCurrentToken = function getCurrentToken() { // Return auth token, if available var authData = $cookieStore.get(AUTH_COOKIE_ID); if (authData) return authData.authToken; // No auth data present return null; }; /** * Returns the identifier of the data source that authenticated the current * user. If the current user is not logged in, this value may not be valid. * * @returns {String} * The identifier of the data source that authenticated the current * user, or null if no authentication data is present. */ service.getDataSource = function getDataSource() { // Return data source, if available var authData = $cookieStore.get(AUTH_COOKIE_ID); if (authData) return authData.dataSource; // No auth data present return null; }; /** * Returns the identifiers of all data sources available to the current * user. If the current user is not logged in, this value may not be valid. * * @returns {String[]} * The identifiers of all data sources availble to the current user, * or an empty array if no authentication data is present. */ service.getAvailableDataSources = function getAvailableDataSources() { // Return data sources, if available var authData = $cookieStore.get(AUTH_COOKIE_ID); if (authData) return authData.availableDataSources; // No auth data present return []; }; return service; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/auth/authModule.js0100664 0001751 0001751 00000002311 12612226251 027637 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The module for authentication and management of tokens. */ angular.module('auth', ['ngCookies']); guacamole-client-0.9.9/guacamole/src/main/webapp/app/storage/services/sessionStorageFactory.js0100664 0001751 0001751 00000011131 12612226251 034416 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Factory for session-local storage. Creating session-local storage creates a * getter/setter with semantics tied to the user's session. If a user is logged * in, the storage is consistent. If the user logs out, the storage will not * persist new values, and attempts to retrieve the existing value will result * only in the default value. */ angular.module('storage').factory('sessionStorageFactory', ['$injector', function sessionStorageFactory($injector) { // Required services var $rootScope = $injector.get('$rootScope'); var authenticationService = $injector.get('authenticationService'); var service = {}; /** * Creates session-local storage that uses the provided default value or * getter to obtain new values as necessary. Beware that if the default is * an object, the resulting getter provide deep copies for new values. * * @param {Function|*} [template] * The default value for new users, or a getter which returns a newly- * created default value. * * @param {Function} [destructor] * Function which will be called just before the stored value is * destroyed on logout, if a value is stored. * * @returns {Function} * A getter/setter which returns or sets the current value of the new * session-local storage. Newly-set values will only persist of the * user is actually logged in. */ service.create = function create(template, destructor) { /** * Whether new values may be stored and retrieved. * * @type Boolean */ var enabled = !!authenticationService.getCurrentToken(); /** * Getter which returns the default value for this storage. * * @type Function */ var getter; // If getter provided, use that if (typeof template === 'function') getter = template; // Otherwise, always create a deep copy else getter = function getCopy() { return angular.copy(template); }; /** * The current value of this storage, or undefined if not yet set. */ var value = undefined; // Reset value and allow storage when the user is logged in $rootScope.$on('guacLogin', function userLoggedIn() { enabled = true; value = undefined; }); // Reset value and disallow storage when the user is logged out $rootScope.$on('guacLogout', function userLoggedOut() { // Call destructor before storage is teared down if (angular.isDefined(value) && destructor) destructor(value); // Destroy storage enabled = false; value = undefined; }); // Return getter/setter for value return function sessionLocalGetterSetter(newValue) { // Only actually store/retrieve values if enabled if (enabled) { // Set value if provided if (angular.isDefined(newValue)) value = newValue; // Obtain new value if unset if (!angular.isDefined(value)) value = getter(); // Return current value return value; } // Otherwise, just pretend to store/retrieve return angular.isDefined(newValue) ? newValue : getter(); }; }; return service; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/storage/storageModule.js0100664 0001751 0001751 00000002305 12612226251 031050 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Module which provides generic storage services. */ angular.module('storage', [ 'auth' ]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/touch/directives/guacTouchPinch.js0100664 0001751 0001751 00000016701 12612226251 032766 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which allows handling of pinch gestures (pinch-to-zoom, for * example) on a particular element. */ angular.module('touch').directive('guacTouchPinch', [function guacTouchPinch() { return { restrict: 'A', link: function linkGuacTouchPinch($scope, $element, $attrs) { /** * Called when a pinch gesture begins, changes, or ends. * * @event * @param {Boolean} inProgress * Whether the gesture is currently in progress. This will * always be true except when the gesture has ended, at which * point one final call will occur with this parameter set to * false. * * @param {Number} startLength * The initial distance between the two touches of the * pinch gesture, in pixels. * * @param {Number} currentLength * The current distance between the two touches of the * pinch gesture, in pixels. * * @param {Number} centerX * The current X coordinate of the center of the pinch gesture. * * @param {Number} centerY * The current Y coordinate of the center of the pinch gesture. * * @return {Boolean} * false if the default action of the touch event should be * prevented, any other value otherwise. */ var guacTouchPinch = $scope.$eval($attrs.guacTouchPinch); /** * The element which will register the pinch gesture. * * @type Element */ var element = $element[0]; /** * The starting pinch distance, or null if the gesture has not yet * started. * * @type Number */ var startLength = null; /** * The current pinch distance, or null if the gesture has not yet * started. * * @type Number */ var currentLength = null; /** * The X coordinate of the current center of the pinch gesture. * * @type Number */ var centerX = 0; /** * The Y coordinate of the current center of the pinch gesture. * @type Number */ var centerY = 0; /** * Given a touch event, calculates the distance between the first * two touches in pixels. * * @param {TouchEvent} e * The touch event to use when performing distance calculation. * * @return {Number} * The distance in pixels between the first two touches. */ var pinchDistance = function pinchDistance(e) { var touchA = e.touches[0]; var touchB = e.touches[1]; var deltaX = touchA.clientX - touchB.clientX; var deltaY = touchA.clientY - touchB.clientY; return Math.sqrt(deltaX*deltaX + deltaY*deltaY); }; /** * Given a touch event, calculates the center between the first two * touches in pixels, returning the X coordinate of this center. * * @param {TouchEvent} e * The touch event to use when performing center calculation. * * @return {Number} * The X coordinate of the center of the first two touches. */ var pinchCenterX = function pinchCenterX(e) { var touchA = e.touches[0]; var touchB = e.touches[1]; return (touchA.clientX + touchB.clientX) / 2; }; /** * Given a touch event, calculates the center between the first two * touches in pixels, returning the Y coordinate of this center. * * @param {TouchEvent} e * The touch event to use when performing center calculation. * * @return {Number} * The Y coordinate of the center of the first two touches. */ var pinchCenterY = function pinchCenterY(e) { var touchA = e.touches[0]; var touchB = e.touches[1]; return (touchA.clientY + touchB.clientY) / 2; }; // When there are exactly two touches, monitor the distance between // them, firing zoom events as appropriate element.addEventListener("touchmove", function pinchTouchMove(e) { if (e.touches.length === 2) { e.stopPropagation(); // Calculate current zoom level currentLength = pinchDistance(e); // Calculate center centerX = pinchCenterX(e); centerY = pinchCenterY(e); // Init start length if pinch is not in progress if (!startLength) startLength = currentLength; // Notify of pinch status if (guacTouchPinch) { $scope.$apply(function pinchChanged() { if (guacTouchPinch(true, startLength, currentLength, centerX, centerY) === false) e.preventDefault(); }); } } }, false); // Reset monitoring and fire end event when done element.addEventListener("touchend", function pinchTouchEnd(e) { if (startLength && e.touches.length < 2) { e.stopPropagation(); // Notify of pinch end if (guacTouchPinch) { $scope.$apply(function pinchComplete() { if (guacTouchPinch(false, startLength, currentLength, centerX, centerY) === false) e.preventDefault(); }); } startLength = null; } }, false); } }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/touch/directives/guacTouchDrag.js0100664 0001751 0001751 00000015063 12612226251 032602 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which allows handling of drag gestures on a particular element. */ angular.module('touch').directive('guacTouchDrag', [function guacTouchDrag() { return { restrict: 'A', link: function linkGuacTouchDrag($scope, $element, $attrs) { /** * Called during a drag gesture as the user's finger is placed upon * the element, moves, and is lifted from the element. * * @event * @param {Boolean} inProgress * Whether the gesture is currently in progress. This will * always be true except when the gesture has ended, at which * point one final call will occur with this parameter set to * false. * * @param {Number} startX * The X location at which the drag gesture began. * * @param {Number} startY * The Y location at which the drag gesture began. * * @param {Number} currentX * The current X location of the user's finger. * * @param {Number} currentY * The current Y location of the user's finger. * * @param {Number} deltaX * The difference in X location relative to the start of the * gesture. * * @param {Number} deltaY * The difference in Y location relative to the start of the * gesture. * * @return {Boolean} * false if the default action of the touch event should be * prevented, any other value otherwise. */ var guacTouchDrag = $scope.$eval($attrs.guacTouchDrag); /** * The element which will register the drag gesture. * * @type Element */ var element = $element[0]; /** * Whether a drag gesture is in progress. * * @type Boolean */ var inProgress = false; /** * The starting X location of the drag gesture. * * @type Number */ var startX = null; /** * The starting Y location of the drag gesture. * * @type Number */ var startY = null; /** * The current X location of the drag gesture. * * @type Number */ var currentX = null; /** * The current Y location of the drag gesture. * * @type Number */ var currentY = null; /** * The change in X relative to drag start. * * @type Number */ var deltaX = 0; /** * The change in X relative to drag start. * * @type Number */ var deltaY = 0; // When there is exactly one touch, monitor the change in location element.addEventListener("touchmove", function dragTouchMove(e) { if (e.touches.length === 1) { e.stopPropagation(); // Get touch location var x = e.touches[0].clientX; var y = e.touches[0].clientY; // Init start location and deltas if gesture is starting if (!startX || !startY) { startX = currentX = x; startY = currentY = y; deltaX = 0; deltaY = 0; inProgress = true; } // Update deltas if gesture is in progress else if (inProgress) { deltaX = x - currentX; deltaY = y - currentY; currentX = x; currentY = y; } // Signal start/change in drag gesture if (inProgress && guacTouchDrag) { $scope.$apply(function dragChanged() { if (guacTouchDrag(true, startX, startY, currentX, currentY, deltaX, deltaY) === false) e.preventDefault(); }); } } }, false); // Reset monitoring and fire end event when done element.addEventListener("touchend", function dragTouchEnd(e) { if (startX && startY && e.touches.length === 0) { e.stopPropagation(); // Signal end of drag gesture if (inProgress && guacTouchDrag) { $scope.$apply(function dragComplete() { if (guacTouchDrag(true, startX, startY, currentX, currentY, deltaX, deltaY === false)) e.preventDefault(); }); } startX = currentX = null; startY = currentY = null; deltaX = 0; deltaY = 0; inProgress = false; } }, false); } }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/touch/touchModule.js0100664 0001751 0001751 00000002321 12612226251 030202 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Module for handling common touch gestures, like panning or pinch-to-zoom. */ angular.module('touch', []); guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/services/historyService.js0100664 0001751 0001751 00000007236 12645510255 032432 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service for operating on history records via the REST API. */ angular.module('rest').factory('historyService', ['$injector', function historyService($injector) { // Required services var $http = $injector.get('$http'); var authenticationService = $injector.get('authenticationService'); var service = {}; /** * Makes a request to the REST API to get the usage history of all * accessible connections, returning a promise that provides the * corresponding array of @link{ConnectionHistoryEntry} objects if * successful. * * @param {String} dataSource * The unique identifier of the data source containing the connection * history records to be retrieved. This identifier corresponds to an * AuthenticationProvider within the Guacamole web application. * * @param {String[]} [requiredContents] * The set of arbitrary strings to filter with. A ConnectionHistoryEntry * must contain each of these values within the associated username, * connection name, start date, or end date to appear in the result. If * null, no filtering will be performed. * * @param {String[]} [sortPredicates] * The set of predicates to sort against. The resulting array of * ConnectionHistoryEntry objects will be sorted according to the * properties and sort orders defined by each predicate. If null, the * order of the resulting entries is undefined. Valid values are listed * within ConnectionHistoryEntry.SortPredicate. * * @returns {Promise.} * A promise which will resolve with an array of * @link{ConnectionHistoryEntry} objects upon success. */ service.getConnectionHistory = function getConnectionHistory(dataSource, requiredContents, sortPredicates) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Filter according to contents if restrictions are specified if (requiredContents) httpParameters.contains = requiredContents; // Sort according to provided predicates, if any if (sortPredicates) httpParameters.order = sortPredicates; // Retrieve connection history return $http({ method : 'GET', url : 'api/data/' + encodeURIComponent(dataSource) + '/history/connections', params : httpParameters }); }; return service; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/services/activeConnectionService.js0100664 0001751 0001751 00000015400 12612226251 034206 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service for operating on active connections via the REST API. */ angular.module('rest').factory('activeConnectionService', ['$injector', function activeConnectionService($injector) { // Required services var $http = $injector.get('$http'); var $q = $injector.get('$q'); var authenticationService = $injector.get('authenticationService'); var service = {}; /** * Makes a request to the REST API to get the list of active tunnels, * returning a promise that provides a map of @link{ActiveConnection} * objects if successful. * * @param {String[]} [permissionTypes] * The set of permissions to filter with. A user must have one or more * of these permissions for an active connection to appear in the * result. If null, no filtering will be performed. Valid values are * listed within PermissionSet.ObjectType. * * @returns {Promise.>} * A promise which will resolve with a map of @link{ActiveConnection} * objects, where each key is the identifier of the corresponding * active connection. */ service.getActiveConnections = function getActiveConnections(dataSource, permissionTypes) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Add permission filter if specified if (permissionTypes) httpParameters.permission = permissionTypes; // Retrieve tunnels return $http({ method : 'GET', url : 'api/data/' + encodeURIComponent(dataSource) + '/activeConnections', params : httpParameters }); }; /** * Returns a promise which resolves with all active connections accessible * by the current user, as a map of @link{ActiveConnection} maps, as would * be returned by getActiveConnections(), grouped by the identifier of * their corresponding data source. All given data sources are queried. If * an error occurs while retrieving any ActiveConnection map, the promise * will be rejected. * * @param {String[]} dataSources * The unique identifier of the data sources containing the active * connections to be retrieved. These identifiers correspond to * AuthenticationProviders within the Guacamole web application. * * @param {String[]} [permissionTypes] * The set of permissions to filter with. A user must have one or more * of these permissions for an active connection to appear in the * result. If null, no filtering will be performed. Valid values are * listed within PermissionSet.ObjectType. * * @returns {Promise.>>} * A promise which resolves with all active connections available to * the current user, as a map of ActiveConnection maps, as would be * returned by getActiveConnections(), grouped by the identifier of * their corresponding data source. */ service.getAllActiveConnections = function getAllActiveConnections(dataSources, permissionTypes) { var deferred = $q.defer(); var activeConnectionRequests = []; var activeConnectionMaps = {}; // Retrieve all active connections from all data sources angular.forEach(dataSources, function retrieveActiveConnections(dataSource) { activeConnectionRequests.push( service.getActiveConnections(dataSource, permissionTypes) .success(function activeConnectionsRetrieved(activeConnections) { activeConnectionMaps[dataSource] = activeConnections; }) ); }); // Resolve when all requests are completed $q.all(activeConnectionRequests) .then( // All requests completed successfully function allActiveConnectionsRetrieved() { deferred.resolve(userArrays); }, // At least one request failed function activeConnectionRetrievalFailed(e) { deferred.reject(e); } ); return deferred.promise; }; /** * Makes a request to the REST API to delete the active connections having * the given identifiers, effectively disconnecting them, returning a * promise that can be used for processing the results of the call. * * @param {String[]} identifiers * The identifiers of the active connections to delete. * * @returns {Promise} * A promise for the HTTP call which will succeed if and only if the * delete operation is successful. */ service.deleteActiveConnections = function deleteActiveConnections(dataSource, identifiers) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Convert provided array of identifiers to a patch var activeConnectionPatch = []; identifiers.forEach(function addActiveConnectionPatch(identifier) { activeConnectionPatch.push({ op : 'remove', path : '/' + identifier }); }); // Perform active connection deletion via PATCH return $http({ method : 'PATCH', url : 'api/data/' + encodeURIComponent(dataSource) + '/activeConnections', params : httpParameters, data : activeConnectionPatch }); }; return service; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/services/cacheService.js0100664 0001751 0001751 00000005027 12612226251 031762 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service which contains all REST API response caches. */ angular.module('rest').factory('cacheService', ['$injector', function cacheService($injector) { // Required services var $cacheFactory = $injector.get('$cacheFactory'); var $rootScope = $injector.get('$rootScope'); // Service containing all caches var service = {}; /** * Shared cache used by both connectionGroupService and * connectionService. * * @type $cacheFactory.Cache */ service.connections = $cacheFactory('API-CONNECTIONS'); /** * Cache used by languageService. * * @type $cacheFactory.Cache */ service.languages = $cacheFactory('API-LANGUAGES'); /** * Cache used by schemaService. * * @type $cacheFactory.Cache */ service.schema = $cacheFactory('API-SCHEMA'); /** * Shared cache used by both userService and permissionService. * * @type $cacheFactory.Cache */ service.users = $cacheFactory('API-USERS'); /** * Clear all caches defined in this service. */ service.clearCaches = function clearCaches() { service.connections.removeAll(); service.languages.removeAll(); service.schema.removeAll(); service.users.removeAll(); }; // Clear caches on logout $rootScope.$on('guacLogout', function handleLogout() { service.clearCaches(); }); return service; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/services/userService.js0100664 0001751 0001751 00000023055 12612226251 031676 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service for operating on users via the REST API. */ angular.module('rest').factory('userService', ['$injector', function userService($injector) { // Required services var $http = $injector.get('$http'); var $q = $injector.get('$q'); var authenticationService = $injector.get('authenticationService'); var cacheService = $injector.get('cacheService'); // Get required types var UserPasswordUpdate = $injector.get("UserPasswordUpdate"); var service = {}; /** * Makes a request to the REST API to get the list of users, * returning a promise that provides an array of @link{User} objects if * successful. * * @param {String} dataSource * The unique identifier of the data source containing the users to be * retrieved. This identifier corresponds to an AuthenticationProvider * within the Guacamole web application. * * @param {String[]} [permissionTypes] * The set of permissions to filter with. A user must have one or more * of these permissions for a user to appear in the result. * If null, no filtering will be performed. Valid values are listed * within PermissionSet.ObjectType. * * @returns {Promise.} * A promise which will resolve with an array of @link{User} objects * upon success. */ service.getUsers = function getUsers(dataSource, permissionTypes) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Add permission filter if specified if (permissionTypes) httpParameters.permission = permissionTypes; // Retrieve users return $http({ cache : cacheService.users, method : 'GET', url : 'api/data/' + encodeURIComponent(dataSource) + '/users', params : httpParameters }); }; /** * Makes a request to the REST API to get the user having the given * username, returning a promise that provides the corresponding * @link{User} if successful. * * @param {String} dataSource * The unique identifier of the data source containing the user to be * retrieved. This identifier corresponds to an AuthenticationProvider * within the Guacamole web application. * * @param {String} username * The username of the user to retrieve. * * @returns {Promise.} * A promise which will resolve with a @link{User} upon success. */ service.getUser = function getUser(dataSource, username) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Retrieve user return $http({ cache : cacheService.users, method : 'GET', url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username), params : httpParameters }); }; /** * Makes a request to the REST API to delete a user, returning a promise * that can be used for processing the results of the call. * * @param {String} dataSource * The unique identifier of the data source containing the user to be * deleted. This identifier corresponds to an AuthenticationProvider * within the Guacamole web application. * * @param {User} user * The user to delete. * * @returns {Promise} * A promise for the HTTP call which will succeed if and only if the * delete operation is successful. */ service.deleteUser = function deleteUser(dataSource, user) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Delete user return $http({ method : 'DELETE', url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(user.username), params : httpParameters }) // Clear the cache .success(function userDeleted(){ cacheService.users.removeAll(); }); }; /** * Makes a request to the REST API to create a user, returning a promise * that can be used for processing the results of the call. * * @param {String} dataSource * The unique identifier of the data source in which the user should be * created. This identifier corresponds to an AuthenticationProvider * within the Guacamole web application. * * @param {User} user * The user to create. * * @returns {Promise} * A promise for the HTTP call which will succeed if and only if the * create operation is successful. */ service.createUser = function createUser(dataSource, user) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Create user return $http({ method : 'POST', url : 'api/data/' + encodeURIComponent(dataSource) + '/users', params : httpParameters, data : user }) // Clear the cache .success(function userCreated(){ cacheService.users.removeAll(); }); }; /** * Makes a request to the REST API to save a user, returning a promise that * can be used for processing the results of the call. * * @param {String} dataSource * The unique identifier of the data source containing the user to be * updated. This identifier corresponds to an AuthenticationProvider * within the Guacamole web application. * * @param {User} user * The user to update. * * @returns {Promise} * A promise for the HTTP call which will succeed if and only if the * save operation is successful. */ service.saveUser = function saveUser(dataSource, user) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Update user return $http({ method : 'PUT', url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(user.username), params : httpParameters, data : user }) // Clear the cache .success(function userUpdated(){ cacheService.users.removeAll(); }); }; /** * Makes a request to the REST API to update the password for a user, * returning a promise that can be used for processing the results of the call. * * @param {String} dataSource * The unique identifier of the data source containing the user to be * updated. This identifier corresponds to an AuthenticationProvider * within the Guacamole web application. * * @param {String} username * The username of the user to update. * * @param {String} oldPassword * The exiting password of the user to update. * * @param {String} newPassword * The new password of the user to update. * * @returns {Promise} * A promise for the HTTP call which will succeed if and only if the * password update operation is successful. */ service.updateUserPassword = function updateUserPassword(dataSource, username, oldPassword, newPassword) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Update user password return $http({ method : 'PUT', url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username) + '/password', params : httpParameters, data : new UserPasswordUpdate({ oldPassword : oldPassword, newPassword : newPassword }) }) // Clear the cache .success(function passwordChanged(){ cacheService.users.removeAll(); }); }; return service; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/services/dataSourceService.js0100664 0001751 0001751 00000011154 12612226251 033007 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service which contains all REST API response caches. */ angular.module('rest').factory('dataSourceService', ['$injector', function dataSourceService($injector) { // Required services var $q = $injector.get('$q'); // Service containing all caches var service = {}; /** * Invokes the given function once for each of the given data sources, * passing that data source as the first argument to each invocation, * followed by any additional arguments passed to apply(). The results of * each invocation are aggregated into a map by data source identifier, * and handled through a single promise which is resolved or rejected * depending on the success/failure of each resulting REST call. Any error * results in rejection of the entire apply() operation, except 404 ("NOT * FOUND") errors, which are ignored. * * @param {Function} fn * The function to call for each of the given data sources. The data * source identifier will be given as the first argument, followed by * the rest of the arguments given to apply(), in order. The function * must return a Promise which is resolved or rejected depending on the * result of the REST call. * * @param {String[]} dataSources * The array or data source identifiers against which the given * function should be called. * * @param {...*} args * Any additional arguments to pass to the given function each time it * is called. * * @returns {Promise.>} * A Promise which resolves with a map of data source identifier to * corresponding result. The result will be the exact object or value * provided as the resolution to the Promise returned by calls to the * given function. */ service.apply = function apply(fn, dataSources) { var deferred = $q.defer(); var requests = []; var results = {}; // Build array of arguments to pass to the given function var args = []; for (var i = 2; i < arguments.length; i++) args.push(arguments[i]); // Retrieve the root group from all data sources angular.forEach(dataSources, function invokeAgainstDataSource(dataSource) { // Add promise to list of pending requests var deferredRequest = $q.defer(); requests.push(deferredRequest.promise); // Retrieve root group from data source fn.apply(this, [dataSource].concat(args)) // Store result on success .then(function immediateRequestSucceeded(response) { results[dataSource] = response.data; deferredRequest.resolve(); }, // Fail on any errors (except "NOT FOUND") function immediateRequestFailed(response) { // Ignore "NOT FOUND" errors if (response.status === 404) deferredRequest.resolve(); // Explicitly abort for all other errors else deferredRequest.reject(response); }); }); // Resolve if all requests succeed $q.all(requests).then(function requestsSucceeded() { deferred.resolve(results); }, // Reject if at least one request fails function requestFailed(response) { deferred.reject(response); }); return deferred.promise; }; return service; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/services/connectionService.js0100664 0001751 0001751 00000017013 12645510255 033062 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service for operating on connections via the REST API. */ angular.module('rest').factory('connectionService', ['$injector', function connectionService($injector) { // Required services var $http = $injector.get('$http'); var authenticationService = $injector.get('authenticationService'); var cacheService = $injector.get('cacheService'); var service = {}; /** * Makes a request to the REST API to get a single connection, returning a * promise that provides the corresponding @link{Connection} if successful. * * @param {String} id The ID of the connection. * * @returns {Promise.} * A promise which will resolve with a @link{Connection} upon success. * * @example * * connectionService.getConnection('myConnection').success(function(connection) { * // Do something with the connection * }); */ service.getConnection = function getConnection(dataSource, id) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Retrieve connection return $http({ cache : cacheService.connections, method : 'GET', url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(id), params : httpParameters }); }; /** * Makes a request to the REST API to get the usage history of a single * connection, returning a promise that provides the corresponding * array of @link{ConnectionHistoryEntry} objects if successful. * * @param {String} id * The identifier of the connection. * * @returns {Promise.} * A promise which will resolve with an array of * @link{ConnectionHistoryEntry} objects upon success. */ service.getConnectionHistory = function getConnectionHistory(dataSource, id) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Retrieve connection history return $http({ method : 'GET', url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(id) + '/history', params : httpParameters }); }; /** * Makes a request to the REST API to get the parameters of a single * connection, returning a promise that provides the corresponding * map of parameter name/value pairs if successful. * * @param {String} id * The identifier of the connection. * * @returns {Promise.>} * A promise which will resolve with an map of parameter name/value * pairs upon success. */ service.getConnectionParameters = function getConnectionParameters(dataSource, id) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Retrieve connection parameters return $http({ cache : cacheService.connections, method : 'GET', url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(id) + '/parameters', params : httpParameters }); }; /** * Makes a request to the REST API to save a connection, returning a * promise that can be used for processing the results of the call. If the * connection is new, and thus does not yet have an associated identifier, * the identifier will be automatically set in the provided connection * upon success. * * @param {Connection} connection The connection to update. * * @returns {Promise} * A promise for the HTTP call which will succeed if and only if the * save operation is successful. */ service.saveConnection = function saveConnection(dataSource, connection) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // If connection is new, add it and set the identifier automatically if (!connection.identifier) { return $http({ method : 'POST', url : 'api/data/' + encodeURIComponent(dataSource) + '/connections', params : httpParameters, data : connection }) // Set the identifier on the new connection and clear the cache .success(function connectionCreated(newConnection){ connection.identifier = newConnection.identifier; cacheService.connections.removeAll(); }); } // Otherwise, update the existing connection else { return $http({ method : 'PUT', url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(connection.identifier), params : httpParameters, data : connection }) // Clear the cache .success(function connectionUpdated(){ cacheService.connections.removeAll(); }); } }; /** * Makes a request to the REST API to delete a connection, * returning a promise that can be used for processing the results of the call. * * @param {Connection} connection The connection to delete. * * @returns {Promise} * A promise for the HTTP call which will succeed if and only if the * delete operation is successful. */ service.deleteConnection = function deleteConnection(dataSource, connection) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Delete connection return $http({ method : 'DELETE', url : 'api/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(connection.identifier), params : httpParameters }) // Clear the cache .success(function connectionDeleted(){ cacheService.connections.removeAll(); }); }; return service; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/services/schemaService.js0100664 0001751 0001751 00000015020 12612226251 032151 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service for operating on metadata via the REST API. */ angular.module('rest').factory('schemaService', ['$injector', function schemaService($injector) { // Required services var $http = $injector.get('$http'); var authenticationService = $injector.get('authenticationService'); var cacheService = $injector.get('cacheService'); var service = {}; /** * Makes a request to the REST API to get the list of available attributes * for user objects, returning a promise that provides an array of * @link{Form} objects if successful. Each element of the array describes * a logical grouping of possible attributes. * * @param {String} dataSource * The unique identifier of the data source containing the users whose * available attributes are to be retrieved. This identifier * corresponds to an AuthenticationProvider within the Guacamole web * application. * * @returns {Promise.} * A promise which will resolve with an array of @link{Form} * objects, where each @link{Form} describes a logical grouping of * possible attributes. */ service.getUserAttributes = function getUserAttributes(dataSource) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Retrieve available user attributes return $http({ cache : cacheService.schema, method : 'GET', url : 'api/schema/' + encodeURIComponent(dataSource) + '/users/attributes', params : httpParameters }); }; /** * Makes a request to the REST API to get the list of available attributes * for connection objects, returning a promise that provides an array of * @link{Form} objects if successful. Each element of the array describes * a logical grouping of possible attributes. * * @param {String} dataSource * The unique identifier of the data source containing the connections * whose available attributes are to be retrieved. This identifier * corresponds to an AuthenticationProvider within the Guacamole web * application. * * @returns {Promise.} * A promise which will resolve with an array of @link{Form} * objects, where each @link{Form} describes a logical grouping of * possible attributes. */ service.getConnectionAttributes = function getConnectionAttributes(dataSource) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Retrieve available connection attributes return $http({ cache : cacheService.schema, method : 'GET', url : 'api/schema/' + encodeURIComponent(dataSource) + '/connections/attributes', params : httpParameters }); }; /** * Makes a request to the REST API to get the list of available attributes * for connection group objects, returning a promise that provides an array * of @link{Form} objects if successful. Each element of the array * a logical grouping of possible attributes. * * @param {String} dataSource * The unique identifier of the data source containing the connection * groups whose available attributes are to be retrieved. This * identifier corresponds to an AuthenticationProvider within the * Guacamole web application. * * @returns {Promise.} * A promise which will resolve with an array of @link{Form} * objects, where each @link{Form} describes a logical grouping of * possible attributes. */ service.getConnectionGroupAttributes = function getConnectionGroupAttributes(dataSource) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Retrieve available connection group attributes return $http({ cache : cacheService.schema, method : 'GET', url : 'api/schema/' + encodeURIComponent(dataSource) + '/connectionGroups/attributes', params : httpParameters }); }; /** * Makes a request to the REST API to get the list of protocols, returning * a promise that provides a map of @link{Protocol} objects by protocol * name if successful. * * @param {String} dataSource * The unique identifier of the data source defining available * protocols. This identifier corresponds to an AuthenticationProvider * within the Guacamole web application. * * @returns {Promise.>} * A promise which will resolve with a map of @link{Protocol} * objects by protocol name upon success. */ service.getProtocols = function getProtocols(dataSource) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Retrieve available protocols return $http({ cache : cacheService.schema, method : 'GET', url : 'api/schema/' + encodeURIComponent(dataSource) + '/protocols', params : httpParameters }); }; return service; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/services/permissionService.js0100664 0001751 0001751 00000023553 12612226251 033113 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service for operating on user permissions via the REST API. */ angular.module('rest').factory('permissionService', ['$injector', function permissionService($injector) { // Required services var $http = $injector.get('$http'); var $q = $injector.get('$q'); var authenticationService = $injector.get('authenticationService'); var cacheService = $injector.get('cacheService'); // Required types var PermissionPatch = $injector.get('PermissionPatch'); var service = {}; /** * Makes a request to the REST API to get the list of permissions for a * given user, returning a promise that provides an array of * @link{Permission} objects if successful. * * @param {String} dataSource * The unique identifier of the data source containing the user whose * permissions should be retrieved. This identifier corresponds to an * AuthenticationProvider within the Guacamole web application. * * @param {String} userID * The ID of the user to retrieve the permissions for. * * @returns {Promise.} * A promise which will resolve with a @link{PermissionSet} upon * success. */ service.getPermissions = function getPermissions(dataSource, userID) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Retrieve user permissions return $http({ cache : cacheService.users, method : 'GET', url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(userID) + '/permissions', params : httpParameters }); }; /** * Makes a request to the REST API to add permissions for a given user, * returning a promise that can be used for processing the results of the * call. * * @param {String} dataSource * The unique identifier of the data source containing the user whose * permissions should be modified. This identifier corresponds to an * AuthenticationProvider within the Guacamole web application. * * @param {String} userID * The ID of the user to modify the permissions of. * * @param {PermissionSet} permissions * The set of permissions to add. * * @returns {Promise} * A promise for the HTTP call which will succeed if and only if the * add operation is successful. */ service.addPermissions = function addPermissions(dataSource, userID, permissions) { return service.patchPermissions(dataSource, userID, permissions, null); }; /** * Makes a request to the REST API to remove permissions for a given user, * returning a promise that can be used for processing the results of the * call. * * @param {String} dataSource * The unique identifier of the data source containing the user whose * permissions should be modified. This identifier corresponds to an * AuthenticationProvider within the Guacamole web application. * * @param {String} userID * The ID of the user to modify the permissions of. * * @param {PermissionSet} permissions * The set of permissions to remove. * * @returns {Promise} * A promise for the HTTP call which will succeed if and only if the * remove operation is successful. */ service.removePermissions = function removePermissions(dataSource, userID, permissions) { return service.patchPermissions(dataSource, userID, null, permissions); }; /** * Adds patches for modifying the permissions associated with specific * objects to the given array of patches. * * @param {PermissionPatch[]} patch * The array of patches to add new patches to. * * @param {String} operation * The operation to specify within each of the patches. Valid values * for this are defined within PermissionPatch.Operation. * * @param {String} path * The path of the permissions being patched. The path is a JSON path * describing the position of the permissions within a PermissionSet. * * @param {Object.} permissions * A map of object identifiers to arrays of permission type strings, * where each type string is a value from * PermissionSet.ObjectPermissionType. */ var addObjectPatchOperations = function addObjectPatchOperations(patch, operation, path, permissions) { // Add object permission operations to patch for (var identifier in permissions) { permissions[identifier].forEach(function addObjectPatch(type) { patch.push({ op : operation, path : path + "/" + identifier, value : type }); }); } }; /** * Adds patches for modifying any permission that can be stored within a * @link{PermissionSet}. * * @param {PermissionPatch[]} patch * The array of patches to add new patches to. * * @param {String} operation * The operation to specify within each of the patches. Valid values * for this are defined within PermissionPatch.Operation. * * @param {PermissionSet} permissions * The set of permissions for which patches should be added. */ var addPatchOperations = function addPatchOperations(patch, operation, permissions) { // Add connection permission operations to patch addObjectPatchOperations(patch, operation, "/connectionPermissions", permissions.connectionPermissions); // Add connection group permission operations to patch addObjectPatchOperations(patch, operation, "/connectionGroupPermissions", permissions.connectionGroupPermissions); // Add active connection permission operations to patch addObjectPatchOperations(patch, operation, "/activeConnectionPermissions", permissions.activeConnectionPermissions); // Add user permission operations to patch addObjectPatchOperations(patch, operation, "/userPermissions", permissions.userPermissions); // Add system operations to patch permissions.systemPermissions.forEach(function addSystemPatch(type) { patch.push({ op : operation, path : "/systemPermissions", value : type }); }); }; /** * Makes a request to the REST API to modify the permissions for a given * user, returning a promise that can be used for processing the results of * the call. * * @param {String} dataSource * The unique identifier of the data source containing the user whose * permissions should be modified. This identifier corresponds to an * AuthenticationProvider within the Guacamole web application. * * @param {String} userID * The ID of the user to modify the permissions of. * * @param {PermissionSet} [permissionsToAdd] * The set of permissions to add, if any. * * @param {PermissionSet} [permissionsToRemove] * The set of permissions to remove, if any. * * @returns {Promise} * A promise for the HTTP call which will succeed if and only if the * patch operation is successful. */ service.patchPermissions = function patchPermissions(dataSource, userID, permissionsToAdd, permissionsToRemove) { var permissionPatch = []; // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Add all the add operations to the patch addPatchOperations(permissionPatch, PermissionPatch.Operation.ADD, permissionsToAdd); // Add all the remove operations to the patch addPatchOperations(permissionPatch, PermissionPatch.Operation.REMOVE, permissionsToRemove); // Patch user permissions return $http({ method : 'PATCH', url : 'api/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(userID) + '/permissions', params : httpParameters, data : permissionPatch }) // Clear the cache .success(function permissionsPatched(){ cacheService.users.removeAll(); }); }; return service; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/services/languageService.js0100664 0001751 0001751 00000004525 12612226251 032504 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service for operating on language metadata via the REST API. */ angular.module('rest').factory('languageService', ['$injector', function languageService($injector) { // Required services var $http = $injector.get('$http'); var authenticationService = $injector.get('authenticationService'); var cacheService = $injector.get('cacheService'); var service = {}; /** * Makes a request to the REST API to get the list of languages, returning * a promise that provides a map of language names by language key if * successful. * * @returns {Promise.>} * A promise which will resolve with a map of language names by * language key upon success. */ service.getLanguages = function getLanguages() { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Retrieve available languages return $http({ cache : cacheService.languages, method : 'GET', url : 'api/languages', params : httpParameters }); }; return service; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/services/connectionGroupService.js0100664 0001751 0001751 00000020113 12645510255 034072 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service for operating on connection groups via the REST API. */ angular.module('rest').factory('connectionGroupService', ['$injector', function connectionGroupService($injector) { // Required services var $http = $injector.get('$http'); var $q = $injector.get('$q'); var authenticationService = $injector.get('authenticationService'); var cacheService = $injector.get('cacheService'); // Required types var ConnectionGroup = $injector.get('ConnectionGroup'); var service = {}; /** * Makes a request to the REST API to get an individual connection group * and all descendants, returning a promise that provides the corresponding * @link{ConnectionGroup} if successful. Descendant groups and connections * will be stored as children of that connection group. If a permission * type is specified, the result will be filtering by that permission. * * @param {String} [connectionGroupID=ConnectionGroup.ROOT_IDENTIFIER] * The ID of the connection group to retrieve. If not provided, the * root connection group will be retrieved by default. * * @param {String[]} [permissionTypes] * The set of permissions to filter with. A user must have one or more * of these permissions for a connection to appear in the result. * If null, no filtering will be performed. Valid values are listed * within PermissionSet.ObjectType. * * @returns {Promise.ConnectionGroup} * A promise which will resolve with a @link{ConnectionGroup} upon * success. */ service.getConnectionGroupTree = function getConnectionGroupTree(dataSource, connectionGroupID, permissionTypes) { // Use the root connection group ID if no ID is passed in connectionGroupID = connectionGroupID || ConnectionGroup.ROOT_IDENTIFIER; // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Add permission filter if specified if (permissionTypes) httpParameters.permission = permissionTypes; // Retrieve connection group return $http({ cache : cacheService.connections, method : 'GET', url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroupID) + '/tree', params : httpParameters }); }; /** * Makes a request to the REST API to get an individual connection group, * returning a promise that provides the corresponding * @link{ConnectionGroup} if successful. * * @param {String} [connectionGroupID=ConnectionGroup.ROOT_IDENTIFIER] * The ID of the connection group to retrieve. If not provided, the * root connection group will be retrieved by default. * * @returns {Promise.} A promise for the HTTP call. * A promise which will resolve with a @link{ConnectionGroup} upon * success. */ service.getConnectionGroup = function getConnectionGroup(dataSource, connectionGroupID) { // Use the root connection group ID if no ID is passed in connectionGroupID = connectionGroupID || ConnectionGroup.ROOT_IDENTIFIER; // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Retrieve connection group return $http({ cache : cacheService.connections, method : 'GET', url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroupID), params : httpParameters }); }; /** * Makes a request to the REST API to save a connection group, returning a * promise that can be used for processing the results of the call. If the * connection group is new, and thus does not yet have an associated * identifier, the identifier will be automatically set in the provided * connection group upon success. * * @param {ConnectionGroup} connectionGroup The connection group to update. * * @returns {Promise} * A promise for the HTTP call which will succeed if and only if the * save operation is successful. */ service.saveConnectionGroup = function saveConnectionGroup(dataSource, connectionGroup) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // If connection group is new, add it and set the identifier automatically if (!connectionGroup.identifier) { return $http({ method : 'POST', url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups', params : httpParameters, data : connectionGroup }) // Set the identifier on the new connection group and clear the cache .success(function connectionGroupCreated(newConnectionGroup){ connectionGroup.identifier = newConnectionGroup.identifier; cacheService.connections.removeAll(); }); } // Otherwise, update the existing connection group else { return $http({ method : 'PUT', url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroup.identifier), params : httpParameters, data : connectionGroup }) // Clear the cache .success(function connectionGroupUpdated(){ cacheService.connections.removeAll(); }); } }; /** * Makes a request to the REST API to delete a connection group, returning * a promise that can be used for processing the results of the call. * * @param {ConnectionGroup} connectionGroup The connection group to delete. * * @returns {Promise} * A promise for the HTTP call which will succeed if and only if the * delete operation is successful. */ service.deleteConnectionGroup = function deleteConnectionGroup(dataSource, connectionGroup) { // Build HTTP parameters set var httpParameters = { token : authenticationService.getCurrentToken() }; // Delete connection group return $http({ method : 'DELETE', url : 'api/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroup.identifier), params : httpParameters }) // Clear the cache .success(function connectionGroupDeleted(){ cacheService.connections.removeAll(); }); }; return service; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/types/Protocol.js0100664 0001751 0001751 00000004133 12612226251 030515 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service which defines the Protocol class. */ angular.module('rest').factory('Protocol', [function defineProtocol() { /** * The object returned by REST API calls when representing the data * associated with a supported remote desktop protocol. * * @constructor * @param {Protocol|Object} [template={}] * The object whose properties should be copied within the new * Protocol. */ var Protocol = function Protocol(template) { // Use empty object by default template = template || {}; /** * The name which uniquely identifies this protocol. * * @type String */ this.name = template.name; /** * An array of forms containing all known parameters for this protocol, * their types, and other information. * * @type Form[] * @default [] */ this.forms = template.forms || []; }; return Protocol; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/types/ConnectionGroup.js0100664 0001751 0001751 00000011226 12612226251 032031 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service which defines the ConnectionGroup class. */ angular.module('rest').factory('ConnectionGroup', [function defineConnectionGroup() { /** * The object returned by REST API calls when representing the data * associated with a connection group. * * @constructor * @param {ConnectionGroup|Object} [template={}] * The object whose properties should be copied within the new * ConnectionGroup. */ var ConnectionGroup = function ConnectionGroup(template) { // Use empty object by default template = template || {}; /** * The unique identifier associated with this connection group. * * @type String */ this.identifier = template.identifier; /** * The unique identifier of the connection group that contains this * connection group. * * @type String * @default ConnectionGroup.ROOT_IDENTIFIER */ this.parentIdentifier = template.parentIdentifier || ConnectionGroup.ROOT_IDENTIFIER; /** * The human-readable name of this connection group, which is not * necessarily unique. * * @type String */ this.name = template.name; /** * The type of this connection group, which may be either * ConnectionGroup.Type.ORGANIZATIONAL or * ConnectionGroup.Type.BALANCING. * * @type String * @default ConnectionGroup.Type.ORGANIZATIONAL */ this.type = template.type || ConnectionGroup.Type.ORGANIZATIONAL; /** * An array of all child connections, if known. This property may be * null or undefined if children have not been queried, and thus the * child connections are unknown. * * @type Connection[] */ this.childConnections = template.childConnections; /** * An array of all child connection groups, if known. This property may * be null or undefined if children have not been queried, and thus the * child connection groups are unknown. * * @type ConnectionGroup[] */ this.childConnectionGroups = template.childConnectionGroups; /** * Arbitrary name/value pairs which further describe this connection * group. The semantics and validity of these attributes are dictated * by the extension which defines them. * * @type Object. */ this.attributes = {}; /** * The count of currently active connections using this connection * group. This field will be returned from the REST API during a get * operation, but manually setting this field will have no effect. * * @type Number */ this.activeConnections = template.activeConnections; }; /** * The reserved identifier which always represents the root connection * group. * * @type String */ ConnectionGroup.ROOT_IDENTIFIER = "ROOT"; /** * All valid connection group types. */ ConnectionGroup.Type = { /** * The type string associated with balancing connection groups. * * @type String */ BALANCING : "BALANCING", /** * The type string associated with organizational connection groups. * * @type String */ ORGANIZATIONAL : "ORGANIZATIONAL" }; return ConnectionGroup; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/types/Error.js0100664 0001751 0001751 00000007100 12612226251 030002 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service which defines the Error class. */ angular.module('rest').factory('Error', [function defineError() { /** * The object returned by REST API calls when an error occurs. * * @constructor * @param {Error|Object} [template={}] * The object whose properties should be copied within the new * Error. */ var Error = function Error(template) { // Use empty object by default template = template || {}; /** * A human-readable message describing the error that occurred. * * @type String */ this.message = template.message; /** * The type string defining which values this parameter may contain, * as well as what properties are applicable. Valid types are listed * within Error.Type. * * @type String * @default Error.Type.INTERNAL_ERROR */ this.type = template.type || Error.Type.INTERNAL_ERROR; /** * Any parameters which were expected in the original request, or are * now expected as a result of the original request, if any. If no * such information is available, this will be null. * * @type Field[] */ this.expected = template.expected; }; /** * All valid field types. */ Error.Type = { /** * The requested operation could not be performed because the request * itself was malformed. * * @type String */ BAD_REQUEST : 'BAD_REQUEST', /** * The credentials provided were invalid. * * @type String */ INVALID_CREDENTIALS : 'INVALID_CREDENTIALS', /** * The credentials provided were not necessarily invalid, but were not * sufficient to determine validity. * * @type String */ INSUFFICIENT_CREDENTIALS : 'INSUFFICIENT_CREDENTIALS', /** * An internal server error has occurred. * * @type String */ INTERNAL_ERROR : 'INTERNAL_ERROR', /** * An object related to the request does not exist. * * @type String */ NOT_FOUND : 'NOT_FOUND', /** * Permission was denied to perform the requested operation. * * @type String */ PERMISSION_DENIED : 'PERMISSION_DENIED' }; return Error; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/types/Form.js0100664 0001751 0001751 00000003775 12612226251 027632 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service which defines the Form class. */ angular.module('rest').factory('Form', [function defineForm() { /** * The object returned by REST API calls when representing the data * associated with a form or set of configuration parameters. * * @constructor * @param {Form|Object} [template={}] * The object whose properties should be copied within the new * Form. */ var Form = function Form(template) { // Use empty object by default template = template || {}; /** * The name which uniquely identifies this form, or null if this form * has no name. * * @type String */ this.name = template.name; /** * All fields contained within this form. * * @type Field[] */ this.fields = template.fields || []; }; return Form; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/types/Field.js0100664 0001751 0001751 00000011754 12612226251 027746 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service which defines the Field class. */ angular.module('rest').factory('Field', [function defineField() { /** * The object returned by REST API calls when representing the data * associated with a field or configuration parameter. * * @constructor * @param {Field|Object} [template={}] * The object whose properties should be copied within the new * Field. */ var Field = function Field(template) { // Use empty object by default template = template || {}; /** * The name which uniquely identifies this parameter. * * @type String */ this.name = template.name; /** * The type string defining which values this parameter may contain, * as well as what properties are applicable. Valid types are listed * within Field.Type. * * @type String * @default Field.Type.TEXT */ this.type = template.type || Field.Type.TEXT; /** * All possible legal values for this parameter. * * @type String[] */ this.options = template.options; }; /** * All valid field types. */ Field.Type = { /** * The type string associated with parameters that may contain a single * line of arbitrary text. * * @type String */ TEXT : "TEXT", /** * The type string associated with parameters that may contain an * arbitrary string, where that string represents the username of the * user authenticating with the remote desktop service. * * @type String */ USERNAME : "USERNAME", /** * The type string associated with parameters that may contain an * arbitrary string, where that string represents the password of the * user authenticating with the remote desktop service. * * @type String */ PASSWORD : "PASSWORD", /** * The type string associated with parameters that may contain only * numeric values. * * @type String */ NUMERIC : "NUMERIC", /** * The type string associated with parameters that may contain only a * single possible value, where that value enables the parameter's * effect. It is assumed that each BOOLEAN field will provide exactly * one possible value (option), which will be the value if that field * is true. * * @type String */ BOOLEAN : "BOOLEAN", /** * The type string associated with parameters that may contain a * strictly-defined set of possible values. * * @type String */ ENUM : "ENUM", /** * The type string associated with parameters that may contain any * number of lines of arbitrary text. * * @type String */ MULTILINE : "MULTILINE", /** * The type string associated with parameters that may contain timezone * IDs. Valid timezone IDs are dictated by Java: * http://docs.oracle.com/javase/7/docs/api/java/util/TimeZone.html#getAvailableIDs%28%29 * * @type String */ TIMEZONE : "TIMEZONE", /** * The type string associated with parameters that may contain dates. * The format of the date is standardized as YYYY-MM-DD, zero-padded. * * @type String */ DATE : "DATE", /** * The type string associated with parameters that may contain times. * The format of the time is stnadardized as HH:MM:DD, zero-padded, * 24-hour. * * @type String */ TIME : "TIME" }; return Field; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/types/ActiveConnection.js0100664 0001751 0001751 00000005547 12612226251 032161 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service which defines the ActiveConnection class. */ angular.module('rest').factory('ActiveConnection', [function defineActiveConnection() { /** * The object returned by REST API calls when representing the data * associated with an active connection. Each active connection is * effectively a pairing of a connection and the user currently using it, * along with other information. * * @constructor * @param {ActiveConnection|Object} [template={}] * The object whose properties should be copied within the new * ActiveConnection. */ var ActiveConnection = function ActiveConnection(template) { // Use empty object by default template = template || {}; /** * The identifier which uniquely identifies this specific active * connection. * * @type String */ this.identifier = template.identifier; /** * The identifier of the connection associated with this active * connection. * * @type String */ this.connectionIdentifier = template.connectionIdentifier; /** * The time that the connection began, in seconds since * 1970-01-01 00:00:00 UTC, if known. * * @type Number */ this.startDate = template.startDate; /** * The remote host that initiated the connection, if known. * * @type String */ this.remoteHost = template.remoteHost; /** * The username of the user associated with the connection, if known. * * @type String */ this.username = template.username; }; return ActiveConnection; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/types/UserPasswordUpdate.js0100664 0001751 0001751 00000004222 12612226251 032517 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service which defines the UserPasswordUpdate class. */ angular.module('rest').factory('UserPasswordUpdate', [function defineUserPasswordUpdate() { /** * The object sent to the REST API when representing the data * associated with a user password update. * * @constructor * @param {UserPasswordUpdate|Object} [template={}] * The object whose properties should be copied within the new * UserPasswordUpdate. */ var UserPasswordUpdate = function UserPasswordUpdate(template) { // Use empty object by default template = template || {}; /** * This user's current password. Required for authenticating the user * as part of to the password update operation. * * @type String */ this.oldPassword = template.oldPassword; /** * The new password to set for the user. * * @type String */ this.newPassword = template.newPassword; }; return UserPasswordUpdate; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/types/User.js0100664 0001751 0001751 00000004662 12612226251 027641 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service which defines the User class. */ angular.module('rest').factory('User', [function defineUser() { /** * The object returned by REST API calls when representing the data * associated with a user. * * @constructor * @param {User|Object} [template={}] * The object whose properties should be copied within the new * User. */ var User = function User(template) { // Use empty object by default template = template || {}; /** * The name which uniquely identifies this user. * * @type String */ this.username = template.username; /** * This user's password. Note that the REST API may not populate this * property for the sake of security. In most cases, it's not even * possible for the authentication layer to retrieve the user's true * password. * * @type String */ this.password = template.password; /** * Arbitrary name/value pairs which further describe this user. The * semantics and validity of these attributes are dictated by the * extension which defines them. * * @type Object. */ this.attributes = {}; }; return User; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/types/Connection.js0100664 0001751 0001751 00000006766 12612226251 031031 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service which defines the Connection class. */ angular.module('rest').factory('Connection', [function defineConnection() { /** * The object returned by REST API calls when representing the data * associated with a connection. * * @constructor * @param {Connection|Object} [template={}] * The object whose properties should be copied within the new * Connection. */ var Connection = function Connection(template) { // Use empty object by default template = template || {}; /** * The unique identifier associated with this connection. * * @type String */ this.identifier = template.identifier; /** * The unique identifier of the connection group that contains this * connection. * * @type String */ this.parentIdentifier = template.parentIdentifier; /** * The human-readable name of this connection, which is not necessarily * unique. * * @type String */ this.name = template.name; /** * The name of the protocol associated with this connection, such as * "vnc" or "rdp". * * @type String */ this.protocol = template.protocol; /** * Connection configuration parameters, as dictated by the protocol in * use, arranged as name/value pairs. This information may not be * available until directly queried. If this information is * unavailable, this property will be null or undefined. * * @type Object. */ this.parameters = template.parameters; /** * Arbitrary name/value pairs which further describe this connection. * The semantics and validity of these attributes are dictated by the * extension which defines them. * * @type Object. */ this.attributes = {}; /** * The count of currently active connections using this connection. * This field will be returned from the REST API during a get * operation, but manually setting this field will have no effect. * * @type Number */ this.activeConnections = template.activeConnections; }; return Connection; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/types/PermissionPatch.js0100664 0001751 0001751 00000006371 12612226251 032032 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service which defines the PermissionPatch class. */ angular.module('rest').factory('PermissionPatch', [function definePermissionPatch() { /** * The object returned by REST API calls when representing changes to the * permissions granted to a specific user. * * @constructor * @param {PermissionPatch|Object} [template={}] * The object whose properties should be copied within the new * PermissionPatch. */ var PermissionPatch = function PermissionPatch(template) { // Use empty object by default template = template || {}; /** * The operation to apply to the permissions indicated by the path. * Valid operation values are defined within PermissionPatch.Operation. * * @type String */ this.op = template.op; /** * The path of the permissions to modify. Depending on the type of the * permission, this will be either "/connectionPermissions/ID", * "/connectionGroupPermissions/ID", "/userPermissions/ID", or * "/systemPermissions", where "ID" is the identifier of the object * to which the permissions apply, if any. * * @type String */ this.path = template.path; /** * The permissions being added or removed. If the permission applies to * an object, such as a connection or connection group, this will be a * value from PermissionSet.ObjectPermissionType. If the permission * applies to the system as a whole (the path is "/systemPermissions"), * this will be a value from PermissionSet.SystemPermissionType. * * @type String */ this.value = template.value; }; /** * All valid patch operations for permissions. Currently, only add and * remove are supported. */ PermissionPatch.Operation = { /** * Adds (grants) the specified permission. */ ADD : "add", /** * Removes (revokes) the specified permission. */ REMOVE : "remove" }; return PermissionPatch; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/types/PermissionFlagSet.js0100664 0001751 0001751 00000020761 12612226251 032317 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for defining the PermissionFlagSet class. */ angular.module('rest').factory('PermissionFlagSet', ['PermissionSet', function definePermissionFlagSet(PermissionSet) { /** * Alternative view of a @link{PermissionSet} which allows manipulation of * each permission through the setting (or retrieval) of boolean property * values. * * @constructor * @param {PermissionFlagSet|Object} template * The object whose properties should be copied within the new * PermissionFlagSet. */ var PermissionFlagSet = function PermissionFlagSet(template) { // Use empty object by default template = template || {}; /** * The granted state of each system permission, as a map of system * permission type string to boolean value. A particular permission is * granted if its corresponding boolean value is set to true. Valid * permission type strings are defined within * PermissionSet.SystemPermissionType. Permissions which are not * granted may be set to false, but this is not required. * * @type Object. */ this.systemPermissions = template.systemPermissions || {}; /** * The granted state of each permission for each connection, as a map * of object permission type string to permission map. The permission * map is, in turn, a map of connection identifier to boolean value. A * particular permission is granted if its corresponding boolean value * is set to true. Valid permission type strings are defined within * PermissionSet.ObjectPermissionType. Permissions which are not * granted may be set to false, but this is not required. * * @type Object.> */ this.connectionPermissions = template.connectionPermissions || { 'READ' : {}, 'UPDATE' : {}, 'DELETE' : {}, 'ADMINISTER' : {} }; /** * The granted state of each permission for each connection group, as a * map of object permission type string to permission map. The * permission map is, in turn, a map of connection group identifier to * boolean value. A particular permission is granted if its * corresponding boolean value is set to true. Valid permission type * strings are defined within PermissionSet.ObjectPermissionType. * Permissions which are not granted may be set to false, but this is * not required. * * @type Object.> */ this.connectionGroupPermissions = template.connectionGroupPermissions || { 'READ' : {}, 'UPDATE' : {}, 'DELETE' : {}, 'ADMINISTER' : {} }; /** * The granted state of each permission for each active connection, as * a map of object permission type string to permission map. The * permission map is, in turn, a map of active connection identifier to * boolean value. A particular permission is granted if its * corresponding boolean value is set to true. Valid permission type * strings are defined within PermissionSet.ObjectPermissionType. * Permissions which are not granted may be set to false, but this is * not required. * * @type Object.> */ this.activeConnectionPermissions = template.activeConnectionPermissions || { 'READ' : {}, 'UPDATE' : {}, 'DELETE' : {}, 'ADMINISTER' : {} }; /** * The granted state of each permission for each user, as a map of * object permission type string to permission map. The permission map * is, in turn, a map of username to boolean value. A particular * permission is granted if its corresponding boolean value is set to * true. Valid permission type strings are defined within * PermissionSet.ObjectPermissionType. Permissions which are not * granted may be set to false, but this is not required. * * @type Object.> */ this.userPermissions = template.userPermissions || { 'READ' : {}, 'UPDATE' : {}, 'DELETE' : {}, 'ADMINISTER' : {} }; }; /** * Iterates through all permissions in the given permission map, setting * the corresponding permission flags in the given permission flag map. * * @param {Object.} permMap * Map of object identifiers to the set of granted permissions. Each * permission is represented by a string listed within * PermissionSet.ObjectPermissionType. * * @param {Object.>} flagMap * Map of permission type strings to identifier/flag pairs representing * whether the permission of that type is granted for the object having * having the associated identifier. */ var addObjectPermissions = function addObjectPermissions(permMap, flagMap) { // For each defined identifier in the permission map for (var identifier in permMap) { // Pull the permission array and loop through each permission var permissions = permMap[identifier]; permissions.forEach(function addObjectPermission(type) { // Get identifier/flag mapping, creating first if necessary var objectFlags = flagMap[type] = flagMap[type] || {}; // Set flag for current permission objectFlags[identifier] = true; }); } }; /** * Creates a new PermissionFlagSet, populating it with all the permissions * indicated as granted within the given PermissionSet. * * @param {PermissionSet} permissionSet * The PermissionSet containing the permissions to be copied into a new * PermissionFlagSet. * * @returns {PermissionFlagSet} * A new PermissionFlagSet containing flags representing all granted * permissions from the given PermissionSet. */ PermissionFlagSet.fromPermissionSet = function fromPermissionSet(permissionSet) { var permissionFlagSet = new PermissionFlagSet(); // Add all granted system permissions permissionSet.systemPermissions.forEach(function addSystemPermission(type) { permissionFlagSet.systemPermissions[type] = true; }); // Add all granted connection permissions addObjectPermissions(permissionSet.connectionPermissions, permissionFlagSet.connectionPermissions); // Add all granted connection group permissions addObjectPermissions(permissionSet.connectionGroupPermissions, permissionFlagSet.connectionGroupPermissions); // Add all granted active connection permissions addObjectPermissions(permissionSet.activeConnectionPermissions, permissionFlagSet.activeConnectionPermissions); // Add all granted user permissions addObjectPermissions(permissionSet.userPermissions, permissionFlagSet.userPermissions); return permissionFlagSet; }; return PermissionFlagSet; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/types/PermissionSet.js0100664 0001751 0001751 00000056255 12612226251 031534 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service which defines the PermissionSet class. */ angular.module('rest').factory('PermissionSet', [function definePermissionSet() { /** * The object returned by REST API calls when representing the permissions * granted to a specific user. * * @constructor * @param {PermissionSet|Object} [template={}] * The object whose properties should be copied within the new * PermissionSet. */ var PermissionSet = function PermissionSet(template) { // Use empty object by default template = template || {}; /** * Map of connection identifiers to the corresponding array of granted * permissions. Each permission is represented by a string listed * within PermissionSet.ObjectPermissionType. * * @type Object. */ this.connectionPermissions = template.connectionPermissions || {}; /** * Map of connection group identifiers to the corresponding array of * granted permissions. Each permission is represented by a string * listed within PermissionSet.ObjectPermissionType. * * @type Object. */ this.connectionGroupPermissions = template.connectionGroupPermissions || {}; /** * Map of active connection identifiers to the corresponding array of * granted permissions. Each permission is represented by a string * listed within PermissionSet.ObjectPermissionType. * * @type Object. */ this.activeConnectionPermissions = template.activeConnectionPermissions || {}; /** * Map of user identifiers to the corresponding array of granted * permissions. Each permission is represented by a string listed * within PermissionSet.ObjectPermissionType. * * @type Object. */ this.userPermissions = template.userPermissions || {}; /** * Array of granted system permissions. Each permission is represented * by a string listed within PermissionSet.SystemPermissionType. * * @type String[] */ this.systemPermissions = template.systemPermissions || []; }; /** * Valid object permission type strings. */ PermissionSet.ObjectPermissionType = { /** * Permission to read from the specified object. */ READ : "READ", /** * Permission to update the specified object. */ UPDATE : "UPDATE", /** * Permission to delete the specified object. */ DELETE : "DELETE", /** * Permission to administer the specified object */ ADMINISTER : "ADMINISTER" }; /** * Valid system permission type strings. */ PermissionSet.SystemPermissionType = { /** * Permission to administer the entire system. */ ADMINISTER : "ADMINISTER", /** * Permission to create new users. */ CREATE_USER : "CREATE_USER", /** * Permission to create new connections. */ CREATE_CONNECTION : "CREATE_CONNECTION", /** * Permission to create new connection groups. */ CREATE_CONNECTION_GROUP : "CREATE_CONNECTION_GROUP" }; /** * Returns whether the given permission is granted for at least one * arbitrary object, regardless of ID. * * @param {Object.} permMap * The permission map to check, where each entry maps an object * identifer to the array of granted permissions. * * @param {String} type * The permission to search for, as defined by * PermissionSet.ObjectPermissionType. * * @returns {Boolean} * true if the permission is present (granted), false otherwise. */ var containsPermission = function containsPermission(permMap, type) { // Search all identifiers for given permission for (var identifier in permMap) { // If permission is granted, then no further searching is necessary if (permMap[identifier].indexOf(type) !== -1) return true; } // No such permission exists return false; }; /** * Returns whether the given permission is granted for the arbitrary * object having the given ID. If no ID is given, this function determines * whether the permission is granted at all for any such arbitrary object. * * @param {Object.} permMap * The permission map to check, where each entry maps an object * identifer to the array of granted permissions. * * @param {String} type * The permission to search for, as defined by * PermissionSet.ObjectPermissionType. * * @param {String} [identifier] * The identifier of the object to which the permission applies. * * @returns {Boolean} * true if the permission is present (granted), false otherwise. */ var hasPermission = function hasPermission(permMap, type, identifier) { // No permission if no permission map at all if (!permMap) return false; // If no identifier given, search ignoring the identifier if (!identifier) return containsPermission(permMap, type); // If identifier not present at all, there are no such permissions if (!(identifier in permMap)) return false; return permMap[identifier].indexOf(type) !== -1; }; /** * Returns whether the given permission is granted for the connection * having the given ID. * * @param {PermissionSet|Object} permSet * The permission set to check. * * @param {String} type * The permission to search for, as defined by * PermissionSet.ObjectPermissionType. * * @param {String} identifier * The identifier of the connection to which the permission applies. * * @returns {Boolean} * true if the permission is present (granted), false otherwise. */ PermissionSet.hasConnectionPermission = function hasConnectionPermission(permSet, type, identifier) { return hasPermission(permSet.connectionPermissions, type, identifier); }; /** * Returns whether the given permission is granted for the connection group * having the given ID. * * @param {PermissionSet|Object} permSet * The permission set to check. * * @param {String} type * The permission to search for, as defined by * PermissionSet.ObjectPermissionType. * * @param {String} identifier * The identifier of the connection group to which the permission * applies. * * @returns {Boolean} * true if the permission is present (granted), false otherwise. */ PermissionSet.hasConnectionGroupPermission = function hasConnectionGroupPermission(permSet, type, identifier) { return hasPermission(permSet.connectionGroupPermissions, type, identifier); }; /** * Returns whether the given permission is granted for the active * connection having the given ID. * * @param {PermissionSet|Object} permSet * The permission set to check. * * @param {String} type * The permission to search for, as defined by * PermissionSet.ObjectPermissionType. * * @param {String} identifier * The identifier of the active connection to which the permission * applies. * * @returns {Boolean} * true if the permission is present (granted), false otherwise. */ PermissionSet.hasActiveConnectionPermission = function hasActiveConnectionPermission(permSet, type, identifier) { return hasPermission(permSet.activeConnectionPermissions, type, identifier); }; /** * Returns whether the given permission is granted for the user having the * given ID. * * @param {PermissionSet|Object} permSet * The permission set to check. * * @param {String} type * The permission to search for, as defined by * PermissionSet.ObjectPermissionType. * * @param {String} identifier * The identifier of the user to which the permission applies. * * @returns {Boolean} * true if the permission is present (granted), false otherwise. */ PermissionSet.hasUserPermission = function hasUserPermission(permSet, type, identifier) { return hasPermission(permSet.userPermissions, type, identifier); }; /** * Returns whether the given permission is granted at the system level. * * @param {PermissionSet|Object} permSet * The permission set to check. * * @param {String} type * The permission to search for, as defined by * PermissionSet.SystemPermissionType. * * @returns {Boolean} * true if the permission is present (granted), false otherwise. */ PermissionSet.hasSystemPermission = function hasSystemPermission(permSet, type) { if (!permSet.systemPermissions) return false; return permSet.systemPermissions.indexOf(type) !== -1; }; /** * Adds the given system permission to the given permission set, if not * already present. If the permission is already present, this function has * no effect. * * @param {PermissionSet} permSet * The permission set to modify. * * @param {String} type * The permission to add, as defined by * PermissionSet.SystemPermissionType. * * @returns {Boolean} * true if the permission was added, false if the permission was * already present in the given permission set. */ PermissionSet.addSystemPermission = function addSystemPermission(permSet, type) { permSet.systemPermissions = permSet.systemPermissions || []; // Add permission, if it doesn't already exist if (permSet.systemPermissions.indexOf(type) === -1) { permSet.systemPermissions.push(type); return true; } // Permission already present return false; }; /** * Removes the given system permission from the given permission set, if * present. If the permission is not present, this function has no effect. * * @param {PermissionSet} permSet * The permission set to modify. * * @param {String} type * The permission to remove, as defined by * PermissionSet.SystemPermissionType. * * @returns {Boolean} * true if the permission was removed, false if the permission was not * present in the given permission set. */ PermissionSet.removeSystemPermission = function removeSystemPermission(permSet, type) { permSet.systemPermissions = permSet.systemPermissions || []; // Remove permission, if it exists var permLocation = permSet.systemPermissions.indexOf(type); if (permLocation !== -1) { permSet.systemPermissions.splice(permLocation, 1); return true; } // Permission not present return false; }; /** * Adds the given permission applying to the arbitrary object with the * given ID to the given permission set, if not already present. If the * permission is already present, this function has no effect. * * @param {Object.} permMap * The permission map to modify, where each entry maps an object * identifer to the array of granted permissions. * * @param {String} type * The permission to add, as defined by * PermissionSet.ObjectPermissionType. * * @param {String} identifier * The identifier of the arbitrary object to which the permission * applies. * * @returns {Boolean} * true if the permission was added, false if the permission was * already present in the given permission set. */ var addObjectPermission = function addObjectPermission(permMap, type, identifier) { // Pull array of permissions, creating it if necessary var permArray = permMap[identifier] = permMap[identifier] || []; // Add permission, if it doesn't already exist if (permArray.indexOf(type) === -1) { permArray.push(type); return true; } // Permission already present return false; }; /** * Removes the given permission applying to the arbitrary object with the * given ID from the given permission set, if present. If the permission is * not present, this function has no effect. * * @param {Object.} permMap * The permission map to modify, where each entry maps an object * identifer to the array of granted permissions. * * @param {String} type * The permission to remove, as defined by * PermissionSet.ObjectPermissionType. * * @param {String} identifier * The identifier of the arbitrary object to which the permission * applies. * * @returns {Boolean} * true if the permission was removed, false if the permission was not * present in the given permission set. */ var removeObjectPermission = function removeObjectPermission(permMap, type, identifier) { // Pull array of permissions var permArray = permMap[identifier]; // If no permissions present at all, nothing to remove if (!(identifier in permMap)) return false; // Remove permission, if it exists var permLocation = permArray.indexOf(type); if (permLocation !== -1) { permArray.splice(permLocation, 1); return true; } // Permission not present return false; }; /** * Adds the given connection permission applying to the connection with * the given ID to the given permission set, if not already present. If the * permission is already present, this function has no effect. * * @param {PermissionSet} permSet * The permission set to modify. * * @param {String} type * The permission to add, as defined by * PermissionSet.ObjectPermissionType. * * @param {String} identifier * The identifier of the connection to which the permission applies. * * @returns {Boolean} * true if the permission was added, false if the permission was * already present in the given permission set. */ PermissionSet.addConnectionPermission = function addConnectionPermission(permSet, type, identifier) { permSet.connectionPermissions = permSet.connectionPermissions || {}; return addObjectPermission(permSet.connectionPermissions, type, identifier); }; /** * Removes the given connection permission applying to the connection with * the given ID from the given permission set, if present. If the * permission is not present, this function has no effect. * * @param {PermissionSet} permSet * The permission set to modify. * * @param {String} type * The permission to remove, as defined by * PermissionSet.ObjectPermissionType. * * @param {String} identifier * The identifier of the connection to which the permission applies. * * @returns {Boolean} * true if the permission was removed, false if the permission was not * present in the given permission set. */ PermissionSet.removeConnectionPermission = function removeConnectionPermission(permSet, type, identifier) { permSet.connectionPermissions = permSet.connectionPermissions || {}; return removeObjectPermission(permSet.connectionPermissions, type, identifier); }; /** * Adds the given connection group permission applying to the connection * group with the given ID to the given permission set, if not already * present. If the permission is already present, this function has no * effect. * * @param {PermissionSet} permSet * The permission set to modify. * * @param {String} type * The permission to add, as defined by * PermissionSet.ObjectPermissionType. * * @param {String} identifier * The identifier of the connection group to which the permission * applies. * * @returns {Boolean} * true if the permission was added, false if the permission was * already present in the given permission set. */ PermissionSet.addConnectionGroupPermission = function addConnectionGroupPermission(permSet, type, identifier) { permSet.connectionGroupPermissions = permSet.connectionGroupPermissions || {}; return addObjectPermission(permSet.connectionGroupPermissions, type, identifier); }; /** * Removes the given connection group permission applying to the connection * group with the given ID from the given permission set, if present. If * the permission is not present, this function has no effect. * * @param {PermissionSet} permSet * The permission set to modify. * * @param {String} type * The permission to remove, as defined by * PermissionSet.ObjectPermissionType. * * @param {String} identifier * The identifier of the connection group to which the permission * applies. * * @returns {Boolean} * true if the permission was removed, false if the permission was not * present in the given permission set. */ PermissionSet.removeConnectionGroupPermission = function removeConnectionGroupPermission(permSet, type, identifier) { permSet.connectionGroupPermissions = permSet.connectionGroupPermissions || {}; return removeObjectPermission(permSet.connectionGroupPermissions, type, identifier); }; /** * Adds the given active connection permission applying to the connection * group with the given ID to the given permission set, if not already * present. If the permission is already present, this function has no * effect. * * @param {PermissionSet} permSet * The permission set to modify. * * @param {String} type * The permission to add, as defined by * PermissionSet.ObjectPermissionType. * * @param {String} identifier * The identifier of the active connection to which the permission * applies. * * @returns {Boolean} * true if the permission was added, false if the permission was * already present in the given permission set. */ PermissionSet.addActiveConnectionPermission = function addActiveConnectionPermission(permSet, type, identifier) { permSet.activeConnectionPermissions = permSet.activeConnectionPermissions || {}; return addObjectPermission(permSet.activeConnectionPermissions, type, identifier); }; /** * Removes the given active connection permission applying to the * connection group with the given ID from the given permission set, if * present. If the permission is not present, this function has no effect. * * @param {PermissionSet} permSet * The permission set to modify. * * @param {String} type * The permission to remove, as defined by * PermissionSet.ObjectPermissionType. * * @param {String} identifier * The identifier of the active connection to which the permission * applies. * * @returns {Boolean} * true if the permission was removed, false if the permission was not * present in the given permission set. */ PermissionSet.removeActiveConnectionPermission = function removeActiveConnectionPermission(permSet, type, identifier) { permSet.activeConnectionPermissions = permSet.activeConnectionPermissions || {}; return removeObjectPermission(permSet.activeConnectionPermissions, type, identifier); }; /** * Adds the given user permission applying to the user with the given ID to * the given permission set, if not already present. If the permission is * already present, this function has no effect. * * @param {PermissionSet} permSet * The permission set to modify. * * @param {String} type * The permission to add, as defined by * PermissionSet.ObjectPermissionType. * * @param {String} identifier * The identifier of the user to which the permission applies. * * @returns {Boolean} * true if the permission was added, false if the permission was * already present in the given permission set. */ PermissionSet.addUserPermission = function addUserPermission(permSet, type, identifier) { permSet.userPermissions = permSet.userPermissions || {}; return addObjectPermission(permSet.userPermissions, type, identifier); }; /** * Removes the given user permission applying to the user with the given ID * from the given permission set, if present. If the permission is not * present, this function has no effect. * * @param {PermissionSet} permSet * The permission set to modify. * * @param {String} type * The permission to remove, as defined by * PermissionSet.ObjectPermissionType. * * @param {String} identifier * The identifier of the user to whom the permission applies. * * @returns {Boolean} * true if the permission was removed, false if the permission was not * present in the given permission set. */ PermissionSet.removeUserPermission = function removeUserPermission(permSet, type, identifier) { permSet.userPermissions = permSet.userPermissions || {}; return removeObjectPermission(permSet.userPermissions, type, identifier); }; return PermissionSet; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/types/ConnectionHistoryEntry.js0100664 0001751 0001751 00000014164 12645510255 033432 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service which defines the ConnectionHistoryEntry class. */ angular.module('rest').factory('ConnectionHistoryEntry', [function defineConnectionHistoryEntry() { /** * The object returned by REST API calls when representing the data * associated with an entry in a connection's usage history. Each history * entry represents the time at which a particular started using a * connection and, if applicable, the time that usage stopped. * * @constructor * @param {ConnectionHistoryEntry|Object} [template={}] * The object whose properties should be copied within the new * ConnectionHistoryEntry. */ var ConnectionHistoryEntry = function ConnectionHistoryEntry(template) { // Use empty object by default template = template || {}; /** * The identifier of the connection associated with this history entry. * * @type String */ this.connectionIdentifier = template.connectionIdentifier; /** * The name of the connection associated with this history entry. * * @type String */ this.connectionName = template.connectionName; /** * The time that usage began, in seconds since 1970-01-01 00:00:00 UTC. * * @type Number */ this.startDate = template.startDate; /** * The time that usage ended, in seconds since 1970-01-01 00:00:00 UTC. * The absence of an endDate does NOT necessarily indicate that the * connection is still in use, particularly if the server was shutdown * or restarted before the history entry could be updated. To determine * whether a connection is still active, check the active property of * this history entry. * * @type Number */ this.endDate = template.endDate; /** * The remote host that initiated this connection, if known. * * @type String */ this.remoteHost = template.remoteHost; /** * The username of the user associated with this particular usage of * the connection. * * @type String */ this.username = template.username; /** * Whether this usage of the connection is still active. Note that this * is the only accurate way to check for connection activity; the * absence of endDate does not necessarily imply the connection is * active, as the history entry may simply be incomplete. * * @type Boolean */ this.active = template.active; }; /** * All possible predicates for sorting ConnectionHistoryEntry objects using * the REST API. By default, each predicate indicates ascending order. To * indicate descending order, add "-" to the beginning of the predicate. * * @type Object. */ ConnectionHistoryEntry.SortPredicate = { /** * The date and time that the connection associated with the history * entry began (connected). */ START_DATE : 'startDate' }; /** * Value/unit pair representing the length of time that a connection was * used. * * @constructor * @param {Number} milliseconds * The number of milliseconds that the associated connection was used. */ ConnectionHistoryEntry.Duration = function Duration(milliseconds) { /** * The provided duration in seconds. * * @type Number */ var seconds = milliseconds / 1000; /** * Rounds the given value to the nearest tenth. * * @param {Number} value The value to round. * @returns {Number} The given value, rounded to the nearest tenth. */ var round = function round(value) { return Math.round(value * 10) / 10; }; // Days if (seconds >= 86400) { this.value = round(seconds / 86400); this.unit = 'day'; } // Hours else if (seconds >= 3600) { this.value = round(seconds / 3600); this.unit = 'hour'; } // Minutes else if (seconds >= 60) { this.value = round(seconds / 60); this.unit = 'minute'; } // Seconds else { /** * The number of seconds (or minutes, or hours, etc.) that the * connection was used. The units associated with this value are * represented by the unit property. * * @type Number */ this.value = round(seconds); /** * The units associated with the value of this duration. Valid * units are 'second', 'minute', 'hour', and 'day'. * * @type String */ this.unit = 'second'; } }; return ConnectionHistoryEntry; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/rest/restModule.js0100664 0001751 0001751 00000002361 12612226251 027674 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The module for code relating to communication with the REST API of the * Guacamole web application. */ angular.module('rest', ['auth']); guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/services/guacImage.js0100664 0001751 0001751 00000011336 12645510255 031567 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for checking browser image support. */ angular.module('client').factory('guacImage', ['$injector', function guacImage($injector) { // Required services var $q = $injector.get('$q'); var service = {}; /** * Map of possibly-supported image mimetypes to corresponding test images * encoded with base64. If the image is correctly decoded, it will be a * single pixel (1x1) image. * * @type Object. */ var testImages = { /** * Test JPEG image, encoded as base64. */ 'image/jpeg' : '/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoH' + 'BwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQME' + 'BAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU' + 'FBQUFBQUFBQUFBQUFBT/wAARCAABAAEDAREAAhEBAxEB/8QAFAABAAAAAAAAAAA' + 'AAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAUAQEAAAAAAAAAAAAAAA' + 'AAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AVMH/2Q==', /** * Test PNG image, encoded as base64. */ 'image/png' : 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX///+nxBvI' + 'AAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==', /** * Test WebP image, encoded as base64. */ 'image/webp' : 'UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==' }; /** * Deferred which tracks the progress and ultimate result of all pending * image format tests. * * @type Deferred */ var deferredSupportedMimetypes = $q.defer(); /** * Array of all promises associated with pending image tests. Each image * test promise MUST be guaranteed to resolve and MUST NOT be rejected. * * @type Promise[] */ var pendingTests = []; /** * The array of supported image formats. This will be gradually populated * by the various image tests that occur in the background, and will not be * fully populated until all promises within pendingTests are resolved. * * @type String[] */ var supported = []; /** * Return a promise which resolves with to an array of image mimetypes * supported by the browser, once those mimetypes are known. The returned * promise is guaranteed to resolve successfully. * * @returns {Promise.} * A promise which resolves with an array of image mimetypes supported * by the browser. */ service.getSupportedMimetypes = function getSupportedMimetypes() { return deferredSupportedMimetypes.promise; }; // Test each possibly-supported image angular.forEach(testImages, function testImageSupport(data, mimetype) { // Add promise for current image test var imageTest = $q.defer(); pendingTests.push(imageTest.promise); // Attempt to load image var image = new Image(); image.src = 'data:' + mimetype + ';base64,' + data; // Store as supported depending on whether load was successful image.onload = image.onerror = function imageTestComplete() { // Image format is supported if successfully decoded if (image.width === 1 && image.height === 1) supported.push(mimetype); // Test is complete imageTest.resolve(); }; }); // When all image tests are complete, resolve promise with list of // supported formats $q.all(pendingTests).then(function imageTestsCompleted() { deferredSupportedMimetypes.resolve(supported); }); return service; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/services/guacVideo.js0100664 0001751 0001751 00000002762 12645510255 031616 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for checking browser video support. */ angular.module('client').factory('guacVideo', [function guacVideo() { /** * Object describing the UI's level of video support. */ return new (function() { /** * Array of all supported video mimetypes. */ this.supported = Guacamole.VideoPlayer.getSupportedTypes(); })(); }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/services/guacClientManager.js0100664 0001751 0001751 00000013214 12612226251 033245 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for managing several active Guacamole clients. */ angular.module('client').factory('guacClientManager', ['$injector', function guacClientManager($injector) { // Required types var ManagedClient = $injector.get('ManagedClient'); // Required services var $window = $injector.get('$window'); var sessionStorageFactory = $injector.get('sessionStorageFactory'); var service = {}; /** * Getter/setter which retrieves or sets the map of all active managed * clients. Each key is the ID of the connection used by that client. * * @type Function */ var storedManagedClients = sessionStorageFactory.create({}, function destroyClientStorage() { // Disconnect all clients when storage is destroyed service.clear(); }); /** * Returns a map of all active managed clients. Each key is the ID of the * connection used by that client. * * @returns {Object.} * A map of all active managed clients. */ service.getManagedClients = function getManagedClients() { return storedManagedClients(); }; /** * Removes the existing ManagedClient associated with the connection having * the given ID, if any. If no such a ManagedClient already exists, this * function has no effect. * * @param {String} id * The ID of the connection whose ManagedClient should be removed. * * @returns {Boolean} * true if an existing client was removed, false otherwise. */ service.removeManagedClient = function replaceManagedClient(id) { var managedClients = storedManagedClients(); // Remove client if it exists if (id in managedClients) { // Disconnect and remove managedClients[id].client.disconnect(); delete managedClients[id]; // A client was removed return true; } // No client was removed return false; }; /** * Creates a new ManagedClient associated with the connection having the * given ID. If such a ManagedClient already exists, it is disconnected and * replaced. * * @param {String} id * The ID of the connection whose ManagedClient should be retrieved. * * @param {String} [connectionParameters] * Any additional HTTP parameters to pass while connecting. This * parameter only has an effect if a new connection is established as * a result of this function call. * * @returns {ManagedClient} * The ManagedClient associated with the connection having the given * ID. */ service.replaceManagedClient = function replaceManagedClient(id, connectionParameters) { // Disconnect any existing client service.removeManagedClient(id); // Set new client return storedManagedClients()[id] = ManagedClient.getInstance(id, connectionParameters); }; /** * Returns the ManagedClient associated with the connection having the * given ID. If no such ManagedClient exists, a new ManagedClient is * created. * * @param {String} id * The ID of the connection whose ManagedClient should be retrieved. * * @param {String} [connectionParameters] * Any additional HTTP parameters to pass while connecting. This * parameter only has an effect if a new connection is established as * a result of this function call. * * @returns {ManagedClient} * The ManagedClient associated with the connection having the given * ID. */ service.getManagedClient = function getManagedClient(id, connectionParameters) { var managedClients = storedManagedClients(); // Create new managed client if it doesn't already exist if (!(id in managedClients)) managedClients[id] = ManagedClient.getInstance(id, connectionParameters); // Return existing client return managedClients[id]; }; /** * Disconnects and removes all currently-connected clients. */ service.clear = function clear() { var managedClients = storedManagedClients(); // Disconnect each managed client for (var id in managedClients) managedClients[id].client.disconnect(); // Clear managed clients storedManagedClients({}); }; // Disconnect all clients when window is unloaded $window.addEventListener('unload', service.clear); return service; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/services/guacAudio.js0100664 0001751 0001751 00000003030 12645510255 031576 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for checking browser audio support. */ angular.module('client').factory('guacAudio', [function guacAudio() { /** * Object describing the UI's level of audio support. */ return new (function() { /** * Array of all supported audio mimetypes. * * @type String[] */ this.supported = Guacamole.AudioPlayer.getSupportedTypes(); })(); }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/directives/guacFileBrowser.js0100664 0001751 0001751 00000026121 12645506164 033310 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which displays the contents of a filesystem received through the * Guacamole client. */ angular.module('client').directive('guacFileBrowser', [function guacFileBrowser() { return { restrict: 'E', replace: true, scope: { /** * The client whose file transfers should be managed by this * directive. * * @type ManagedClient */ client : '=', /** * @type ManagedFilesystem */ filesystem : '=' }, templateUrl: 'app/client/templates/guacFileBrowser.html', controller: ['$scope', '$element', '$injector', function guacFileBrowserController($scope, $element, $injector) { // Required types var ManagedFilesystem = $injector.get('ManagedFilesystem'); // Required services var $interpolate = $injector.get('$interpolate'); var $templateRequest = $injector.get('$templateRequest'); /** * The jQuery-wrapped element representing the contents of the * current directory within the file browser. * * @type Element[] */ var currentDirectoryContents = $element.find('.current-directory-contents'); /** * Statically-cached template HTML used to render each file within * a directory. Once available, this will be used through * createFileElement() to generate the DOM elements which make up * a directory listing. * * @type String */ var fileTemplate = null; /** * Returns whether the given file is a normal file. * * @param {ManagedFilesystem.File} file * The file to test. * * @returns {Boolean} * true if the given file is a normal file, false otherwise. */ $scope.isNormalFile = function isNormalFile(file) { return file.type === ManagedFilesystem.File.Type.NORMAL; }; /** * Returns whether the given file is a directory. * * @param {ManagedFilesystem.File} file * The file to test. * * @returns {Boolean} * true if the given file is a directory, false otherwise. */ $scope.isDirectory = function isDirectory(file) { return file.type === ManagedFilesystem.File.Type.DIRECTORY; }; /** * Changes the currently-displayed directory to the given * directory. * * @param {ManagedFilesystem.File} file * The directory to change to. */ $scope.changeDirectory = function changeDirectory(file) { ManagedFilesystem.changeDirectory($scope.filesystem, file); }; /** * Initiates a download of the given file. The progress of the * download can be observed through guacFileTransferManager. * * @param {ManagedFilesystem.File} file * The file to download. */ $scope.downloadFile = function downloadFile(file) { ManagedFilesystem.downloadFile($scope.client, $scope.filesystem, file.streamName); }; /** * Recursively interpolates all text nodes within the DOM tree of * the given element. All other node types, attributes, etc. will * be left uninterpolated. * * @param {Element} element * The element at the root of the DOM tree to be interpolated. * * @param {Object} context * The evaluation context to use when evaluating expressions * embedded in text nodes within the provided element. */ var interpolateElement = function interpolateElement(element, context) { // Interpolate the contents of text nodes directly if (element.nodeType === Node.TEXT_NODE) element.nodeValue = $interpolate(element.nodeValue)(context); // Recursively interpolate the contents of all descendant text // nodes if (element.hasChildNodes()) { var children = element.childNodes; for (var i = 0; i < children.length; i++) interpolateElement(children[i], context); } }; /** * Creates a new element representing the given file and properly * handling user events, bypassing the overhead incurred through * use of ngRepeat and related techniques. * * Note that this function depends on the availability of the * statically-cached fileTemplate. * * @param {ManagedFilesystem.File} file * The file to generate an element for. * * @returns {Element[]} * A jQuery-wrapped array containing a single DOM element * representing the given file. */ var createFileElement = function createFileElement(file) { // Create from internal template var element = angular.element(fileTemplate); interpolateElement(element[0], file); // Double-clicking on unknown file types will do nothing var fileAction = function doNothing() {}; // Change current directory when directories are clicked if ($scope.isDirectory(file)) { element.addClass('directory'); fileAction = function changeDirectory() { $scope.changeDirectory(file); }; } // Initiate downloads when normal files are clicked else if ($scope.isNormalFile(file)) { element.addClass('normal-file'); fileAction = function downloadFile() { $scope.downloadFile(file); }; } // Mark file as focused upon click element.on('click', function handleFileClick() { // Fire file-specific action if already focused if (element.hasClass('focused')) { fileAction(); element.removeClass('focused'); } // Otherwise mark as focused else { element.parent().children().removeClass('focused'); element.addClass('focused'); } }); // Prevent text selection during navigation element.on('selectstart', function avoidSelect(e) { e.preventDefault(); e.stopPropagation(); }); return element; }; /** * Sorts the given map of files, returning an array of those files * grouped by file type (directories first, followed by non- * directories) and sorted lexicographically. * * @param {Object.} files * The map of files to sort. * * @returns {ManagedFilesystem.File[]} * An array of all files in the given map, sorted * lexicographically with directories first, followed by non- * directories. */ var sortFiles = function sortFiles(files) { // Get all given files as an array var unsortedFiles = []; for (var name in files) unsortedFiles.push(files[name]); // Sort files - directories first, followed by all other files // sorted by name return unsortedFiles.sort(function fileComparator(a, b) { // Directories come before non-directories if ($scope.isDirectory(a) && !$scope.isDirectory(b)) return -1; // Non-directories come after directories if (!$scope.isDirectory(a) && $scope.isDirectory(b)) return 1; // All other combinations are sorted by name return a.name.localeCompare(b.name); }); }; // Watch directory contents once file template is available $templateRequest('app/client/templates/file.html').then(function fileTemplateRetrieved(html) { // Store file template statically fileTemplate = html; // Update the contents of the file browser whenever the current directory (or its contents) changes $scope.$watch('filesystem.currentDirectory.files', function currentDirectoryChanged(files) { // Clear current content currentDirectoryContents.html(''); // Display all files within current directory, sorted angular.forEach(sortFiles(files), function displayFile(file) { currentDirectoryContents.append(createFileElement(file)); }); }); }); // end retrieve file template // Refresh file browser when any upload completes $scope.$on('guacUploadComplete', function uploadComplete(event, filename) { // Refresh filesystem, if it exists if ($scope.filesystem) ManagedFilesystem.refresh($scope.filesystem, $scope.filesystem.currentDirectory); }); }] }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/directives/guacFileTransferManager.js0100664 0001751 0001751 00000007145 12612226251 034737 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Directive which displays all active file transfers. */ angular.module('client').directive('guacFileTransferManager', [function guacFileTransferManager() { return { restrict: 'E', replace: true, scope: { /** * The client whose file transfers should be managed by this * directive. * * @type ManagerClient */ client : '=' }, templateUrl: 'app/client/templates/guacFileTransferManager.html', controller: ['$scope', '$injector', function guacFileTransferManagerController($scope, $injector) { // Required types var ManagedFileTransferState = $injector.get('ManagedFileTransferState'); /** * Determines whether the given file transfer state indicates an * in-progress transfer. * * @param {ManagedFileTransferState} transferState * The file transfer state to check. * * @returns {Boolean} * true if the given file transfer state indicates an in- * progress transfer, false otherwise. */ var isInProgress = function isInProgress(transferState) { switch (transferState.streamState) { // IDLE or OPEN file transfers are active case ManagedFileTransferState.StreamState.IDLE: case ManagedFileTransferState.StreamState.OPEN: return true; // All others are not active default: return false; } }; /** * Removes all file transfers which are not currently in-progress. */ $scope.clearCompletedTransfers = function clearCompletedTransfers() { // Nothing to clear if no client attached if (!$scope.client) return; // Remove completed uploads $scope.client.uploads = $scope.client.uploads.filter(function isUploadInProgress(upload) { return isInProgress(upload.transferState); }); // Remove completed downloads $scope.client.downloads = $scope.client.downloads.filter(function isDownloadInProgress(download) { return isInProgress(download.transferState); }); }; }] }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/directives/guacThumbnail.js0100664 0001751 0001751 00000012620 12612226251 032775 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive for displaying a Guacamole client as a non-interactive * thumbnail. */ angular.module('client').directive('guacThumbnail', [function guacThumbnail() { return { // Element only restrict: 'E', replace: true, scope: { /** * The client to display within this guacThumbnail directive. * * @type ManagedClient */ client : '=' }, templateUrl: 'app/client/templates/guacThumbnail.html', controller: ['$scope', '$injector', '$element', function guacThumbnailController($scope, $injector, $element) { // Required services var $window = $injector.get('$window'); /** * The optimal thumbnail width, in pixels. * * @type Number */ var THUMBNAIL_WIDTH = 320; /** * The optimal thumbnail height, in pixels. * * @type Number */ var THUMBNAIL_HEIGHT = 240; /** * The display of the current Guacamole client instance. * * @type Guacamole.Display */ var display = null; /** * The element associated with the display of the current * Guacamole client instance. * * @type Element */ var displayElement = null; /** * The element which must contain the Guacamole display element. * * @type Element */ var displayContainer = $element.find('.display')[0]; /** * The main containing element for the entire directive. * * @type Element */ var main = $element[0]; /** * Updates the scale of the attached Guacamole.Client based on current window * size and "auto-fit" setting. */ $scope.updateDisplayScale = function updateDisplayScale() { if (!display) return; // Fit within available area display.scale(Math.min( main.offsetWidth / Math.max(display.getWidth(), 1), main.offsetHeight / Math.max(display.getHeight(), 1) )); }; // Attach any given managed client $scope.$watch('client', function attachManagedClient(managedClient) { // Remove any existing display displayContainer.innerHTML = ""; // Only proceed if a client is given if (!managedClient) return; // Get Guacamole client instance var client = managedClient.client; // Attach possibly new display display = client.getDisplay(); // Add display element displayElement = display.getElement(); displayContainer.appendChild(displayElement); }); // Update scale when display is resized $scope.$watch('client.managedDisplay.size', function setDisplaySize(size) { var width; var height; // If no display size yet, assume optimal thumbnail size if (!size || size.width === 0 || size.height === 0) { width = THUMBNAIL_WIDTH; height = THUMBNAIL_HEIGHT; } // Otherwise, generate size that fits within thumbnail bounds else { var scale = Math.min(THUMBNAIL_WIDTH / size.width, THUMBNAIL_HEIGHT / size.height, 1); width = size.width * scale; height = size.height * scale; } // Generate dummy background image var thumbnail = document.createElement("canvas"); thumbnail.width = width; thumbnail.height = height; $scope.thumbnail = thumbnail.toDataURL("image/png"); // Init display scale $scope.$evalAsync($scope.updateDisplayScale); }); }] }; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/directives/guacFileTransfer.js0100664 0001751 0001751 00000020003 12612226251 033430 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Directive which displays an active file transfer, providing links for * downloads, if applicable. */ angular.module('client').directive('guacFileTransfer', [function guacFileTransfer() { return { restrict: 'E', replace: true, scope: { /** * The file transfer to display. * * @type ManagedFileUpload|ManagedFileDownload */ transfer : '=' }, templateUrl: 'app/client/templates/guacFileTransfer.html', controller: ['$scope', '$injector', function guacFileTransferController($scope, $injector) { // Required types var ManagedFileTransferState = $injector.get('ManagedFileTransferState'); /** * All upload error codes handled and passed off for translation. * Any error code not present in this list will be represented by * the "DEFAULT" translation. */ var UPLOAD_ERRORS = { 0x0100: true, 0x0201: true, 0x0202: true, 0x0203: true, 0x0204: true, 0x0205: true, 0x0301: true, 0x0303: true, 0x0308: true, 0x031D: true }; /** * Returns the unit string that is most appropriate for the * number of bytes transferred thus far - either 'gb', 'mb', 'kb', * or 'b'. * * @returns {String} * The unit string that is most appropriate for the number of * bytes transferred thus far. */ $scope.getProgressUnit = function getProgressUnit() { var bytes = $scope.transfer.progress; // Gigabytes if (bytes > 1000000000) return 'gb'; // Megabytes if (bytes > 1000000) return 'mb'; // Kilobytes if (bytes > 1000) return 'kb'; // Bytes return 'b'; }; /** * Returns the amount of data transferred thus far, in the units * returned by getProgressUnit(). * * @returns {Number} * The amount of data transferred thus far, in the units * returned by getProgressUnit(). */ $scope.getProgressValue = function getProgressValue() { var bytes = $scope.transfer.progress; if (!bytes) return bytes; // Convert bytes to necessary units switch ($scope.getProgressUnit()) { // Gigabytes case 'gb': return (bytes / 1000000000).toFixed(1); // Megabytes case 'mb': return (bytes / 1000000).toFixed(1); // Kilobytes case 'kb': return (bytes / 1000).toFixed(1); // Bytes case 'b': default: return bytes; } }; /** * Returns the percentage of bytes transferred thus far, if the * overall length of the file is known. * * @returns {Number} * The percentage of bytes transferred thus far, if the * overall length of the file is known. */ $scope.getPercentDone = function getPercentDone() { return $scope.transfer.progress / $scope.transfer.length * 100; }; /** * Determines whether the associated file transfer is in progress. * * @returns {Boolean} * true if the file transfer is in progress, false othherwise. */ $scope.isInProgress = function isInProgress() { // Not in progress if there is no transfer if (!$scope.transfer) return false; // Determine in-progress status based on stream state switch ($scope.transfer.transferState.streamState) { // IDLE or OPEN file transfers are active case ManagedFileTransferState.StreamState.IDLE: case ManagedFileTransferState.StreamState.OPEN: return true; // All others are not active default: return false; } }; /** * Returns whether the file associated with this file transfer can * be saved locally via a call to save(). * * @returns {Boolean} * true if a call to save() will result in the file being * saved, false otherwise. */ $scope.isSavable = function isSavable() { return !!$scope.transfer.blob; }; /** * Saves the downloaded file, if any. If this transfer is an upload * or the download is not yet complete, this function has no * effect. */ $scope.save = function save() { // Ignore if no blob exists if (!$scope.transfer.blob) return; // Save file saveAs($scope.transfer.blob, $scope.transfer.filename); }; /** * Returns whether an error has occurred. If an error has occurred, * the transfer is no longer active, and the text of the error can * be read from getErrorText(). * * @returns {Boolean} * true if an error has occurred during transfer, false * otherwise. */ $scope.hasError = function hasError() { return $scope.transfer.transferState.streamState === ManagedFileTransferState.StreamState.ERROR; }; /** * Returns the text of the current error as a translation string. * * @returns {String} * The name of the translation string containing the text * associated with the current error. */ $scope.getErrorText = function getErrorText() { // Determine translation name of error var status = $scope.transfer.transferState.statusCode; var errorName = (status in UPLOAD_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; // Return translation string return 'CLIENT.ERROR_UPLOAD_' + errorName; }; }] // end file transfer controller }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/directives/guacViewport.js0100664 0001751 0001751 00000010210 12612226251 032662 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which provides a fullscreen environment for its content. */ angular.module('client').directive('guacViewport', [function guacViewport() { return { // Element only restrict: 'E', scope: {}, transclude: true, templateUrl: 'app/client/templates/guacViewport.html', controller: ['$scope', '$injector', '$element', function guacViewportController($scope, $injector, $element) { // Required services var $window = $injector.get('$window'); var $document = $injector.get('$document'); /** * The fullscreen container element. * * @type Element */ var element = $element.find('.viewport')[0]; /** * The main document object. * * @type Document */ var document = $document[0]; /** * The current adjusted height of the viewport element, if any. * * @type Number */ var currentAdjustedHeight = null; /** * Resizes the container element inside the guacViewport such that * it exactly fits within the visible area, even if the browser has * been scrolled. */ var fitVisibleArea = function fitVisibleArea() { // Pull scroll properties var scrollLeft = document.body.scrollLeft; var scrollTop = document.body.scrollTop; var scrollWidth = document.body.scrollWidth; var scrollHeight = document.body.scrollHeight; // Calculate new height var adjustedHeight = scrollHeight - scrollTop; // Only update if not in response to our own call to scrollTo() if (scrollLeft !== scrollWidth && scrollTop !== scrollHeight && currentAdjustedHeight !== adjustedHeight) { // Adjust element to fit exactly within visible area element.style.height = adjustedHeight + 'px'; currentAdjustedHeight = adjustedHeight; // Scroll to bottom $window.scrollTo(scrollWidth, scrollHeight); } // Manually attempt scroll if height has not been adjusted else if (adjustedHeight === 0) $window.scrollTo(scrollWidth, scrollHeight); }; // Fit container within visible region when window scrolls $window.addEventListener('scroll', fitVisibleArea); // Poll every 10ms, in case scroll event does not fire var pollArea = $window.setInterval(fitVisibleArea, 10); // Clean up on destruction $scope.$on('$destroy', function destroyViewport() { $window.removeEventListener('scroll', fitVisibleArea); $window.clearInterval(pollArea); }); }] }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/directives/guacClient.js0100664 0001751 0001751 00000041106 12626164604 032301 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive for the guacamole client. */ angular.module('client').directive('guacClient', [function guacClient() { return { // Element only restrict: 'E', replace: true, scope: { /** * The client to display within this guacClient directive. * * @type ManagedClient */ client : '=' }, templateUrl: 'app/client/templates/guacClient.html', controller: ['$scope', '$injector', '$element', function guacClientController($scope, $injector, $element) { // Required types var ManagedClient = $injector.get('ManagedClient'); // Required services var $window = $injector.get('$window'); /** * Whether the local, hardware mouse cursor is in use. * * @type Boolean */ var localCursor = false; /** * The current Guacamole client instance. * * @type Guacamole.Client */ var client = null; /** * The display of the current Guacamole client instance. * * @type Guacamole.Display */ var display = null; /** * The element associated with the display of the current * Guacamole client instance. * * @type Element */ var displayElement = null; /** * The element which must contain the Guacamole display element. * * @type Element */ var displayContainer = $element.find('.display')[0]; /** * The main containing element for the entire directive. * * @type Element */ var main = $element[0]; /** * The element which functions as a detector for size changes. * * @type Element */ var resizeSensor = $element.find('.resize-sensor')[0]; /** * Guacamole mouse event object, wrapped around the main client * display. * * @type Guacamole.Mouse */ var mouse = new Guacamole.Mouse(displayContainer); /** * Guacamole absolute mouse emulation object, wrapped around the * main client display. * * @type Guacamole.Mouse.Touchscreen */ var touchScreen = new Guacamole.Mouse.Touchscreen(displayContainer); /** * Guacamole relative mouse emulation object, wrapped around the * main client display. * * @type Guacamole.Mouse.Touchpad */ var touchPad = new Guacamole.Mouse.Touchpad(displayContainer); /** * Updates the scale of the attached Guacamole.Client based on current window * size and "auto-fit" setting. */ var updateDisplayScale = function updateDisplayScale() { if (!display) return; // Calculate scale to fit screen $scope.client.clientProperties.minScale = Math.min( main.offsetWidth / Math.max(display.getWidth(), 1), main.offsetHeight / Math.max(display.getHeight(), 1) ); // Calculate appropriate maximum zoom level $scope.client.clientProperties.maxScale = Math.max($scope.client.clientProperties.minScale, 3); // Clamp zoom level, maintain auto-fit if (display.getScale() < $scope.client.clientProperties.minScale || $scope.client.clientProperties.autoFit) $scope.client.clientProperties.scale = $scope.client.clientProperties.minScale; else if (display.getScale() > $scope.client.clientProperties.maxScale) $scope.client.clientProperties.scale = $scope.client.clientProperties.maxScale; }; /** * Scrolls the client view such that the mouse cursor is visible. * * @param {Guacamole.Mouse.State} mouseState The current mouse * state. */ var scrollToMouse = function scrollToMouse(mouseState) { // Determine mouse position within view var mouse_view_x = mouseState.x + displayContainer.offsetLeft - main.scrollLeft; var mouse_view_y = mouseState.y + displayContainer.offsetTop - main.scrollTop; // Determine viewport dimensions var view_width = main.offsetWidth; var view_height = main.offsetHeight; // Determine scroll amounts based on mouse position relative to document var scroll_amount_x; if (mouse_view_x > view_width) scroll_amount_x = mouse_view_x - view_width; else if (mouse_view_x < 0) scroll_amount_x = mouse_view_x; else scroll_amount_x = 0; var scroll_amount_y; if (mouse_view_y > view_height) scroll_amount_y = mouse_view_y - view_height; else if (mouse_view_y < 0) scroll_amount_y = mouse_view_y; else scroll_amount_y = 0; // Scroll (if necessary) to keep mouse on screen. main.scrollLeft += scroll_amount_x; main.scrollTop += scroll_amount_y; }; /** * Sends the given mouse state to the current client. * * @param {Guacamole.Mouse.State} mouseState The mouse state to * send. */ var sendScaledMouseState = function sendScaledMouseState(mouseState) { // Scale event by current scale var scaledState = new Guacamole.Mouse.State( mouseState.x / display.getScale(), mouseState.y / display.getScale(), mouseState.left, mouseState.middle, mouseState.right, mouseState.up, mouseState.down); // Send mouse event client.sendMouseState(scaledState); }; // Attach any given managed client $scope.$watch('client', function attachManagedClient(managedClient) { // Remove any existing display displayContainer.innerHTML = ""; // Only proceed if a client is given if (!managedClient) return; // Get Guacamole client instance client = managedClient.client; // Attach possibly new display display = client.getDisplay(); display.scale($scope.client.clientProperties.scale); // Add display element displayElement = display.getElement(); displayContainer.appendChild(displayElement); // Do nothing when the display element is clicked on display.getElement().onclick = function(e) { e.preventDefault(); return false; }; }); // Update actual view scrollLeft when scroll properties change $scope.$watch('client.clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) { main.scrollLeft = scrollLeft; $scope.client.clientProperties.scrollLeft = main.scrollLeft; }); // Update actual view scrollTop when scroll properties change $scope.$watch('client.clientProperties.scrollTop', function scrollTopChanged(scrollTop) { main.scrollTop = scrollTop; $scope.client.clientProperties.scrollTop = main.scrollTop; }); // Update scale when display is resized $scope.$watch('client.managedDisplay.size', function setDisplaySize() { $scope.$evalAsync(updateDisplayScale); }); // Keep local cursor up-to-date $scope.$watch('client.managedDisplay.cursor', function setCursor(cursor) { if (cursor) localCursor = mouse.setCursor(cursor.canvas, cursor.x, cursor.y); }); // Swap mouse emulation modes depending on absolute mode flag $scope.$watch('client.clientProperties.emulateAbsoluteMouse', function(emulateAbsoluteMouse) { if (!client || !display) return; var handleMouseState = function handleMouseState(mouseState) { // Ensure software cursor is shown display.showCursor(true); // Send mouse state, ensure cursor is visible scrollToMouse(mouseState); sendScaledMouseState(mouseState); }; var newMode, oldMode; // Switch to touchscreen if absolute if (emulateAbsoluteMouse) { newMode = touchScreen; oldMode = touchPad; } // Switch to touchpad if not absolute (relative) else { newMode = touchPad; oldMode = touchScreen; } // Set applicable mouse emulation object, unset the old one if (newMode) { if (oldMode) { oldMode.onmousedown = oldMode.onmouseup = oldMode.onmousemove = null; newMode.currentState.x = oldMode.currentState.x; newMode.currentState.y = oldMode.currentState.y; } newMode.onmousedown = newMode.onmouseup = newMode.onmousemove = handleMouseState; } }); // Adjust scale if modified externally $scope.$watch('client.clientProperties.scale', function changeScale(scale) { // Fix scale within limits scale = Math.max(scale, $scope.client.clientProperties.minScale); scale = Math.min(scale, $scope.client.clientProperties.maxScale); // If at minimum zoom level, hide scroll bars if (scale === $scope.client.clientProperties.minScale) main.style.overflow = "hidden"; // If not at minimum zoom level, show scroll bars else main.style.overflow = "auto"; // Apply scale if client attached if (display) display.scale(scale); if (scale !== $scope.client.clientProperties.scale) $scope.client.clientProperties.scale = scale; }); // If autofit is set, the scale should be set to the minimum scale, filling the screen $scope.$watch('client.clientProperties.autoFit', function changeAutoFit(autoFit) { if(autoFit) $scope.client.clientProperties.scale = $scope.client.clientProperties.minScale; }); // If the element is resized, attempt to resize client $scope.mainElementResized = function mainElementResized() { // Send new display size, if changed if (client && display) { var pixelDensity = $window.devicePixelRatio || 1; var width = main.offsetWidth * pixelDensity; var height = main.offsetHeight * pixelDensity; if (display.getWidth() !== width || display.getHeight() !== height) client.sendSize(width, height); } $scope.$evalAsync(updateDisplayScale); }; // Watch for changes to mouse emulation mode // Send all received mouse events to the client mouse.onmousedown = mouse.onmouseup = mouse.onmousemove = function(mouseState) { if (!client || !display) return; // Send mouse state, show cursor if necessary display.showCursor(!localCursor); sendScaledMouseState(mouseState); }; // Hide software cursor when mouse leaves display mouse.onmouseout = function() { if (!display) return; display.showCursor(false); }; // Update remote clipboard if local clipboard changes $scope.$on('guacClipboard', function onClipboard(event, mimetype, data) { if (client) client.setClipboard(data); }); // Translate local keydown events to remote keydown events if keyboard is enabled $scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) { if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) { client.sendKeyEvent(1, keysym); event.preventDefault(); } }); // Translate local keyup events to remote keyup events if keyboard is enabled $scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) { if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) { client.sendKeyEvent(0, keysym); event.preventDefault(); } }); // Universally handle all synthetic keydown events $scope.$on('guacSyntheticKeydown', function syntheticKeydownListener(event, keysym) { client.sendKeyEvent(1, keysym); }); // Universally handle all synthetic keyup events $scope.$on('guacSyntheticKeyup', function syntheticKeyupListener(event, keysym) { client.sendKeyEvent(0, keysym); }); /** * Ignores the given event. * * @param {Event} e The event to ignore. */ function ignoreEvent(e) { e.preventDefault(); e.stopPropagation(); } // Handle and ignore dragenter/dragover displayContainer.addEventListener("dragenter", ignoreEvent, false); displayContainer.addEventListener("dragover", ignoreEvent, false); // File drop event handler displayContainer.addEventListener("drop", function(e) { e.preventDefault(); e.stopPropagation(); // Ignore file drops if no attached client if (!$scope.client) return; // Upload each file var files = e.dataTransfer.files; for (var i=0; i * { -ms-flex: 0 0 auto; -moz-box-flex: 0; -webkit-box-flex: 0; -webkit-flex: 0 0 auto; flex: 0 0 auto; } .menu-section h3 { margin: 0; padding: 0; padding-bottom: 1em; } .menu-section ~ .menu-section h3 { padding-top: 1em; } .menu, .menu.closed { left: -480px; opacity: 0; } .menu.open { left: 0px; opacity: 1; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/styles/filesystem-menu.css0100664 0001751 0001751 00000004507 12612226251 032703 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #filesystem-menu .header h2 { font-size: 1em; font-weight: normal; padding-top: 0; padding-bottom: 0; } #filesystem-menu .header { -ms-flex-align: center; -moz-box-align: center; -webkit-box-align: center; -webkit-align-items: center; align-items: center; } #filesystem-menu .menu-body { padding: 0.25em; } #filesystem-menu .header.breadcrumbs { display: block; background: rgba(0,0,0,0.0125); border-bottom: 1px solid rgba(0,0,0,0.05); box-shadow: none; margin-top: 0; border-top: none; } #filesystem-menu .header.breadcrumbs .breadcrumb { display: inline-block; padding: 0.5em; font-size: 0.8em; font-weight: bold; } #filesystem-menu .header.breadcrumbs .breadcrumb:hover { background-color: #CDA; cursor: pointer; } #filesystem-menu .header.breadcrumbs .breadcrumb.root { background-size: 1.5em 1.5em; -moz-background-size: 1.5em 1.5em; -webkit-background-size: 1.5em 1.5em; -khtml-background-size: 1.5em 1.5em; background-repeat: no-repeat; background-position: center center; background-image: url('images/drive.png'); width: 2em; height: 2em; padding: 0; vertical-align: middle; }guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/styles/transfer.css0100664 0001751 0001751 00000006131 12612226251 031374 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .transfer { display: table-row; } .transfer .transfer-status { display: table-cell; padding: 0.25em; position: relative; } .transfer .text { display: table-cell; text-align: right; padding: 0.25em } .transfer .filename { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; position: relative; font-family: monospace; font-weight: bold; padding: 0.125em; } @keyframes transfer-progress { from {background-position: 0px 0px;} to {background-position: 64px 0px;} } @-webkit-keyframes transfer-progress { from {background-position: 0px 0px;} to {background-position: 64px 0px;} } .transfer .progress { width: 100%; padding: 0.25em; position: absolute; top: 0; left: 0; bottom: 0; opacity: 0.25; } .transfer.in-progress .progress { background-color: #EEE; background-image: url('images/progress.png'); background-size: 16px 16px; -moz-background-size: 16px 16px; -webkit-background-size: 16px 16px; -khtml-background-size: 16px 16px; animation-name: transfer-progress; animation-duration: 2s; animation-timing-function: linear; animation-iteration-count: infinite; -webkit-animation-name: transfer-progress; -webkit-animation-duration: 2s; -webkit-animation-timing-function: linear; -webkit-animation-iteration-count: infinite; } .transfer .progress .bar { display: none; background: #A3D655; position: absolute; top: 0; left: 0; height: 100%; width: 0; } .transfer.in-progress .progress .bar { display: initial; } .transfer.savable { cursor: pointer; } .transfer.savable .filename { color: blue; text-decoration: underline; } .transfer.error { background: #FDD; } .transfer.error .text, .transfer.error .progress .bar { display: none; } .transfer .error-text { display: none; } .transfer.error .error-text { display: block; margin: 0; margin-top: 0.5em; width: 100%; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/styles/transfer-manager.css0100664 0001751 0001751 00000003034 12612226251 033003 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .transfer-manager { background: white; } .transfer-manager .header h2 { font-size: 1em; padding-top: 0; padding-bottom: 0; } .transfer-manager .header { margin: 0; -ms-flex-align: center; -moz-box-align: center; -webkit-box-align: center; -webkit-align-items: center; align-items: center; } .transfer-manager .transfers { display: table; padding: 0.25em; width: 100%; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/styles/file-transfer-dialog.css0100664 0001751 0001751 00000005603 12645510255 033557 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #file-transfer-dialog { position: absolute; right: 0; bottom: 0; z-index: 20; font-size: 0.8em; width: 4in; max-width: 100%; max-height: 3in; } #file-transfer-dialog .transfer-manager { /* IE10 */ display: -ms-flexbox; -ms-flex-align: stretch; -ms-flex-direction: column; /* Ancient Mozilla */ display: -moz-box; -moz-box-align: stretch; -moz-box-orient: vertical; /* Ancient WebKit */ display: -webkit-box; -webkit-box-align: stretch; -webkit-box-orient: vertical; /* Old WebKit */ display: -webkit-flex; -webkit-align-items: stretch; -webkit-flex-direction: column; /* W3C */ display: flex; align-items: stretch; flex-direction: column; max-width: inherit; max-height: inherit; border: 1px solid rgba(0, 0, 0, 0.5); box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.25); } #file-transfer-dialog .transfer-manager .header { -ms-flex: 0 0 auto; -moz-box-flex: 0; -webkit-box-flex: 0; -webkit-flex: 0 0 auto; flex: 0 0 auto; } #file-transfer-dialog .transfer-manager .transfer-manager-body { -ms-flex: 1 1 auto; -moz-box-flex: 1; -webkit-box-flex: 1; -webkit-flex: 1 1 auto; flex: 1 1 auto; overflow: auto; } /* * Shrink maximum height if viewport is too small for default 3in dialog. */ @media all and (max-height: 3in) { #file-transfer-dialog { max-height: 1.5in; } } /* * If viewport is too small for even the 1.5in dialog, fit all available space. */ @media all and (max-height: 1.5in) { #file-transfer-dialog { height: 100%; } #file-transfer-dialog .transfer-manager { position: absolute; left: 0.5em; top: 0.5em; right: 0.5em; bottom: 0.5em; } } guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/styles/client.css0100664 0001751 0001751 00000005553 12612226251 031035 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ body.client { background: black; padding: 0; margin: 0; overflow: hidden; } #preload { visibility: hidden; position: absolute; left: 0; right: 0; width: 0; height: 0; overflow: hidden; } .client-view { position: absolute; top: 0; left: 0; width: 100%; height: 100%; font-size: 0px; } .client-view-content { /* IE10 */ display: -ms-flexbox; -ms-flex-align: stretch; -ms-flex-direction: column; -ms-flex-pack: end; /* Ancient Mozilla */ display: -moz-box; -moz-box-align: stretch; -moz-box-orient: vertical; -moz-box-pack: end; /* Ancient WebKit */ display: -webkit-box; -webkit-box-align: stretch; -webkit-box-orient: vertical; -webkit-box-pack: end; /* Old WebKit */ display: -webkit-flex; -webkit-align-items: stretch; -webkit-flex-direction: column; -webkit-flex-pack: end; /* W3C */ display: flex; align-items: stretch; flex-direction: column; flex-pack: end; width: 100%; height: 100%; font-size: 12pt; } .client-view .client-body { -ms-flex: 1 1 auto; -moz-box-flex: 1; -webkit-box-flex: 1; -webkit-flex: 1 1 auto; flex: 1 1 auto; position: relative; } .client-view .client-bottom { -ms-flex: 0 0 auto; -moz-box-flex: 0; -webkit-box-flex: 0; -webkit-flex: 0 0 auto; flex: 0 0 auto; } .client-view .client-body .main { position: absolute; left: 0; top: 0; right: 0; bottom: 0; width: auto; height: auto; } .client .user-menu .options li a.disconnect { background-repeat: no-repeat; background-size: 1em; background-position: 0.75em center; padding-left: 2.5em; background-image: url('images/x.png'); } guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/styles/thumbnail-display.css0100664 0001751 0001751 00000002530 12612226251 033175 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ div.thumbnail-main { overflow: hidden; width: 100%; height: 100%; position: relative; font-size: 0px; } .thumbnail-main img { max-width: 100%; } .thumbnail-main .display { position: absolute; pointer-events: none; }guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/styles/display.css0100664 0001751 0001751 00000003443 12626164604 031230 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .software-cursor { cursor: url('images/mouse/blank.gif'),url('images/mouse/blank.cur'),default; overflow: hidden; cursor: none; } .guac-error .software-cursor { cursor: default; } div.main { overflow: auto; width: 100%; height: 100%; position: relative; font-size: 0px; } div.displayOuter { height: 100%; width: 100%; position: absolute; left: 0; top: 0; display: table; } div.displayMiddle { width: 100%; display: table-cell; vertical-align: middle; text-align: center; } div.display { display: inline-block; } div.display * { position: relative; } div.display > * { margin-left: auto; margin-right: auto; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/styles/keyboard.css0100664 0001751 0001751 00000002434 12612226251 031352 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .keyboard-container { text-align: center; width: 100%; margin: 0; padding: 0; border-top: 1px solid black; background: #222; opacity: 0.85; z-index: 1; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/styles/file-browser.css0100664 0001751 0001751 00000003421 12612226251 032147 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /* Hide directory contents by default */ .file-browser .directory > .children { padding-left: 1em; display: none; } .file-browser .list-item .caption { white-space: nowrap; border: 1px solid transparent; } .file-browser .list-item.focused .caption { border: 1px dotted rgba(0, 0, 0, 0.5); background: rgba(204, 221, 170, 0.5); } /* Directory / file icons */ .file-browser .normal-file > .caption .icon { background-image: url('images/file.png'); } .file-browser .directory > .caption .icon { background-image: url('images/folder-closed.png'); } .file-browser .directory.previous > .caption .icon { background-image: url('images/folder-up.png'); } guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/styles/viewport.css0100664 0001751 0001751 00000002331 12612226251 031425 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .viewport { position: absolute; bottom: 0; left: 0; width: 100%; height: 100%; overflow: hidden; }guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/styles/guac-menu.css0100664 0001751 0001751 00000010204 12612226251 031425 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #guac-menu .content { padding: 0; margin: 0; /* IE10 */ display: -ms-flexbox; -ms-flex-align: stretch; -ms-flex-direction: column; /* Ancient Mozilla */ display: -moz-box; -moz-box-align: stretch; -moz-box-orient: vertical; /* Ancient WebKit */ display: -webkit-box; -webkit-box-align: stretch; -webkit-box-orient: vertical; /* Old WebKit */ display: -webkit-flex; -webkit-align-items: stretch; -webkit-flex-direction: column; /* W3C */ display: flex; align-items: stretch; flex-direction: column; } #guac-menu .content > * { margin: 0; -ms-flex: 0 0 auto; -moz-box-flex: 0; -webkit-box-flex: 0; -webkit-flex: 0 0 auto; flex: 0 0 auto; } #guac-menu .content > * + * { margin-top: 1em; } #guac-menu #clipboard-settings textarea { width: 100%; border: 1px solid #AAA; -moz-border-radius: 0.25em; -webkit-border-radius: 0.25em; -khtml-border-radius: 0.25em; border-radius: 0.25em; white-space: pre; display: block; font-size: 1em; } #guac-menu #mouse-settings .choice { text-align: center; } #guac-menu #mouse-settings .choice .figure { display: inline-block; vertical-align: middle; width: 75%; max-width: 320px; } #guac-menu #keyboard-settings .caption { font-size: 0.9em; margin-left: 2em; margin-right: 2em; } #guac-menu #mouse-settings .figure .caption { text-align: center; font-size: 0.9em; } #guac-menu #mouse-settings .figure img { display: block; width: 100%; max-width: 320px; margin: 1em auto; } #guac-menu #keyboard-settings .figure { float: right; max-width: 30%; margin: 1em; } #guac-menu #keyboard-settings .figure img { max-width: 100%; } #guac-menu #zoom-settings { text-align: center; } #guac-menu #zoom-out, #guac-menu #zoom-in, #guac-menu #zoom-state { display: inline-block; vertical-align: middle; } #guac-menu #zoom-out, #guac-menu #zoom-in { max-width: 3em; border: 1px solid rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.1); border-radius: 2em; margin: 0.5em; cursor: pointer; } #guac-menu #zoom-out img, #guac-menu #zoom-in img { max-width: 100%; opacity: 0.5; } #guac-menu #zoom-out:hover, #guac-menu #zoom-in:hover { border: 1px solid rgba(0, 0, 0, 1); background: #CDA; } #guac-menu #zoom-out:hover img, #guac-menu #zoom-in:hover img { opacity: 1; } #guac-menu #zoom-state { font-size: 2em; } #guac-menu #devices .device { padding: 1em; border: 1px solid rgba(0, 0, 0, 0.125); background: rgba(0, 0, 0, 0.04); padding-left: 3.5em; background-size: 1.5em 1.5em; -moz-background-size: 1.5em 1.5em; -webkit-background-size: 1.5em 1.5em; -khtml-background-size: 1.5em 1.5em; background-repeat: no-repeat; background-position: 1em center; } #guac-menu #devices .device:hover { cursor: pointer; border-color: black; } #guac-menu #devices .device.filesystem { background-image: url('images/drive.png'); } guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/controllers/clientController.js0100664 0001751 0001751 00000057467 12645510255 033771 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The controller for the page used to connect to a connection or balancing group. */ angular.module('client').controller('clientController', ['$scope', '$routeParams', '$injector', function clientController($scope, $routeParams, $injector) { // Required types var ManagedClient = $injector.get('ManagedClient'); var ManagedClientState = $injector.get('ManagedClientState'); var ManagedFilesystem = $injector.get('ManagedFilesystem'); var ScrollState = $injector.get('ScrollState'); // Required services var $location = $injector.get('$location'); var authenticationService = $injector.get('authenticationService'); var guacClientManager = $injector.get('guacClientManager'); var guacNotification = $injector.get('guacNotification'); var preferenceService = $injector.get('preferenceService'); var userPageService = $injector.get('userPageService'); /** * The minimum number of pixels a drag gesture must move to result in the * menu being shown or hidden. * * @type Number */ var MENU_DRAG_DELTA = 64; /** * The maximum X location of the start of a drag gesture for that gesture * to potentially show the menu. * * @type Number */ var MENU_DRAG_MARGIN = 64; /** * When showing or hiding the menu via a drag gesture, the maximum number * of pixels the touch can move vertically and still affect the menu. * * @type Number */ var MENU_DRAG_VERTICAL_TOLERANCE = 10; /* * In order to open the guacamole menu, we need to hit ctrl-alt-shift. There are * several possible keysysms for each key. */ var SHIFT_KEYS = {0xFFE1 : true, 0xFFE2 : true}, ALT_KEYS = {0xFFE9 : true, 0xFFEA : true, 0xFE03 : true, 0xFFE7 : true, 0xFFE8 : true}, CTRL_KEYS = {0xFFE3 : true, 0xFFE4 : true}, MENU_KEYS = angular.extend({}, SHIFT_KEYS, ALT_KEYS, CTRL_KEYS); /** * All client error codes handled and passed off for translation. Any error * code not present in this list will be represented by the "DEFAULT" * translation. */ var CLIENT_ERRORS = { 0x0201: true, 0x0202: true, 0x0203: true, 0x0205: true, 0x0301: true, 0x0303: true, 0x0308: true, 0x031D: true }; /** * All error codes for which automatic reconnection is appropriate when a * client error occurs. */ var CLIENT_AUTO_RECONNECT = { 0x0200: true, 0x0202: true, 0x0203: true, 0x0301: true, 0x0308: true }; /** * All tunnel error codes handled and passed off for translation. Any error * code not present in this list will be represented by the "DEFAULT" * translation. */ var TUNNEL_ERRORS = { 0x0201: true, 0x0202: true, 0x0203: true, 0x0204: true, 0x0205: true, 0x0301: true, 0x0303: true, 0x0308: true, 0x031D: true }; /** * All error codes for which automatic reconnection is appropriate when a * tunnel error occurs. */ var TUNNEL_AUTO_RECONNECT = { 0x0200: true, 0x0202: true, 0x0203: true, 0x0308: true }; /** * Action which logs out from Guacamole entirely. */ var LOGOUT_ACTION = { name : "CLIENT.ACTION_LOGOUT", className : "logout button", callback : function logoutCallback() { authenticationService.logout()['finally'](function logoutComplete() { $location.url('/'); }); } }; /** * Action which returns the user to the home screen. If the home page has * not yet been determined, this will be null. */ var NAVIGATE_HOME_ACTION = null; // Assign home page action once user's home page has been determined userPageService.getHomePage() .then(function homePageRetrieved(homePage) { // Define home action only if different from current location if ($location.path() !== homePage.url) { NAVIGATE_HOME_ACTION = { name : "CLIENT.ACTION_NAVIGATE_HOME", className : "home button", callback : function navigateHomeCallback() { $location.url(homePage.url); } }; } }); /** * Action which replaces the current client with a newly-connected client. */ var RECONNECT_ACTION = { name : "CLIENT.ACTION_RECONNECT", className : "reconnect button", callback : function reconnectCallback() { $scope.client = guacClientManager.replaceManagedClient($routeParams.id, $routeParams.params); guacNotification.showStatus(false); } }; /** * The reconnect countdown to display if an error or status warrants an * automatic, timed reconnect. */ var RECONNECT_COUNTDOWN = { text: "CLIENT.TEXT_RECONNECT_COUNTDOWN", callback: RECONNECT_ACTION.callback, remaining: 15 }; /** * Menu-specific properties. */ $scope.menu = { /** * Whether the menu is currently shown. * * @type Boolean */ shown : false, /** * Whether the Guacamole display should be scaled to fit the browser * window. * * @type Boolean */ autoFit : true, /** * The currently selected input method. This may be any of the values * defined within preferenceService.inputMethods. * * @type String */ inputMethod : preferenceService.preferences.inputMethod, /** * The current scroll state of the menu. * * @type ScrollState */ scrollState : new ScrollState() }; // Convenience method for closing the menu $scope.closeMenu = function closeMenu() { $scope.menu.shown = false; }; // Update the model when clipboard data received from client $scope.$on('guacClientClipboard', function clientClipboardListener(event, client, mimetype, clipboardData) { $scope.clipboardData = clipboardData; }); /** * The client which should be attached to the client UI. * * @type ManagedClient */ $scope.client = guacClientManager.getManagedClient($routeParams.id, $routeParams.params); var keysCurrentlyPressed = {}; /* * Check to see if all currently pressed keys are in the set of menu keys. */ function checkMenuModeActive() { for(var keysym in keysCurrentlyPressed) { if(!MENU_KEYS[keysym]) { return false; } } return true; } // Hide menu when the user swipes from the right $scope.menuDrag = function menuDrag(inProgress, startX, startY, currentX, currentY, deltaX, deltaY) { // Hide menu if swipe gesture is detected if (Math.abs(currentY - startY) < MENU_DRAG_VERTICAL_TOLERANCE && startX - currentX >= MENU_DRAG_DELTA) $scope.menu.shown = false; // Scroll menu by default else { $scope.menu.scrollState.left -= deltaX; $scope.menu.scrollState.top -= deltaY; } return false; }; // Update menu or client based on dragging gestures $scope.clientDrag = function clientDrag(inProgress, startX, startY, currentX, currentY, deltaX, deltaY) { // Show menu if the user swipes from the left if (startX <= MENU_DRAG_MARGIN) { if (Math.abs(currentY - startY) < MENU_DRAG_VERTICAL_TOLERANCE && currentX - startX >= MENU_DRAG_DELTA) $scope.menu.shown = true; } // Scroll display if absolute mouse is in use else if ($scope.client.clientProperties.emulateAbsoluteMouse) { $scope.client.clientProperties.scrollLeft -= deltaX; $scope.client.clientProperties.scrollTop -= deltaY; } return false; }; /** * If a pinch gesture is in progress, the scale of the client display when * the pinch gesture began. * * @type Number */ var initialScale = null; /** * If a pinch gesture is in progress, the X coordinate of the point on the * client display that was centered within the pinch at the time the * gesture began. * * @type Number */ var initialCenterX = 0; /** * If a pinch gesture is in progress, the Y coordinate of the point on the * client display that was centered within the pinch at the time the * gesture began. * * @type Number */ var initialCenterY = 0; // Zoom and pan client via pinch gestures $scope.clientPinch = function clientPinch(inProgress, startLength, currentLength, centerX, centerY) { // Do not handle pinch gestures while relative mouse is in use if (!$scope.client.clientProperties.emulateAbsoluteMouse) return false; // Stop gesture if not in progress if (!inProgress) { initialScale = null; return false; } // Set initial scale if gesture has just started if (!initialScale) { initialScale = $scope.client.clientProperties.scale; initialCenterX = (centerX + $scope.client.clientProperties.scrollLeft) / initialScale; initialCenterY = (centerY + $scope.client.clientProperties.scrollTop) / initialScale; } // Determine new scale absolutely var currentScale = initialScale * currentLength / startLength; // Fix scale within limits - scroll will be miscalculated otherwise currentScale = Math.max(currentScale, $scope.client.clientProperties.minScale); currentScale = Math.min(currentScale, $scope.client.clientProperties.maxScale); // Update scale based on pinch distance $scope.menu.autoFit = false; $scope.client.clientProperties.autoFit = false; $scope.client.clientProperties.scale = currentScale; // Scroll display to keep original pinch location centered within current pinch $scope.client.clientProperties.scrollLeft = initialCenterX * currentScale - centerX; $scope.client.clientProperties.scrollTop = initialCenterY * currentScale - centerY; return false; }; // Show/hide UI elements depending on input method $scope.$watch('menu.inputMethod', function setInputMethod(inputMethod) { // Show input methods only if selected $scope.showOSK = (inputMethod === 'osk'); $scope.showTextInput = (inputMethod === 'text'); }); $scope.$watch('menu.shown', function menuVisibilityChanged(menuShown, menuShownPreviousState) { // Send clipboard data if menu is hidden if (!menuShown && menuShownPreviousState) $scope.$broadcast('guacClipboard', 'text/plain', $scope.client.clipboardData); // Disable client keyboard if the menu is shown $scope.client.clientProperties.keyboardEnabled = !menuShown; }); $scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) { keysCurrentlyPressed[keysym] = true; /* * If only menu keys are pressed, and we have one keysym from each group, * and one of the keys is being released, show the menu. */ if(checkMenuModeActive()) { var currentKeysPressedKeys = Object.keys(keysCurrentlyPressed); // Check that there is a key pressed for each of the required key classes if(!_.isEmpty(_.pick(SHIFT_KEYS, currentKeysPressedKeys)) && !_.isEmpty(_.pick(ALT_KEYS, currentKeysPressedKeys)) && !_.isEmpty(_.pick(CTRL_KEYS, currentKeysPressedKeys)) ) { // Don't send this key event through to the client event.preventDefault(); // Reset the keys pressed keysCurrentlyPressed = {}; keyboard.reset(); // Toggle the menu $scope.$apply(function() { $scope.menu.shown = !$scope.menu.shown; }); } } }); // Listen for broadcasted keyup events and fire the appropriate listeners $scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) { delete keysCurrentlyPressed[keysym]; }); // Update page title when client name is received $scope.$watch('client.name', function clientNameChanged(name) { $scope.page.title = name; }); /** * Displays a notification at the end of a Guacamole connection, whether * that connection is ending normally or due to an error. As the end of * a Guacamole connection may be due to changes in authentication status, * this will also implicitly peform a re-authentication attempt to check * for such changes, possibly resulting in auth-related events like * guacInvalidCredentials. * * @param {Notification|Boolean|Object} status * The status notification to show, as would be accepted by * guacNotification.showStatus(). */ var notifyConnectionClosed = function notifyConnectionClosed(status) { // Re-authenticate to verify auth status at end of connection authenticationService.updateCurrentToken($location.search()) // Show the requested status once the authentication check has finished ['finally'](function authenticationCheckComplete() { guacNotification.showStatus(status); }); }; // Show status dialog when connection status changes $scope.$watch('client.clientState.connectionState', function clientStateChanged(connectionState) { // Hide any existing status guacNotification.showStatus(false); // Do not display status if status not known if (!connectionState) return; // Build array of available actions var actions; if (NAVIGATE_HOME_ACTION) actions = [ NAVIGATE_HOME_ACTION, RECONNECT_ACTION, LOGOUT_ACTION ]; else actions = [ RECONNECT_ACTION, LOGOUT_ACTION ]; // Get any associated status code var status = $scope.client.clientState.statusCode; // Connecting if (connectionState === ManagedClientState.ConnectionState.CONNECTING || connectionState === ManagedClientState.ConnectionState.WAITING) { guacNotification.showStatus({ title: "CLIENT.DIALOG_HEADER_CONNECTING", text: "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase() }); } // Client error else if (connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) { // Determine translation name of error var errorName = (status in CLIENT_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; // Determine whether the reconnect countdown applies var countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null; // Show error status notifyConnectionClosed({ className : "error", title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", text : "CLIENT.ERROR_CLIENT_" + errorName, countdown : countdown, actions : actions }); } // Tunnel error else if (connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR) { // Determine translation name of error var errorName = (status in TUNNEL_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; // Determine whether the reconnect countdown applies var countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null; // Show error status notifyConnectionClosed({ className : "error", title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", text : "CLIENT.ERROR_TUNNEL_" + errorName, countdown : countdown, actions : actions }); } // Disconnected else if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED) { notifyConnectionClosed({ title : "CLIENT.DIALOG_HEADER_DISCONNECTED", text : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase(), actions : actions }); } // Hide status for all other states else guacNotification.showStatus(false); }); $scope.formattedScale = function formattedScale() { return Math.round($scope.client.clientProperties.scale * 100); }; $scope.zoomIn = function zoomIn() { $scope.menu.autoFit = false; $scope.client.clientProperties.autoFit = false; $scope.client.clientProperties.scale += 0.1; }; $scope.zoomOut = function zoomOut() { $scope.client.clientProperties.autoFit = false; $scope.client.clientProperties.scale -= 0.1; }; $scope.changeAutoFit = function changeAutoFit() { if ($scope.menu.autoFit && $scope.client.clientProperties.minScale) { $scope.client.clientProperties.autoFit = true; } else { $scope.client.clientProperties.autoFit = false; $scope.client.clientProperties.scale = 1; } }; $scope.autoFitDisabled = function() { return $scope.client.clientProperties.minZoom >= 1; }; /** * Immediately disconnects the currently-connected client, if any. */ $scope.disconnect = function disconnect() { // Disconnect if client is available if ($scope.client) $scope.client.client.disconnect(); // Hide menu $scope.menu.shown = false; }; /** * Action which immediately disconnects the currently-connected client, if * any. */ var DISCONNECT_MENU_ACTION = { name : 'CLIENT.ACTION_DISCONNECT', className : 'danger disconnect', callback : $scope.disconnect }; // Set client-specific menu actions $scope.clientMenuActions = [ DISCONNECT_MENU_ACTION ]; /** * The currently-visible filesystem within the filesystem menu, if the * filesystem menu is open. If no filesystem is currently visible, this * will be null. * * @type ManagedFilesystem */ $scope.filesystemMenuContents = null; /** * Hides the filesystem menu. */ $scope.hideFilesystemMenu = function hideFilesystemMenu() { $scope.filesystemMenuContents = null; }; /** * Shows the filesystem menu, displaying the contents of the given * filesystem within it. * * @param {ManagedFilesystem} filesystem * The filesystem to show within the filesystem menu. */ $scope.showFilesystemMenu = function showFilesystemMenu(filesystem) { $scope.filesystemMenuContents = filesystem; }; /** * Returns whether the filesystem menu should be visible. * * @returns {Boolean} * true if the filesystem menu is shown, false otherwise. */ $scope.isFilesystemMenuShown = function isFilesystemMenuShown() { return !!$scope.filesystemMenuContents && $scope.menu.shown; }; // Automatically refresh display when filesystem menu is shown $scope.$watch('isFilesystemMenuShown()', function refreshFilesystem() { // Refresh filesystem, if defined var filesystem = $scope.filesystemMenuContents; if (filesystem) ManagedFilesystem.refresh(filesystem, filesystem.currentDirectory); }); /** * Returns the full path to the given file as an ordered array of parent * directories. * * @param {ManagedFilesystem.File} file * The file whose full path should be retrieved. * * @returns {ManagedFilesystem.File[]} * An array of directories which make up the hierarchy containing the * given file, in order of increasing depth. */ $scope.getPath = function getPath(file) { var path = []; // Add all files to path in ascending order of depth while (file && file.parent) { path.unshift(file); file = file.parent; } return path; }; /** * Changes the current directory of the given filesystem to the given * directory. * * @param {ManagedFilesystem} filesystem * The filesystem whose current directory should be changed. * * @param {ManagedFilesystem.File} file * The directory to change to. */ $scope.changeDirectory = function changeDirectory(filesystem, file) { ManagedFilesystem.changeDirectory(filesystem, file); }; /** * Begins a file upload through the attached Guacamole client for * each file in the given FileList. * * @param {FileList} files * The files to upload. */ $scope.uploadFiles = function uploadFiles(files) { // Ignore file uploads if no attached client if (!$scope.client) return; // Upload each file for (var i = 0; i < files.length; i++) ManagedClient.uploadFile($scope.client, files[i], $scope.filesystemMenuContents); }; /** * Determines whether the attached client has associated file transfers, * regardless of those file transfers' state. * * @returns {Boolean} * true if there are any file transfers associated with the * attached client, false otherise. */ $scope.hasTransfers = function hasTransfers() { // There are no file transfers if there is no client if (!$scope.client) return false; return !!($scope.client.uploads.length || $scope.client.downloads.length); }; // Clean up when view destroyed $scope.$on('$destroy', function clientViewDestroyed() { // Remove client from client manager if no longer connected var managedClient = $scope.client; if (managedClient) { // Get current connection state var connectionState = managedClient.clientState.connectionState; // If disconnected, remove from management if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED || connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR || connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) guacClientManager.removeManagedClient(managedClient.id); } }); }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/types/ManagedClient.js0100664 0001751 0001751 00000044637 12645510255 031733 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the ManagedClient class used by the guacClientManager service. */ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', function defineManagedClient($rootScope, $injector) { // Required types var ClientProperties = $injector.get('ClientProperties'); var ClientIdentifier = $injector.get('ClientIdentifier'); var ManagedClientState = $injector.get('ManagedClientState'); var ManagedDisplay = $injector.get('ManagedDisplay'); var ManagedFileDownload = $injector.get('ManagedFileDownload'); var ManagedFilesystem = $injector.get('ManagedFilesystem'); var ManagedFileUpload = $injector.get('ManagedFileUpload'); // Required services var $document = $injector.get('$document'); var $q = $injector.get('$q'); var $window = $injector.get('$window'); var authenticationService = $injector.get('authenticationService'); var connectionGroupService = $injector.get('connectionGroupService'); var connectionService = $injector.get('connectionService'); var guacAudio = $injector.get('guacAudio'); var guacHistory = $injector.get('guacHistory'); var guacImage = $injector.get('guacImage'); var guacVideo = $injector.get('guacVideo'); /** * Object which serves as a surrogate interface, encapsulating a Guacamole * client while it is active, allowing it to be detached and reattached * from different client views. * * @constructor * @param {ManagedClient|Object} [template={}] * The object whose properties should be copied within the new * ManagedClient. */ var ManagedClient = function ManagedClient(template) { // Use empty object by default template = template || {}; /** * The ID of the connection associated with this client. * * @type String */ this.id = template.id; /** * The actual underlying Guacamole client. * * @type Guacamole.Client */ this.client = template.client; /** * The tunnel being used by the underlying Guacamole client. * * @type Guacamole.Tunnel */ this.tunnel = template.tunnel; /** * The display associated with the underlying Guacamole client. * * @type ManagedDisplay */ this.managedDisplay = template.managedDisplay; /** * The name returned associated with the connection or connection * group in use. * * @type String */ this.name = template.name; /** * The current clipboard contents. * * @type String */ this.clipboardData = template.clipboardData || ''; /** * All downloaded files. As files are downloaded, their progress can be * observed through the elements of this array. It is intended that * this array be manipulated externally as needed. * * @type ManagedFileDownload[] */ this.downloads = template.downloads || []; /** * All uploaded files. As files are uploaded, their progress can be * observed through the elements of this array. It is intended that * this array be manipulated externally as needed. * * @type ManagedFileUpload[] */ this.uploads = template.uploads || []; /** * All currently-exposed filesystems. When the Guacamole server exposes * a filesystem object, that object will be made available as a * ManagedFilesystem within this array. * * @type ManagedFilesystem[] */ this.filesystems = template.filesystems || []; /** * The current state of the Guacamole client (idle, connecting, * connected, terminated with error, etc.). * * @type ManagedClientState */ this.clientState = template.clientState || new ManagedClientState(); /** * Properties associated with the display and behavior of the Guacamole * client. * * @type ClientProperties */ this.clientProperties = template.clientProperties || new ClientProperties(); }; /** * Returns a promise which resolves with the string of connection * parameters to be passed to the Guacamole client during connection. This * string generally contains the desired connection ID, display resolution, * and supported audio/video/image formats. The returned promise is * guaranteed to resolve successfully. * * @param {ClientIdentifier} identifier * The identifier representing the connection or group to connect to. * * @param {String} [connectionParameters] * Any additional HTTP parameters to pass while connecting. * * @returns {Promise.} * A promise which resolves with the string of connection parameters to * be passed to the Guacamole client, once the string is ready. */ var getConnectString = function getConnectString(identifier, connectionParameters) { var deferred = $q.defer(); // Calculate optimal width/height for display var pixel_density = $window.devicePixelRatio || 1; var optimal_dpi = pixel_density * 96; var optimal_width = $window.innerWidth * pixel_density; var optimal_height = $window.innerHeight * pixel_density; // Build base connect string var connectString = "token=" + encodeURIComponent(authenticationService.getCurrentToken()) + "&GUAC_DATA_SOURCE=" + encodeURIComponent(identifier.dataSource) + "&GUAC_ID=" + encodeURIComponent(identifier.id) + "&GUAC_TYPE=" + encodeURIComponent(identifier.type) + "&GUAC_WIDTH=" + Math.floor(optimal_width) + "&GUAC_HEIGHT=" + Math.floor(optimal_height) + "&GUAC_DPI=" + Math.floor(optimal_dpi) + (connectionParameters ? '&' + connectionParameters : ''); // Add audio mimetypes to connect string guacAudio.supported.forEach(function(mimetype) { connectString += "&GUAC_AUDIO=" + encodeURIComponent(mimetype); }); // Add video mimetypes to connect string guacVideo.supported.forEach(function(mimetype) { connectString += "&GUAC_VIDEO=" + encodeURIComponent(mimetype); }); // Add image mimetypes to connect string guacImage.getSupportedMimetypes().then(function supportedMimetypesKnown(mimetypes) { // Add each image mimetype angular.forEach(mimetypes, function addImageMimetype(mimetype) { connectString += "&GUAC_IMAGE=" + encodeURIComponent(mimetype); }); // Connect string is now ready - nothing else is deferred deferred.resolve(connectString); }); return deferred.promise; }; /** * Store the thumbnail of the given managed client within the connection * history under its associated ID. If the client is not connected, this * function has no effect. * * @param {String} managedClient * The client whose history entry should be updated. */ var updateHistoryEntry = function updateHistoryEntry(managedClient) { var display = managedClient.client.getDisplay(); // Update stored thumbnail of previous connection if (display && display.getWidth() > 0 && display.getHeight() > 0) { // Get screenshot var canvas = display.flatten(); // Calculate scale of thumbnail (max 320x240, max zoom 100%) var scale = Math.min(320 / canvas.width, 240 / canvas.height, 1); // Create thumbnail canvas var thumbnail = $document[0].createElement("canvas"); thumbnail.width = canvas.width*scale; thumbnail.height = canvas.height*scale; // Scale screenshot to thumbnail var context = thumbnail.getContext("2d"); context.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, thumbnail.width, thumbnail.height ); guacHistory.updateThumbnail(managedClient.id, thumbnail.toDataURL("image/png")); } }; /** * Creates a new ManagedClient, connecting it to the specified connection * or group. * * @param {String} id * The ID of the connection or group to connect to. This String must be * a valid ClientIdentifier string, as would be generated by * ClientIdentifier.toString(). * * @param {String} [connectionParameters] * Any additional HTTP parameters to pass while connecting. * * @returns {ManagedClient} * A new ManagedClient instance which is connected to the connection or * connection group having the given ID. */ ManagedClient.getInstance = function getInstance(id, connectionParameters) { var tunnel; // If WebSocket available, try to use it. if ($window.WebSocket) tunnel = new Guacamole.ChainedTunnel( new Guacamole.WebSocketTunnel('websocket-tunnel'), new Guacamole.HTTPTunnel('tunnel') ); // If no WebSocket, then use HTTP. else tunnel = new Guacamole.HTTPTunnel('tunnel'); // Get new client instance var client = new Guacamole.Client(tunnel); // Associate new managed client with new client and tunnel var managedClient = new ManagedClient({ id : id, client : client, tunnel : tunnel }); // Fire events for tunnel errors tunnel.onerror = function tunnelError(status) { $rootScope.$apply(function handleTunnelError() { ManagedClientState.setConnectionState(managedClient.clientState, ManagedClientState.ConnectionState.TUNNEL_ERROR, status.code); }); }; // Update connection state as tunnel state changes tunnel.onstatechange = function tunnelStateChanged(state) { $rootScope.$evalAsync(function updateTunnelState() { switch (state) { // Connection is being established case Guacamole.Tunnel.State.CONNECTING: ManagedClientState.setConnectionState(managedClient.clientState, ManagedClientState.ConnectionState.CONNECTING); break; // Connection has closed case Guacamole.Tunnel.State.CLOSED: ManagedClientState.setConnectionState(managedClient.clientState, ManagedClientState.ConnectionState.DISCONNECTED); break; } }); }; // Update connection state as client state changes client.onstatechange = function clientStateChanged(clientState) { $rootScope.$evalAsync(function updateClientState() { switch (clientState) { // Idle case 0: ManagedClientState.setConnectionState(managedClient.clientState, ManagedClientState.ConnectionState.IDLE); break; // Ignore "connecting" state case 1: // Connecting break; // Connected + waiting case 2: ManagedClientState.setConnectionState(managedClient.clientState, ManagedClientState.ConnectionState.WAITING); break; // Connected case 3: ManagedClientState.setConnectionState(managedClient.clientState, ManagedClientState.ConnectionState.CONNECTED); break; // Update history when disconnecting case 4: // Disconnecting case 5: // Disconnected updateHistoryEntry(managedClient); break; } }); }; // Disconnect and update status when the client receives an error client.onerror = function clientError(status) { $rootScope.$apply(function handleClientError() { // Disconnect, if connected client.disconnect(); // Update state ManagedClientState.setConnectionState(managedClient.clientState, ManagedClientState.ConnectionState.CLIENT_ERROR, status.code); }); }; // Handle any received clipboard data client.onclipboard = function clientClipboardReceived(stream, mimetype) { // Only text/plain is supported for now if (mimetype !== "text/plain") { stream.sendAck("Only text/plain supported", Guacamole.Status.Code.UNSUPPORTED); return; } var reader = new Guacamole.StringReader(stream); var data = ""; // Append any received data to buffer reader.ontext = function clipboard_text_received(text) { data += text; stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); }; // Update state when done reader.onend = function clipboard_text_end() { $rootScope.$apply(function updateClipboard() { managedClient.clipboardData = data; }); }; }; // Handle any received files client.onfile = function clientFileReceived(stream, mimetype, filename) { $rootScope.$apply(function startDownload() { managedClient.downloads.push(ManagedFileDownload.getInstance(stream, mimetype, filename)); }); }; // Handle any received filesystem objects client.onfilesystem = function fileSystemReceived(object, name) { $rootScope.$apply(function exposeFilesystem() { managedClient.filesystems.push(ManagedFilesystem.getInstance(object, name)); }); }; // Manage the client display managedClient.managedDisplay = ManagedDisplay.getInstance(client.getDisplay()); // Parse connection details from ID var clientIdentifier = ClientIdentifier.fromString(id); // Connect the Guacamole client getConnectString(clientIdentifier, connectionParameters) .then(function connectClient(connectString) { client.connect(connectString); }); // If using a connection, pull connection name if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION) { connectionService.getConnection(clientIdentifier.dataSource, clientIdentifier.id) .success(function connectionRetrieved(connection) { managedClient.name = connection.name; }); } // If using a connection group, pull connection name else if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION_GROUP) { connectionGroupService.getConnectionGroup(clientIdentifier.dataSource, clientIdentifier.id) .success(function connectionGroupRetrieved(group) { managedClient.name = group.name; }); } return managedClient; }; /** * Uploads the given file to the server through the given Guacamole client. * The file transfer can be monitored through the corresponding entry in * the uploads array of the given managedClient. * * @param {ManagedClient} managedClient * The ManagedClient through which the file is to be uploaded. * * @param {File} file * The file to upload. * * @param {ManagedFilesystem} [filesystem] * The filesystem to upload the file to, if any. If not specified, the * file will be sent as a generic Guacamole file stream. * * @param {ManagedFilesystem.File} [directory=filesystem.currentDirectory] * The directory within the given filesystem to upload the file to. If * not specified, but a filesystem is given, the current directory of * that filesystem will be used. */ ManagedClient.uploadFile = function uploadFile(managedClient, file, filesystem, directory) { // Use generic Guacamole file streams by default var object = null; var streamName = null; // If a filesystem is given, determine the destination object and stream if (filesystem) { object = filesystem.object; streamName = (directory || filesystem.currentDirectory).streamName + '/' + file.name; } // Start and manage file upload managedClient.uploads.push(ManagedFileUpload.getInstance(managedClient.client, file, object, streamName)); }; return ManagedClient; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/types/ManagedClientState.js0100664 0001751 0001751 00000012617 12612226251 032717 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the ManagedClient class used by the guacClientManager service. */ angular.module('client').factory('ManagedClientState', [function defineManagedClientState() { /** * Object which represents the state of a Guacamole client and its tunnel, * including any error conditions. * * @constructor * @param {ManagedClientState|Object} [template={}] * The object whose properties should be copied within the new * ManagedClientState. */ var ManagedClientState = function ManagedClientState(template) { // Use empty object by default template = template || {}; /** * The current connection state. Valid values are described by * ManagedClientState.ConnectionState. * * @type String * @default ManagedClientState.ConnectionState.IDLE */ this.connectionState = template.connectionState || ManagedClientState.ConnectionState.IDLE; /** * The status code of the current error condition, if connectionState * is CLIENT_ERROR or TUNNEL_ERROR. For all other connectionState * values, this will be @link{Guacamole.Status.Code.SUCCESS}. * * @type Number * @default Guacamole.Status.Code.SUCCESS */ this.statusCode = template.statusCode || Guacamole.Status.Code.SUCCESS; }; /** * Valid connection state strings. Each state string is associated with a * specific state of a Guacamole connection. */ ManagedClientState.ConnectionState = { /** * The Guacamole connection has not yet been attempted. * * @type String */ IDLE : "IDLE", /** * The Guacamole connection is being established. * * @type String */ CONNECTING : "CONNECTING", /** * The Guacamole connection has been successfully established, and the * client is now waiting for receipt of initial graphical data. * * @type String */ WAITING : "WAITING", /** * The Guacamole connection has been successfully established, and * initial graphical data has been received. * * @type String */ CONNECTED : "CONNECTED", /** * The Guacamole connection has terminated successfully. No errors are * indicated. * * @type String */ DISCONNECTED : "DISCONNECTED", /** * The Guacamole connection has terminated due to an error reported by * the client. The associated error code is stored in statusCode. * * @type String */ CLIENT_ERROR : "CLIENT_ERROR", /** * The Guacamole connection has terminated due to an error reported by * the tunnel. The associated error code is stored in statusCode. * * @type String */ TUNNEL_ERROR : "TUNNEL_ERROR" }; /** * Sets the current client state and, if given, the associated status code. * If an error is already represented, this function has no effect. * * @param {ManagedClientState} clientState * The ManagedClientState to update. * * @param {String} connectionState * The connection state to assign to the given ManagedClientState, as * listed within ManagedClientState.ConnectionState. * * @param {Number} [statusCode] * The status code to assign to the given ManagedClientState, if any, * as listed within Guacamole.Status.Code. If no status code is * specified, the status code of the ManagedClientState is not touched. */ ManagedClientState.setConnectionState = function(clientState, connectionState, statusCode) { // Do not set state after an error is registered if (clientState.connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR || clientState.connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) return; // Update connection state clientState.connectionState = connectionState; // Set status code, if given if (statusCode) clientState.statusCode = statusCode; }; return ManagedClientState; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/types/ManagedFileUpload.js0100664 0001751 0001751 00000017314 12612226251 032523 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the ManagedFileUpload class used by the guacClientManager service. */ angular.module('client').factory('ManagedFileUpload', ['$rootScope', '$injector', function defineManagedFileUpload($rootScope, $injector) { // Required types var ManagedFileTransferState = $injector.get('ManagedFileTransferState'); // Required services var $window = $injector.get('$window'); /** * The maximum number of bytes to include in each blob for the Guacamole * file stream. Note that this, along with instruction opcode and protocol- * related overhead, must not exceed the 8192 byte maximum imposed by the * Guacamole protocol. * * @type Number */ var STREAM_BLOB_SIZE = 4096; /** * Object which serves as a surrogate interface, encapsulating a Guacamole * file upload while it is active, allowing it to be detached and * reattached from different client views. * * @constructor * @param {ManagedFileUpload|Object} [template={}] * The object whose properties should be copied within the new * ManagedFileUpload. */ var ManagedFileUpload = function ManagedFileUpload(template) { // Use empty object by default template = template || {}; /** * The current state of the file transfer stream. * * @type ManagedFileTransferState */ this.transferState = template.transferState || new ManagedFileTransferState(); /** * The mimetype of the file being transferred. * * @type String */ this.mimetype = template.mimetype; /** * The filename of the file being transferred. * * @type String */ this.filename = template.filename; /** * The number of bytes transferred so far. * * @type Number */ this.progress = template.progress; /** * The total number of bytes in the file. * * @type Number */ this.length = template.length; }; /** * Converts the given bytes to a base64-encoded string. * * @param {Uint8Array} bytes A Uint8Array which contains the data to be * encoded as base64. * @return {String} The base64-encoded string. */ var getBase64 = function getBase64(bytes) { var data = ""; // Produce binary string from bytes in buffer for (var i=0; i= bytes.length) { stream.sendEnd(); managedFileUpload.progress = bytes.length; // Upload complete ManagedFileTransferState.setStreamState(managedFileUpload.transferState, ManagedFileTransferState.StreamState.CLOSED); // Notify of upload completion $rootScope.$broadcast('guacUploadComplete', file.name); } // Otherwise, update progress else managedFileUpload.progress = offset; }); }; // end ack handler }; reader.readAsArrayBuffer(file); return managedFileUpload; }; return ManagedFileUpload; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/types/ManagedFileDownload.js0100664 0001751 0001751 00000012347 12612226251 033047 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the ManagedFileDownload class used by the guacClientManager service. */ angular.module('client').factory('ManagedFileDownload', ['$rootScope', '$injector', function defineManagedFileDownload($rootScope, $injector) { // Required types var ManagedFileTransferState = $injector.get('ManagedFileTransferState'); /** * Object which serves as a surrogate interface, encapsulating a Guacamole * file download while it is active, allowing it to be detached and * reattached from different client views. * * @constructor * @param {ManagedFileDownload|Object} [template={}] * The object whose properties should be copied within the new * ManagedFileDownload. */ var ManagedFileDownload = function ManagedFileDownload(template) { // Use empty object by default template = template || {}; /** * The current state of the file transfer stream. * * @type ManagedFileTransferState */ this.transferState = template.transferState || new ManagedFileTransferState(); /** * The mimetype of the file being transferred. * * @type String */ this.mimetype = template.mimetype; /** * The filename of the file being transferred. * * @type String */ this.filename = template.filename; /** * The number of bytes transferred so far. * * @type Number */ this.progress = template.progress; /** * A blob containing the complete downloaded file. This is available * only after the download has finished. * * @type Blob */ this.blob = template.blob; }; /** * Creates a new ManagedFileDownload which downloads the contents of the * given stream as a file having the given mimetype and filename. * * @param {Guacamole.InputStream} stream * The stream whose contents should be downloaded as a file. * * @param {String} mimetype * The mimetype of the stream contents. * * @param {String} filename * The filename of the file being received over the steram. * * @return {ManagedFileDownload} * A new ManagedFileDownload object which can be used to track the * progress of the download. */ ManagedFileDownload.getInstance = function getInstance(stream, mimetype, filename) { // Init new file download object var managedFileDownload = new ManagedFileDownload({ mimetype : mimetype, filename : filename, progress : 0, transferState : new ManagedFileTransferState({ streamState : ManagedFileTransferState.StreamState.OPEN }) }); // Begin file download var blob_reader = new Guacamole.BlobReader(stream, mimetype); // Update progress as data is received blob_reader.onprogress = function onprogress() { // Update progress $rootScope.$apply(function downloadStreamProgress() { managedFileDownload.progress = blob_reader.getLength(); }); // Signal server that data was received stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); }; // Save blob and close stream when complete blob_reader.onend = function onend() { $rootScope.$apply(function downloadStreamEnd() { // Save blob managedFileDownload.blob = blob_reader.getBlob(); // Mark stream as closed ManagedFileTransferState.setStreamState(managedFileDownload.transferState, ManagedFileTransferState.StreamState.CLOSED); // Notify of upload completion $rootScope.$broadcast('guacDownloadComplete', filename); }); }; // Signal server that data is ready to be received stream.sendAck("Ready", Guacamole.Status.Code.SUCCESS); return managedFileDownload; }; return ManagedFileDownload; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/types/ClientProperties.js0100664 0001751 0001751 00000006725 12612226251 032521 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for generating new guacClient properties objects. */ angular.module('client').factory('ClientProperties', ['$injector', function defineClientProperties($injector) { // Required services var preferenceService = $injector.get('preferenceService'); /** * Object used for interacting with a guacClient directive. * * @constructor * @param {ClientProperties|Object} [template={}] * The object whose properties should be copied within the new * ClientProperties. */ var ClientProperties = function ClientProperties(template) { // Use empty object by default template = template || {}; /** * Whether the display should be scaled automatically to fit within the * available space. * * @type Boolean */ this.autoFit = template.autoFit || true; /** * The current scale. If autoFit is true, the effect of setting this * value is undefined. * * @type Number */ this.scale = template.scale || 1; /** * The minimum scale value. * * @type Number */ this.minScale = template.minScale || 1; /** * The maximum scale value. * * @type Number */ this.maxScale = template.maxScale || 3; /** * Whether or not the client should listen to keyboard events. * * @type Boolean */ this.keyboardEnabled = template.keyboardEnabled || true; /** * Whether translation of touch to mouse events should emulate an * absolute pointer device, or a relative pointer device. * * @type Boolean */ this.emulateAbsoluteMouse = template.emulateAbsoluteMouse || preferenceService.preferences.emulateAbsoluteMouse; /** * The relative Y coordinate of the scroll offset of the display within * the client element. * * @type Number */ this.scrollTop = template.scrollTop || 0; /** * The relative X coordinate of the scroll offset of the display within * the client element. * * @type Number */ this.scrollLeft = template.scrollLeft || 0; }; return ClientProperties; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/types/ManagedDisplay.js0100664 0001751 0001751 00000012446 12612226251 032105 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the ManagedDisplay class used by the guacClientManager service. */ angular.module('client').factory('ManagedDisplay', ['$rootScope', function defineManagedDisplay($rootScope) { /** * Object which serves as a surrogate interface, encapsulating a Guacamole * display while it is active, allowing it to be detached and reattached * from different client views. * * @constructor * @param {ManagedDisplay|Object} [template={}] * The object whose properties should be copied within the new * ManagedDisplay. */ var ManagedDisplay = function ManagedDisplay(template) { // Use empty object by default template = template || {}; /** * The underlying Guacamole display. * * @type Guacamole.Display */ this.display = template.display; /** * The current size of the Guacamole display. * * @type ManagedDisplay.Dimensions */ this.size = new ManagedDisplay.Dimensions(template.size); /** * The current mouse cursor, if any. * * @type ManagedDisplay.Cursor */ this.cursor = template.cursor; }; /** * Object which represents the size of the Guacamole display. * * @constructor * @param {ManagedDisplay.Dimensions|Object} template * The object whose properties should be copied within the new * ManagedDisplay.Dimensions. */ ManagedDisplay.Dimensions = function Dimensions(template) { // Use empty object by default template = template || {}; /** * The current width of the Guacamole display, in pixels. * * @type Number */ this.width = template.width || 0; /** * The current width of the Guacamole display, in pixels. * * @type Number */ this.height = template.height || 0; }; /** * Object which represents a mouse cursor used by the Guacamole display. * * @constructor * @param {ManagedDisplay.Cursor|Object} template * The object whose properties should be copied within the new * ManagedDisplay.Cursor. */ ManagedDisplay.Cursor = function Cursor(template) { // Use empty object by default template = template || {}; /** * The actual mouse cursor image. * * @type HTMLCanvasElement */ this.canvas = template.canvas; /** * The X coordinate of the cursor hotspot. * * @type Number */ this.x = template.x; /** * The Y coordinate of the cursor hotspot. * * @type Number */ this.y = template.y; }; /** * Creates a new ManagedDisplay which represents the current state of the * given Guacamole display. * * @param {Guacamole.Display} display * The Guacamole display to represent. Changes to this display will * affect this ManagedDisplay. * * @returns {ManagedDisplay} * A new ManagedDisplay which represents the current state of the * given Guacamole display. */ ManagedDisplay.getInstance = function getInstance(display) { var managedDisplay = new ManagedDisplay({ display : display }); // Store changes to display size display.onresize = function setClientSize() { $rootScope.$apply(function updateClientSize() { managedDisplay.size = new ManagedDisplay.Dimensions({ width : display.getWidth(), height : display.getHeight() }); }); }; // Store changes to display cursor display.oncursor = function setClientCursor(canvas, x, y) { $rootScope.$apply(function updateClientCursor() { managedDisplay.cursor = new ManagedDisplay.Cursor({ canvas : canvas, x : x, y : y }); }); }; return managedDisplay; }; return ManagedDisplay; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/types/ManagedFilesystem.js0100664 0001751 0001751 00000026132 12612226251 032621 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the ManagedFilesystem class used by ManagedClient to represent * available remote filesystems. */ angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector', function defineManagedFilesystem($rootScope, $injector) { // Required types var ManagedFileDownload = $injector.get('ManagedFileDownload'); var ManagedFileUpload = $injector.get('ManagedFileUpload'); /** * Object which serves as a surrogate interface, encapsulating a Guacamole * filesystem object while it is active, allowing it to be detached and * reattached from different client views. * * @constructor * @param {ManagedFilesystem|Object} [template={}] * The object whose properties should be copied within the new * ManagedFilesystem. */ var ManagedFilesystem = function ManagedFilesystem(template) { // Use empty object by default template = template || {}; /** * The Guacamole filesystem object, as received via a "filesystem" * instruction. * * @type Guacamole.Object */ this.object = template.object; /** * The declared, human-readable name of the filesystem * * @type String */ this.name = template.name; /** * The root directory of the filesystem. * * @type ManagedFilesystem.File */ this.root = template.root; /** * The current directory being viewed or manipulated within the * filesystem. * * @type ManagedFilesystem.File */ this.currentDirectory = template.currentDirectory || template.root; }; /** * Refreshes the contents of the given file, if that file is a directory. * Only the immediate children of the file are refreshed. Files further * down the directory tree are not refreshed. * * @param {ManagedFilesystem} filesystem * The filesystem associated with the file being refreshed. * * @param {ManagedFilesystem.File} file * The file being refreshed. */ ManagedFilesystem.refresh = function updateDirectory(filesystem, file) { // Do not attempt to refresh the contents of directories if (file.mimetype !== Guacamole.Object.STREAM_INDEX_MIMETYPE) return; // Request contents of given file filesystem.object.requestInputStream(file.streamName, function handleStream(stream, mimetype) { // Ignore stream if mimetype is wrong if (mimetype !== Guacamole.Object.STREAM_INDEX_MIMETYPE) { stream.sendAck('Unexpected mimetype', Guacamole.Status.Code.UNSUPPORTED); return; } // Signal server that data is ready to be received stream.sendAck('Ready', Guacamole.Status.Code.SUCCESS); // Read stream as JSON var reader = new Guacamole.JSONReader(stream); // Acknowledge received JSON blobs reader.onprogress = function onprogress() { stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); }; // Reset contents of directory reader.onend = function jsonReady() { $rootScope.$evalAsync(function updateFileContents() { // Empty contents file.files = {}; // Determine the expected filename prefix of each stream var expectedPrefix = file.streamName; if (expectedPrefix.charAt(expectedPrefix.length - 1) !== '/') expectedPrefix += '/'; // For each received stream name var mimetypes = reader.getJSON(); for (var name in mimetypes) { // Assert prefix is correct if (name.substring(0, expectedPrefix.length) !== expectedPrefix) continue; // Extract filename from stream name var filename = name.substring(expectedPrefix.length); // Deduce type from mimetype var type = ManagedFilesystem.File.Type.NORMAL; if (mimetypes[name] === Guacamole.Object.STREAM_INDEX_MIMETYPE) type = ManagedFilesystem.File.Type.DIRECTORY; // Add file entry file.files[filename] = new ManagedFilesystem.File({ mimetype : mimetypes[name], streamName : name, type : type, parent : file, name : filename }); } }); }; }); }; /** * Creates a new ManagedFilesystem instance from the given Guacamole.Object * and human-readable name. Upon creation, a request to populate the * contents of the root directory will be automatically dispatched. * * @param {Guacamole.Object} object * The Guacamole.Object defining the filesystem. * * @param {String} name * A human-readable name for the filesystem. * * @returns {ManagedFilesystem} * The newly-created ManagedFilesystem. */ ManagedFilesystem.getInstance = function getInstance(object, name) { // Init new filesystem object var managedFilesystem = new ManagedFilesystem({ object : object, name : name, root : new ManagedFilesystem.File({ mimetype : Guacamole.Object.STREAM_INDEX_MIMETYPE, streamName : Guacamole.Object.ROOT_STREAM, type : ManagedFilesystem.File.Type.DIRECTORY }) }); // Retrieve contents of root ManagedFilesystem.refresh(managedFilesystem, managedFilesystem.root); return managedFilesystem; }; /** * Downloads the given file from the server using the given Guacamole * client and filesystem. The file transfer can be monitored through the * corresponding entry in the downloads array of the given ManagedClient. * * @param {ManagedClient} managedClient * The ManagedClient from which the file is to be downloaded. * * @param {ManagedFilesystem} managedFilesystem * The ManagedFilesystem from which the file is to be downloaded. Any * path information provided must be relative to this filesystem. * * @param {String} path * The full, absolute path of the file to download. */ ManagedFilesystem.downloadFile = function downloadFile(managedClient, managedFilesystem, path) { // Request download managedFilesystem.object.requestInputStream(path, function downloadStreamReceived(stream, mimetype) { // Parse filename from string var filename = path.match(/(.*[\\/])?(.*)/)[2]; // Start and track download managedClient.downloads.push(ManagedFileDownload.getInstance(stream, mimetype, filename)); }); }; /** * Changes the current directory of the given filesystem, automatically * refreshing the contents of that directory. * * @param {ManagedFilesystem} filesystem * The filesystem whose current directory should be changed. * * @param {ManagedFilesystem.File} file * The directory to change to. */ ManagedFilesystem.changeDirectory = function changeDirectory(filesystem, file) { // Refresh contents ManagedFilesystem.refresh(filesystem, file); // Set current directory filesystem.currentDirectory = file; }; /** * A file within a ManagedFilesystem. Each ManagedFilesystem.File provides * sufficient information for retrieval or replacement of the file's * contents, as well as the file's name and type. * * @param {ManagedFilesystem|Object} [template={}] * The object whose properties should be copied within the new * ManagedFilesystem.File. */ ManagedFilesystem.File = function File(template) { /** * The mimetype of the data contained within this file. * * @type String */ this.mimetype = template.mimetype; /** * The name of the stream representing this files contents within its * associated filesystem object. * * @type String */ this.streamName = template.streamName; /** * The type of this file. All legal file type strings are defined * within ManagedFilesystem.File.Type. * * @type String */ this.type = template.type; /** * The name of this file. * * @type String */ this.name = template.name; /** * The parent directory of this file. In the case of the root * directory, this will be null. * * @type ManagedFilesystem.File */ this.parent = template.parent; /** * Map of all known files containined within this file by name. This is * only applicable to directories. * * @type Object. */ this.files = template.files || {}; }; /** * All legal type strings for a ManagedFilesystem.File. * * @type Object. */ ManagedFilesystem.File.Type = { /** * A normal file. As ManagedFilesystem does not currently represent any * other non-directory types of files, like symbolic links, this type * string may be used for any non-directory file. * * @type String */ NORMAL : 'NORMAL', /** * A directory. * * @type String */ DIRECTORY : 'DIRECTORY' }; return ManagedFilesystem; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/types/ManagedFileTransferState.js0100664 0001751 0001751 00000011076 12612226251 034063 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the ManagedFileTransferState class used by the guacClientManager * service. */ angular.module('client').factory('ManagedFileTransferState', [function defineManagedFileTransferState() { /** * Object which represents the state of a Guacamole stream, including any * error conditions. * * @constructor * @param {ManagedFileTransferState|Object} [template={}] * The object whose properties should be copied within the new * ManagedFileTransferState. */ var ManagedFileTransferState = function ManagedFileTransferState(template) { // Use empty object by default template = template || {}; /** * The current stream state. Valid values are described by * ManagedFileTransferState.StreamState. * * @type String * @default ManagedFileTransferState.StreamState.IDLE */ this.streamState = template.streamState || ManagedFileTransferState.StreamState.IDLE; /** * The status code of the current error condition, if streamState * is ERROR. For all other streamState values, this will be * @link{Guacamole.Status.Code.SUCCESS}. * * @type Number * @default Guacamole.Status.Code.SUCCESS */ this.statusCode = template.statusCode || Guacamole.Status.Code.SUCCESS; }; /** * Valid stream state strings. Each state string is associated with a * specific state of a Guacamole stream. */ ManagedFileTransferState.StreamState = { /** * The stream has not yet been opened. * * @type String */ IDLE : "IDLE", /** * The stream has been successfully established. Data can be sent or * received. * * @type String */ OPEN : "OPEN", /** * The stream has terminated successfully. No errors are indicated. * * @type String */ CLOSED : "CLOSED", /** * The stream has terminated due to an error. The associated error code * is stored in statusCode. * * @type String */ ERROR : "ERROR" }; /** * Sets the current transfer state and, if given, the associated status * code. If an error is already represented, this function has no effect. * * @param {ManagedFileTransferState} transferState * The ManagedFileTransferState to update. * * @param {String} streamState * The stream state to assign to the given ManagedFileTransferState, as * listed within ManagedFileTransferState.StreamState. * * @param {Number} [statusCode] * The status code to assign to the given ManagedFileTransferState, if * any, as listed within Guacamole.Status.Code. If no status code is * specified, the status code of the ManagedFileTransferState is not * touched. */ ManagedFileTransferState.setStreamState = function setStreamState(transferState, streamState, statusCode) { // Do not set state after an error is registered if (transferState.streamState === ManagedFileTransferState.StreamState.ERROR) return; // Update stream state transferState.streamState = streamState; // Set status code, if given if (statusCode) transferState.statusCode = statusCode; }; return ManagedFileTransferState; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/templates/file.html0100664 0001751 0001751 00000002473 12612226251 031323 0ustar00mjumpermjumper0000000 0000000
{{::name}}
guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/templates/guacFileTransfer.html0100664 0001751 0001751 00000003625 12612226251 033630 0ustar00mjumpermjumper0000000 0000000
{{transfer.filename}}

{{getErrorText() | translate}}

guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/templates/client.html0100664 0001751 0001751 00000022403 12620747674 031675 0ustar00mjumpermjumper0000000 0000000
guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/templates/guacViewport.html0100664 0001751 0001751 00000002315 12612226251 033056 0ustar00mjumpermjumper0000000 0000000
guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/templates/guacFileBrowser.html0100664 0001751 0001751 00000002441 12612226251 033462 0ustar00mjumpermjumper0000000 0000000
guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/templates/guacClient.html0100664 0001751 0001751 00000002627 12626164604 032473 0ustar00mjumpermjumper0000000 0000000
guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/templates/guacThumbnail.html0100664 0001751 0001751 00000002564 12612226251 033170 0ustar00mjumpermjumper0000000 0000000
guacamole-client-0.9.9/guacamole/src/main/webapp/app/client/templates/guacFileTransferManager.html0100664 0001751 0001751 00000003611 12645510255 035124 0ustar00mjumpermjumper0000000 0000000

{{'CLIENT.SECTION_HEADER_FILE_TRANSFERS' | translate}}

guacamole-client-0.9.9/guacamole/src/main/webapp/app/groupList/directives/guacGroupListFilter.js0100664 0001751 0001751 00000021112 12645510255 034664 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which provides a filtering text input field which automatically * produces a filtered subset of the given connection groups. */ angular.module('groupList').directive('guacGroupListFilter', [function guacGroupListFilter() { return { restrict: 'E', replace: true, scope: { /** * The property to which a subset of the provided map of connection * groups will be assigned. * * @type Array */ filteredConnectionGroups : '=', /** * The placeholder text to display within the filter input field * when no filter has been provided. * * @type String */ placeholder : '&', /** * The connection groups to filter, as a map of data source * identifier to corresponding root group. A subset of this map * will be exposed as filteredConnectionGroups. * * @type Object. */ connectionGroups : '&', /** * An array of expressions to filter against for each connection in * the hierarchy of connections and groups in the provided map. * These expressions must be Angular expressions which resolve to * properties on the connections in the provided map. * * @type String[] */ connectionProperties : '&', /** * An array of expressions to filter against for each connection group * in the hierarchy of connections and groups in the provided map. * These expressions must be Angular expressions which resolve to * properties on the connection groups in the provided map. * * @type String[] */ connectionGroupProperties : '&' }, templateUrl: 'app/groupList/templates/guacGroupListFilter.html', controller: ['$scope', '$injector', function guacGroupListFilterController($scope, $injector) { // Required types var ConnectionGroup = $injector.get('ConnectionGroup'); var FilterPattern = $injector.get('FilterPattern'); /** * The pattern object to use when filtering connections. * * @type FilterPattern */ var connectionFilterPattern = new FilterPattern($scope.connectionProperties()); /** * The pattern object to use when filtering connection groups. * * @type FilterPattern */ var connectionGroupFilterPattern = new FilterPattern($scope.connectionGroupProperties()); /** * The filter search string to use to restrict the displayed * connection groups. * * @type String */ $scope.searchString = null; /** * Flattens the connection group hierarchy of the given connection * group such that all descendants are copied as immediate * children. The hierarchy of nested connection groups is otherwise * completely preserved. A connection or connection group nested * two or more levels deep within the hierarchy will thus appear * within the returned connection group in two places: in its * original location AND as an immediate child. * * @param {ConnectionGroup} connectionGroup * The connection group whose descendents should be copied as * first-level children. * * @returns {ConnectionGroup} * A new connection group completely identical to the provided * connection group, except that absolutely all descendents * have been copied into the first level of children. */ var flattenConnectionGroup = function flattenConnectionGroup(connectionGroup) { // Replace connection group with shallow copy connectionGroup = new ConnectionGroup(connectionGroup); // Ensure child arrays are defined and independent copies connectionGroup.childConnections = angular.copy(connectionGroup.childConnections) || []; connectionGroup.childConnectionGroups = angular.copy(connectionGroup.childConnectionGroups) || []; // Flatten all children to the top-level group angular.forEach(connectionGroup.childConnectionGroups, function flattenChild(child) { var flattenedChild = flattenConnectionGroup(child); // Merge all child connections Array.prototype.push.apply( connectionGroup.childConnections, flattenedChild.childConnections ); // Merge all child connection groups Array.prototype.push.apply( connectionGroup.childConnectionGroups, flattenedChild.childConnectionGroups ); }); return connectionGroup; }; /** * Applies the current filter predicate, filtering all provided * connection groups and storing the result in * filteredConnectionGroups. */ var updateFilteredConnectionGroups = function updateFilteredConnectionGroups() { // Do not apply any filtering (and do not flatten) if no // search string is provided if (!$scope.searchString) { $scope.filteredConnectionGroups = $scope.connectionGroups() || {}; return; } // Clear all current filtered groups $scope.filteredConnectionGroups = {}; // Re-filter any provided groups var connectionGroups = $scope.connectionGroups(); if (connectionGroups) { angular.forEach(connectionGroups, function updateFilteredConnectionGroup(connectionGroup, dataSource) { // Flatten hierarchy of connection group var filteredGroup = flattenConnectionGroup(connectionGroup); // Filter all direct children filteredGroup.childConnections = filteredGroup.childConnections.filter(connectionFilterPattern.predicate); filteredGroup.childConnectionGroups = filteredGroup.childConnectionGroups.filter(connectionGroupFilterPattern.predicate); // Store now-filtered root $scope.filteredConnectionGroups[dataSource] = filteredGroup; }); } }; // Recompile and refilter when pattern is changed $scope.$watch('searchString', function searchStringChanged(searchString) { connectionFilterPattern.compile(searchString); connectionGroupFilterPattern.compile(searchString); updateFilteredConnectionGroups(); }); // Refilter when items change $scope.$watchCollection($scope.connectionGroups, function itemsChanged() { updateFilteredConnectionGroups(); }); }] }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js0100664 0001751 0001751 00000023677 12612226251 033532 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which displays the contents of a connection group within an * automatically-paginated view. */ angular.module('groupList').directive('guacGroupList', [function guacGroupList() { return { restrict: 'E', replace: true, scope: { /** * The connection groups to display as a map of data source * identifier to corresponding root group. * * @type Object. */ connectionGroups : '=', /** * Arbitrary object which shall be made available to the connection * and connection group templates within the scope as * context. * * @type Object */ context : '=', /** * The URL or ID of the Angular template to use when rendering a * connection. The @link{GroupListItem} associated with that * connection will be exposed within the scope of the template * as item, and the arbitrary context object, if any, * will be exposed as context. * * @type String */ connectionTemplate : '=', /** * The URL or ID of the Angular template to use when rendering a * connection group. The @link{GroupListItem} associated with that * connection group will be exposed within the scope of the * template as item, and the arbitrary context object, * if any, will be exposed as context. * * @type String */ connectionGroupTemplate : '=', /** * Whether the root of the connection group hierarchy given should * be shown. If false (the default), only the descendants of the * given connection group will be listed. * * @type Boolean */ showRootGroup : '=', /** * The maximum number of connections or groups to show per page. * * @type Number */ pageSize : '=' }, templateUrl: 'app/groupList/templates/guacGroupList.html', controller: ['$scope', '$injector', function guacGroupListController($scope, $injector) { // Required services var activeConnectionService = $injector.get('activeConnectionService'); var dataSourceService = $injector.get('dataSourceService'); // Required types var GroupListItem = $injector.get('GroupListItem'); /** * Map of data source identifier to the number of active * connections associated with a given connection identifier. * If this information is unknown, or there are no active * connections for a given identifier, no number will be stored. * * @type Object.> */ var connectionCount = {}; /** * A list of all items which should appear at the root level. As * connections and connection groups from multiple data sources may * be included in a guacGroupList, there may be multiple root * items, even if the root connection group is shown. * * @type GroupListItem[] */ $scope.rootItems = []; /** * Returns the number of active usages of a given connection. * * @param {String} dataSource * The identifier of the data source containing the given * connection. * * @param {Connection} connection * The connection whose active connections should be counted. * * @returns {Number} * The number of currently-active usages of the given * connection. */ var countActiveConnections = function countActiveConnections(dataSource, connection) { return connectionCount[dataSource][connection.identifier]; }; /** * Returns whether the given item represents a connection that can * be displayed. If there is no connection template, then no * connection is visible. * * @param {GroupListItem} item * The item to check. * * @returns {Boolean} * true if the given item is a connection that can be * displayed, false otherwise. */ $scope.isVisibleConnection = function isVisibleConnection(item) { return item.isConnection && !!$scope.connectionTemplate; }; /** * Returns whether the given item represents a connection group * that can be displayed. If there is no connection group template, * then no connection group is visible. * * @param {GroupListItem} item * The item to check. * * @returns {Boolean} * true if the given item is a connection group that can be * displayed, false otherwise. */ $scope.isVisibleConnectionGroup = function isVisibleConnectionGroup(item) { return item.isConnectionGroup && !!$scope.connectionGroupTemplate; }; // Set contents whenever the connection group is assigned or changed $scope.$watch('connectionGroups', function setContents(connectionGroups) { // Reset stored data var dataSources = []; $scope.rootItems = []; connectionCount = {}; // If connection groups are given, add them to the interface if (connectionGroups) { // Add each provided connection group angular.forEach(connectionGroups, function addConnectionGroup(connectionGroup, dataSource) { // Prepare data source for active connection counting dataSources.push(dataSource); connectionCount[dataSource] = {}; // Create root item for current connection group var rootItem = GroupListItem.fromConnectionGroup(dataSource, connectionGroup, !!$scope.connectionTemplate, countActiveConnections); // If root group is to be shown, add it as a root item if ($scope.showRootGroup) $scope.rootItems.push(rootItem); // Otherwise, add its children as root items else { angular.forEach(rootItem.children, function addRootItem(child) { $scope.rootItems.push(child); }); } }); // Count active connections by connection identifier dataSourceService.apply( activeConnectionService.getActiveConnections, dataSources ) .then(function activeConnectionsRetrieved(activeConnectionMap) { // Within each data source, count each active connection by identifier angular.forEach(activeConnectionMap, function addActiveConnections(activeConnections, dataSource) { angular.forEach(activeConnections, function addActiveConnection(activeConnection) { // If counter already exists, increment var identifier = activeConnection.connectionIdentifier; if (connectionCount[dataSource][identifier]) connectionCount[dataSource][identifier]++; // Otherwise, initialize counter to 1 else connectionCount[dataSource][identifier] = 1; }); }); }); } }); /** * Toggle the open/closed status of a group list item. * * @param {GroupListItem} groupListItem * The list item to expand, which should represent a * connection group. */ $scope.toggleExpanded = function toggleExpanded(groupListItem) { groupListItem.isExpanded = !groupListItem.isExpanded; }; }] }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/groupList/groupListModule.js0100664 0001751 0001751 00000002424 12612226251 031722 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Module for displaying the contents of a connection group, allowing the user * to select individual connections or groups. */ angular.module('groupList', ['list', 'rest']); guacamole-client-0.9.9/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js0100664 0001751 0001751 00000023330 12612226251 032476 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the GroupListItem class definition. */ angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', function defineGroupListItem(ConnectionGroup) { /** * Creates a new GroupListItem, initializing the properties of that * GroupListItem with the corresponding properties of the given template. * * @constructor * @param {GroupListItem|Object} [template={}] * The object whose properties should be copied within the new * GroupListItem. */ var GroupListItem = function GroupListItem(template) { // Use empty object by default template = template || {}; /** * The identifier of the data source associated with the connection or * connection group this item represents. * * @type String */ this.dataSource = template.dataSource; /** * The unique identifier associated with the connection or connection * group this item represents. * * @type String */ this.identifier = template.identifier; /** * The human-readable display name of this item. * * @type String */ this.name = template.name; /** * The unique identifier of the protocol, if this item represents a * connection. If this item does not represent a connection, this * property is not applicable. * * @type String */ this.protocol = template.protocol; /** * All children items of this item. If this item contains no children, * this will be an empty array. * * @type GroupListItem[] */ this.children = template.children || []; /** * Whether this item represents a connection. If this item represents * a connection group, this MUST be false. * * @type Boolean */ this.isConnection = template.isConnection; /** * Whether this item represents a connection group. If this item * represents a connection, this MUST be false. * * @type Boolean */ this.isConnectionGroup = template.isConnectionGroup; /** * Whether this item represents a balancing connection group. * * @type Boolean */ this.isBalancing = template.isBalancing; /** * Whether the children items should be displayed. * * @type Boolean */ this.isExpanded = template.isExpanded; /** * Returns the number of currently active users for this connection or * connection group, if known. * * @type Number */ this.getActiveConnections = template.getActiveConnections || (function getActiveConnections() { return null; }); /** * The connection or connection group whose data is exposed within * this GroupListItem. * * @type Connection|ConnectionGroup */ this.wrappedItem = template.wrappedItem; }; /** * Creates a new GroupListItem using the contents of the given connection. * * @param {String} dataSource * The identifier of the data source containing the given connection * group. * * @param {ConnectionGroup} connection * The connection whose contents should be represented by the new * GroupListItem. * * @param {Function} [countActiveConnections] * A getter which returns the current number of active connections for * the given connection. If omitted, the number of active connections * known at the time this function was called is used instead. This * function will be passed, in order, the data source identifier and * the connection in question. * * @returns {GroupListItem} * A new GroupListItem which represents the given connection. */ GroupListItem.fromConnection = function fromConnection(dataSource, connection, countActiveConnections) { // Return item representing the given connection return new GroupListItem({ // Identifying information name : connection.name, identifier : connection.identifier, protocol : connection.protocol, dataSource : dataSource, // Type information isConnection : true, isConnectionGroup : false, // Count of currently active connections using this connection getActiveConnections : function getActiveConnections() { // Use getter, if provided if (countActiveConnections) return countActiveConnections(dataSource, connection); return connection.activeConnections; }, // Wrapped item wrappedItem : connection }); }; /** * Creates a new GroupListItem using the contents and descendants of the * given connection group. * * @param {String} dataSource * The identifier of the data source containing the given connection * group. * * @param {ConnectionGroup} connectionGroup * The connection group whose contents and descendants should be * represented by the new GroupListItem and its descendants. * * @param {Boolean} [includeConnections=true] * Whether connections should be included in the contents of the * resulting GroupListItem. By default, connections are included. * * @param {Function} [countActiveConnections] * A getter which returns the current number of active connections for * the given connection. If omitted, the number of active connections * known at the time this function was called is used instead. This * function will be passed, in order, the data source identifier and * the connection group in question. * * @param {Function} [countActiveConnectionGroups] * A getter which returns the current number of active connections for * the given connection group. If omitted, the number of active * connections known at the time this function was called is used * instead. This function will be passed, in order, the data source * identifier and the connection group in question. * * @returns {GroupListItem} * A new GroupListItem which represents the given connection group, * including all descendants. */ GroupListItem.fromConnectionGroup = function fromConnectionGroup(dataSource, connectionGroup, includeConnections, countActiveConnections, countActiveConnectionGroups) { var children = []; // Add any child connections if (connectionGroup.childConnections && includeConnections !== false) { connectionGroup.childConnections.forEach(function addChildConnection(child) { children.push(GroupListItem.fromConnection(dataSource, child, countActiveConnections)); }); } // Add any child groups if (connectionGroup.childConnectionGroups) { connectionGroup.childConnectionGroups.forEach(function addChildGroup(child) { children.push(GroupListItem.fromConnectionGroup(dataSource, child, includeConnections, countActiveConnections, countActiveConnectionGroups)); }); } // Return item representing the given connection group return new GroupListItem({ // Identifying information name : connectionGroup.name, identifier : connectionGroup.identifier, dataSource : dataSource, // Type information isConnection : false, isConnectionGroup : true, isBalancing : connectionGroup.type === ConnectionGroup.Type.BALANCING, // Already-converted children children : children, // Count of currently active connection groups using this connection getActiveConnections : function getActiveConnections() { // Use getter, if provided if (countActiveConnectionGroups) return countActiveConnectionGroups(dataSource, connectionGroup); return connectionGroup.activeConnections; }, // Wrapped item wrappedItem : connectionGroup }); }; return GroupListItem; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html0100664 0001751 0001751 00000005065 12612226251 033706 0ustar00mjumpermjumper0000000 0000000
guacamole-client-0.9.9/guacamole/src/main/webapp/app/groupList/templates/guacGroupListFilter.html0100664 0001751 0001751 00000002524 12645510255 035057 0ustar00mjumpermjumper0000000 0000000
guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/indexModule.js0100664 0001751 0001751 00000002610 12612226251 030155 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The module for the root of the application. */ angular.module('index', [ 'auth', 'client', 'home', 'login', 'manage', 'navigation', 'ngRoute', 'ngTouch', 'notification', 'pascalprecht.translate', 'rest', 'settings', 'templates-main' ]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/styles/dialog.css0100664 0001751 0001751 00000004253 12612226251 030643 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .dialog-container { position: fixed; top: 0; left: 0; bottom: 0; right: 0; background: rgba(0, 0, 0, 0.5); padding: 1em; } .dialog-outer { display: table; height: 100%; width: 100%; position: fixed; left: 0; top: 0; background: rgba(0, 0, 0, 0.5); } .dialog-middle { width: 100%; text-align: center; display: table-cell; vertical-align: middle; } .dialog.edit { max-height: 100%; } .dialog { max-width: 100%; width: 8in; margin-left: auto; margin-right: auto; overflow: auto; border: 1px solid rgba(0, 0, 0, 0.5); background: #E7E7E7; -moz-border-radius: 0.2em; -webkit-border-radius: 0.2em; -khtml-border-radius: 0.2em; border-radius: 0.2em; box-shadow: 0.1em 0.1em 0.2em rgba(0, 0, 0, 0.6); } .dialog > * { margin: 1em; } .dialog .header { margin: 0; } .dialog td { position: relative; } .dialog .overlay { position: fixed; top: 0; left: 0; bottom: 0; right: 0; z-index: 1; } .dialog .footer { text-align: center; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/styles/lists.css0100664 0001751 0001751 00000004177 12612226251 030547 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .user, .group, .connection { cursor: pointer; } .user a, .connection a, .group a { text-decoration:none; color: black; } .user a:hover, .connection a:hover, .group a:hover { text-decoration:none; color: black; } .user a:visited, .connection a:visited, .group a:visited { text-decoration:none; color: black; } .connection:hover { background: #CDA; } .connection .thumbnail { margin: 0.5em; } .connection .thumbnail > * { border: 1px solid black; background: black; box-shadow: 1px 1px 5px black; max-width: 75%; display: inline-block; } div.recent-connections .connection .thumbnail { display: block; } div.recent-connections .protocol { display: none; } .caption * { vertical-align: middle; } .caption .choice { display: inline-block; } .caption .name { margin-left: 0.25em; } .placeholder { color: rgba(255, 255, 255, 0.5); text-shadow: -1px -1px rgba(0, 0, 0, 0.5); text-align: center; opacity: 0.5; font-size: 2em; font-weight: bolder; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/styles/input.css0100664 0001751 0001751 00000003353 12645510255 030551 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ input[type=checkbox], input[type=number], input[type=text], input[type=radio], label, textarea { -webkit-tap-highlight-color: rgba(128,192,128,0.5); } div.location, input[type=text], input[type=number], input[type=password], textarea { border: 1px solid #777; -moz-border-radius: 0.2em; -webkit-border-radius: 0.2em; -khtml-border-radius: 0.2em; border-radius: 0.2em; width: 100%; max-width: 16em; padding: 0.25em; font-size: 0.8em; background: white; cursor: text; } textarea { max-width: none; width: 30em; height: 10em; white-space: pre; word-wrap: normal; overflow: auto; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/styles/ui.css0100664 0001751 0001751 00000011252 12645510255 030024 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ * { -webkit-tap-highlight-color: rgba(0,0,0,0); -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } body { background: white; font-family: Carlito, FreeSans, Helvetica, Arial, sans-serif; padding: 0; margin: 0; } img { border: none; vertical-align: middle; } div.section { margin: 1em; padding: 0; } /* * List elements */ .list-item { display: block; text-align: left; cursor: pointer; position: relative; } .icon { width: 24px; height: 24px; background-size: 16px 16px; -moz-background-size: 16px 16px; -webkit-background-size: 16px 16px; -khtml-background-size: 16px 16px; background-repeat: no-repeat; background-position: center center; display: inline-block; vertical-align: middle; } .list-item * { vertical-align: middle; } .list-item .caption { padding: 0.1em; } .list-item .caption:after { clear: right; content: ""; display: block; } .list-item .name { color: black; font-weight: normal; padding: 0.1em; margin-left: 0.25em; } .list-item .usage { float: right; font-style: italic; color: gray; } .list-item.in-use { opacity: 0.5; } .choice .list-item.in-use { opacity: 1; } /* * List element styling */ .list-item.selected { background: #DEB; } .caption.active * { opacity: 0.5; } .caption .activeUserCount { font-style: italic; margin-right: 1em; float: right; } .list-item:not(.selected) .caption:hover { background: #CDA; } .choice .list-item { display: inline-block; } .choice input[type='checkbox'] { vertical-align: top; height: 24px; padding: 0; margin: 0; } .disabled .list-item:not(.selected) { opacity: 0.25; } .disabled .list-item:not(.selected):hover { background: inherit; } /* * List element icons */ .icon.user { background-image: url('images/user-icons/guac-user.png'); } .icon.user.add { background-image: url('images/action-icons/guac-user-add.png'); } .icon.connection { background-image: url('images/protocol-icons/guac-plug.png'); } .icon.connection.add { background-image: url('images/action-icons/guac-monitor-add.png'); } .protocol { display: inline-block; } .protocol .icon { width: 24px; height: 24px; background-image: url('images/protocol-icons/guac-plug.png'); background-size: 16px 16px; -moz-background-size: 16px 16px; -webkit-background-size: 16px 16px; -khtml-background-size: 16px 16px; background-repeat: no-repeat; background-position: center center; } .protocol .icon.ssh, .protocol .icon.telnet { background-image: url('images/protocol-icons/guac-text.png'); } .protocol .icon.vnc, .protocol .icon.rdp { background-image: url('images/protocol-icons/guac-monitor.png'); } /* * Groups */ .group > .children { margin-left: 13px; padding-left: 6px; } .group .icon.group.type.empty.balancer { opacity: 1; background-image: url('images/protocol-icons/guac-monitor.png'); } .group.expanded > .children { display: block; border-left: 1px dotted rgba(0, 0, 0, 0.25); } .group > .caption .icon.group { opacity: 0.75; background-image: url('images/group-icons/guac-closed.png'); } .group .icon.type.group.expanded { background-image: url('images/group-icons/guac-open.png'); } .group .icon.type.group.empty { opacity: 0.25; background-image: url('images/group-icons/guac-open.png'); } .history th, .history td { padding-left: 1em; padding-right: 1em; } .buttons { text-align: center; margin: 1em; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/styles/loading.css0100664 0001751 0001751 00000004324 12612226251 031020 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .loading { position: relative; min-height: 200px; } .view.loading { position: fixed; top: 0; left: 0; width: 100%; height: 100%; } .loading * { visibility: hidden; } .loading::before { display: block; position: absolute; content: ''; /* Dictated by size of image */ width: 96px; height: 96px; margin-left: -48px; margin-top: -48px; top: 50%; left: 50%; background-image: url('images/cog.png'); background-size: 96px 96px; background-position: center center; background-repeat: no-repeat; animation: spinning-cog 4s linear infinite; -moz-animation: spinning-cog 4s linear infinite; -webkit-animation: spinning-cog 4s linear infinite; } @keyframes spinning-cog { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } @-moz-keyframes spinning-cog { 0% { -moz-transform: rotate(0deg); } 100% { -moz-transform: rotate(360deg); } } @-webkit-keyframes spinning-cog { 0% { -webkit-transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); } } guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/styles/headers.css0100664 0001751 0001751 00000005176 12645510255 031032 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ h1 { margin: 0; padding: 0.5em; font-size: 2em; vertical-align: middle; text-align: center; } h2 { font-size: 1.25em; font-weight: bold; text-transform: uppercase; padding: 0.75em 0.5em; margin: 0; } .header { border-bottom: 1px solid rgba(0, 0, 0, 0.125); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.125); background: rgba(0, 0, 0, 0.04); margin-bottom: 1em; margin-top: 0; border-top: none; width: 100%; /* IE10 */ display: -ms-flexbox; -ms-flex-align: stretch; -ms-flex-direction: row; /* Ancient Mozilla */ display: -moz-box; -moz-box-align: stretch; -moz-box-orient: horizontal; /* Ancient WebKit */ display: -webkit-box; -webkit-box-align: stretch; -webkit-box-orient: horizontal; /* Old WebKit */ display: -webkit-flex; -webkit-align-items: stretch; -webkit-flex-direction: row; /* W3C */ display: flex; align-items: stretch; flex-direction: row; } .header ~ * .header, .header ~ .header { margin-top: 1em; border-top: 1px solid rgba(0, 0, 0, 0.125); } .header h2 { -ms-flex: 1 1 auto; -moz-box-flex: 1; -webkit-box-flex: 1; -webkit-flex: 1 1 auto; flex: 1 1 auto; } .header .filter { margin: 0; padding: 0.75em 0.5em; } .header .filter input { -moz-border-radius: 0; -webkit-border-radius: 0; -khtml-border-radius: 0; border-radius: 0; border: none; border-left: 1px solid rgba(0, 0, 0, 0.125); background-color: transparent; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/styles/status.css0100664 0001751 0001751 00000004104 12612226251 030722 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .status-outer { display: table; height: 100%; width: 100%; position: fixed; left: 0; top: 0; background: rgba(0, 0, 0, 0.5); z-index: 10; } .status-middle { width: 100%; text-align: center; display: table-cell; vertical-align: middle; } .status-middle .notification { width: 75%; max-width: 5in; margin-left: auto; margin-right: auto; overflow: auto; text-align: left; } .status-middle .notification .body { margin: 1.25em; } .status-middle .notification .buttons { margin: 1em; } /* Fade entire status area in/out based on shown status */ .status-outer { visibility: hidden; opacity: 0; transition: opacity, visibility; transition-duration: 0.25s; } .shown.status-outer { visibility: visible; opacity: 1; } /* Hide dialog immediately based on status */ .status-middle .notification { visibility: hidden; } .shown .status-middle .notification { visibility: visible; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/styles/animation.css0100664 0001751 0001751 00000003256 12612226251 031365 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * fadein: Fade from fully transparent to fully opaque. */ @keyframes fadein { from { opacity: 0; } to { opacity: 1; } } @-moz-keyframes fadein { from { opacity: 0; } to { opacity: 1; } } @-webkit-keyframes fadein { from { opacity: 0; } to { opacity: 1; } } /** * fadeout: Fade from fully opaque to fully transparent. */ @keyframes fadeout { from { opacity: 1; } to { opacity: 0; } } @-moz-keyframes fadeout { from { opacity: 1; } to { opacity: 0; } } @-webkit-keyframes fadeout { from { opacity: 1; } to { opacity: 0; } } guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/styles/buttons.css0100664 0001751 0001751 00000007547 12645510255 031121 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ a.button { cursor: default; display: inline-block; } input[type="submit"], button, a.button { -webkit-appearance: none; text-decoration: none; background-color: #3C3C3C; border: 1px solid rgba(0, 0, 0, 0.4); color: white; text-shadow: -1px -1px rgba(0, 0, 0, 0.3); font-size: 1em; font-weight: bold; font-family: Carlito, FreeSans, Helvetica, Arial, sans-serif; padding: 0.35em 1em; min-width: 5em; margin: 0.25em; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); } input[type="submit"]:hover, button:hover, a.button:hover { background-color: #5A5A5A; } input[type="submit"]:active, button:active, a.button:active { background-color: #2C2C2C; box-shadow: inset 1px 1px 0.25em rgba(0, 0, 0, 0.25), -1px -1px 0.25em rgba(0, 0, 0, 0.25), 1px 1px 0.25em rgba(255, 255, 255, 0.25); } button.danger, a.button.danger { background: #A43; } button.danger:hover, a.button.danger:hover { background: #C54; } button.danger:active, a.button.danger:active { background: #932; } input[type="submit"]:disabled, button:disabled, button.danger:disabled { background-color: #3C3C3C; color: rgba(255, 255, 255, 0.5); opacity: 0.75; } .button.logout, button.logout, .button.reconnect, button.reconnect, .button.manage, button.manage, .button.back, button.back, .button.home, button.home, .button.change-password, button.change-password { position: relative; padding-left: 1.8em; } .button.logout::before, button.logout::before, .button.reconnect::before, button.reconnect::before, .button.manage::before, button.manage::before, .button.back::before, button.back::before, .button.home::before, button.home::before, .button.change-password::before, button.change-password::before { content: ' '; position: absolute; left: 0; top: 0; bottom: 0; width: 1.8em; background-repeat: no-repeat; background-size: 1em; background-position: 0.5em 0.45em; } .button.logout::before, button.logout::before { background-image: url('images/action-icons/guac-logout.png'); } .button.reconnect::before, button.reconnect::before { background-image: url('images/circle-arrows.png'); } .button.manage::before, button.manage::before { background-image: url('images/action-icons/guac-config.png'); } .button.back::before, button.back::before { background-image: url('images/action-icons/guac-back.png'); } .button.home::before, button.home::before { background-image: url('images/action-icons/guac-home.png'); } .button.change-password::before, button.change-password::before { background-image: url('images/action-icons/guac-key.png'); } guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/styles/sorted-tables.css0100664 0001751 0001751 00000003533 12612226251 032154 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ table.sorted { border-collapse: collapse; } table.sorted th { background: rgba(0, 0, 0, 0.125); font-weight: normal; } table.sorted th, table.sorted td { border: 1px solid #AAA; padding: 0.5em 1em; } table.sorted th.sortable { cursor: pointer; } table.sorted th.sort-primary { font-weight: bold; padding-right: 0; } table.sorted th.sort-primary:after { display: inline-block; width: 1em; height: 1em; vertical-align: middle; content: ' '; background-size: 1em 1em; background-position: right center; background-repeat: no-repeat; background-image: url('images/arrows/down.png'); } table.sorted th.sort-primary.sort-descending:after { background-image: url('images/arrows/up.png'); } guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/styles/font-carlito.css0100664 0001751 0001751 00000003560 12612226251 032005 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /* * The Carlito font is licensed under the SIL Open Font License, and thus is * freely redistributable so long as it is distributed with software. * * The SIL OFL license can be found at http://scripts.sil.org/OFL or in the * "LICENSE" file within the same directory as the Carlito-*.woff font files. */ @font-face { font-family: 'Carlito'; font-weight: normal; font-style: normal; src: url('fonts/carlito/Carlito-Regular.woff') format('woff'); } @font-face { font-family: 'Carlito'; font-weight: bold; font-style: normal; src: url('fonts/carlito/Carlito-Bold.woff') format('woff'); } @font-face { font-family: 'Carlito'; font-weight: normal; font-style: italic; src: url('fonts/carlito/Carlito-Italic.woff') format('woff'); } guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/controllers/indexController.js0100664 0001751 0001751 00000013220 12631123302 033412 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The controller for the root of the application. */ angular.module('index').controller('indexController', ['$scope', '$injector', function indexController($scope, $injector) { // Required services var $document = $injector.get('$document'); var $window = $injector.get('$window'); var guacNotification = $injector.get('guacNotification'); /** * The notification service. */ $scope.guacNotification = guacNotification; /** * The message to display to the user as instructions for the login * process. * * @type String */ $scope.loginHelpText = null; /** * The credentials that the authentication service is has already accepted, * pending additional credentials, if any. If the user is logged in, or no * credentials have been accepted, this will be null. If credentials have * been accepted, this will be a map of name/value pairs corresponding to * the parameters submitted in a previous authentication attempt. * * @type Object. */ $scope.acceptedCredentials = null; /** * The credentials that the authentication service is currently expecting, * if any. If the user is logged in, this will be null. * * @type Field[] */ $scope.expectedCredentials = null; /** * Basic page-level information. */ $scope.page = { /** * The title of the page. * * @type String */ title: '', /** * The name of the CSS class to apply to the page body, if any. * * @type String */ bodyClassName: '' }; // Create event listeners at the global level var keyboard = new Guacamole.Keyboard($document[0]); // Broadcast keydown events keyboard.onkeydown = function onkeydown(keysym) { // Do not handle key events if not logged in if ($scope.expectedCredentials) return true; // Warn of pending keydown var guacBeforeKeydownEvent = $scope.$broadcast('guacBeforeKeydown', keysym, keyboard); if (guacBeforeKeydownEvent.defaultPrevented) return true; // If not prevented via guacBeforeKeydown, fire corresponding keydown event var guacKeydownEvent = $scope.$broadcast('guacKeydown', keysym, keyboard); return !guacKeydownEvent.defaultPrevented; }; // Broadcast keyup events keyboard.onkeyup = function onkeyup(keysym) { // Do not handle key events if not logged in if ($scope.expectedCredentials) return; // Warn of pending keyup var guacBeforeKeydownEvent = $scope.$broadcast('guacBeforeKeyup', keysym, keyboard); if (guacBeforeKeydownEvent.defaultPrevented) return; // If not prevented via guacBeforeKeyup, fire corresponding keydown event $scope.$broadcast('guacKeyup', keysym, keyboard); }; // Release all keys when window loses focus $window.onblur = function () { keyboard.reset(); }; // Display login screen if a whole new set of credentials is needed $scope.$on('guacInvalidCredentials', function loginInvalid(event, parameters, error) { $scope.page.title = 'APP.NAME'; $scope.page.bodyClassName = ''; $scope.loginHelpText = null; $scope.acceptedCredentials = {}; $scope.expectedCredentials = error.expected; }); // Prompt for remaining credentials if provided credentials were not enough $scope.$on('guacInsufficientCredentials', function loginInsufficient(event, parameters, error) { $scope.page.title = 'APP.NAME'; $scope.page.bodyClassName = ''; $scope.loginHelpText = error.message; $scope.acceptedCredentials = parameters; $scope.expectedCredentials = error.expected; }); // Clear login screen if login was successful $scope.$on('guacLogin', function loginSuccessful() { $scope.loginHelpText = null; $scope.acceptedCredentials = null; $scope.expectedCredentials = null; }); // Update title and CSS class upon navigation $scope.$on('$routeChangeSuccess', function(event, current, previous) { // If the current route is available if (current.$$route) { // Set title var title = current.$$route.title; if (title) $scope.page.title = title; // Set body CSS class $scope.page.bodyClassName = current.$$route.bodyClassName || ''; } }); }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js0100664 0001751 0001751 00000014632 12645510255 032436 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The config block for setting up all the url routing. */ angular.module('index').config(['$routeProvider', '$locationProvider', function indexRouteConfig($routeProvider, $locationProvider) { // Disable HTML5 mode (use # for routing) $locationProvider.html5Mode(false); /** * Attempts to re-authenticate with the Guacamole server, sending any * query parameters in the URL, along with the current auth token, and * updating locally stored token if necessary. * * @param {Service} $injector * The Angular $injector service. * * @returns {Promise} * A promise which resolves successfully only after an attempt to * re-authenticate has been made. If the authentication attempt fails, * the promise will be rejected. */ var updateCurrentToken = ['$injector', function updateCurrentToken($injector) { // Required services var $location = $injector.get('$location'); var authenticationService = $injector.get('authenticationService'); // Re-authenticate including any parameters in URL return authenticationService.updateCurrentToken($location.search()); }]; /** * Redirects the user to their home page. This necessarily requires * attempting to re-authenticate with the Guacamole server, as the user's * credentials may have changed, and thus their most-appropriate home page * may have changed as well. * * @param {Service} $injector * The Angular $injector service. * * @returns {Promise} * A promise which resolves successfully only after an attempt to * re-authenticate and determine the user's proper home page has been * made. */ var routeToUserHomePage = ['$injector', function routeToUserHomePage($injector) { // Required services var $location = $injector.get('$location'); var $q = $injector.get('$q'); var userPageService = $injector.get('userPageService'); // Promise for routing attempt var route = $q.defer(); // Re-authenticate including any parameters in URL $injector.invoke(updateCurrentToken) .then(function tokenUpdateComplete() { // Redirect to home page userPageService.getHomePage() .then(function homePageRetrieved(homePage) { // If home page is the requested location, allow through if ($location.path() === homePage.url) route.resolve(); // Otherwise, reject and reroute else { $location.url(homePage.url); route.reject(); } }) // If retrieval of home page fails, assume requested page is OK ['catch'](function homePageFailed() { route.resolve(); }); }); // Return promise that will resolve only if the requested page is the // home page return route.promise; }]; // Configure each possible route $routeProvider // Home screen .when('/', { title : 'APP.NAME', bodyClassName : 'home', templateUrl : 'app/home/templates/home.html', controller : 'homeController', resolve : { routeToUserHomePage: routeToUserHomePage } }) // Management screen .when('/settings/:dataSource?/:tab', { title : 'APP.NAME', bodyClassName : 'settings', templateUrl : 'app/settings/templates/settings.html', controller : 'settingsController', resolve : { updateCurrentToken: updateCurrentToken } }) // Connection editor .when('/manage/:dataSource/connections/:id?', { title : 'APP.NAME', bodyClassName : 'manage', templateUrl : 'app/manage/templates/manageConnection.html', controller : 'manageConnectionController', resolve : { updateCurrentToken: updateCurrentToken } }) // Connection group editor .when('/manage/:dataSource/connectionGroups/:id?', { title : 'APP.NAME', bodyClassName : 'manage', templateUrl : 'app/manage/templates/manageConnectionGroup.html', controller : 'manageConnectionGroupController', resolve : { updateCurrentToken: updateCurrentToken } }) // User editor .when('/manage/:dataSource/users/:id?', { title : 'APP.NAME', bodyClassName : 'manage', templateUrl : 'app/manage/templates/manageUser.html', controller : 'manageUserController', resolve : { updateCurrentToken: updateCurrentToken } }) // Client view .when('/client/:id/:params?', { bodyClassName : 'client', templateUrl : 'app/client/templates/client.html', controller : 'clientController', resolve : { updateCurrentToken: updateCurrentToken } }) // Redirect to home screen if page not found .otherwise({ resolve : { routeToUserHomePage: routeToUserHomePage } }); }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/config/indexHttpPatchConfig.js0100664 0001751 0001751 00000002564 12612226251 033232 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The config block for setting up the HTTP PATCH method. */ angular.module('index').config(['$httpProvider', function indexHttpPatchConfig($httpProvider) { $httpProvider.defaults.headers.patch = { 'Content-Type': 'application/json' } }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/index/config/indexTranslationConfig.js0100664 0001751 0001751 00000003702 12612226251 033624 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The configuration block for setting up everything having to do with i18n. */ angular.module('index').config(['$injector', function($injector) { // Required providers var $translateProvider = $injector.get('$translateProvider'); var preferenceServiceProvider = $injector.get('preferenceServiceProvider'); // Fallback to US English $translateProvider.fallbackLanguage('en'); // Prefer chosen language $translateProvider.preferredLanguage(preferenceServiceProvider.preferences.language); // Escape any HTML in translation strings $translateProvider.useSanitizeValueStrategy('escape'); // Load translations via translationLoader service $translateProvider.useLoader('translationLoader'); // Provide pluralization, etc. via messageformat.js $translateProvider.useMessageFormatInterpolation(); }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/list/directives/guacSortOrder.js0100664 0001751 0001751 00000007776 12612226251 032512 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Updates the priority of the sorting property given by "guac-sort-property" * within the SortOrder object given by "guac-sort-order". The CSS classes * "sort-primary" and "sort-descending" will be applied to the associated * element depending on the priority and sort direction of the given property. * * The associated element will automatically be assigned the "sortable" CSS * class. */ angular.module('list').directive('guacSortOrder', [function guacFocus() { return { restrict: 'A', link: function linkGuacSortOrder($scope, $element, $attrs) { /** * The object defining the sorting order. * * @type SortOrder */ var sortOrder = $scope.$eval($attrs.guacSortOrder); /** * The name of the property whose priority within the sort order * is controlled by this directive. * * @type String */ var sortProperty = $scope.$eval($attrs.guacSortProperty); /** * Returns whether the sort property defined via the * "guac-sort-property" attribute is the primary sort property of * the associated sort order. * * @returns {Boolean} * true if the sort property defined via the * "guac-sort-property" attribute is the primary sort property, * false otherwise. */ var isPrimary = function isPrimary() { return sortOrder.primary === sortProperty; }; /** * Returns whether the primary property of the sort order is * sorted in descending order. * * @returns {Boolean} * true if the primary property of the sort order is sorted in * descending order, false otherwise. */ var isDescending = function isDescending() { return sortOrder.descending; }; // Assign "sortable" class to associated element $element.addClass('sortable'); // Add/remove "sort-primary" class depending on sort order $scope.$watch(isPrimary, function primaryChanged(primary) { $element.toggleClass('sort-primary', primary); }); // Add/remove "sort-descending" class depending on sort order $scope.$watch(isDescending, function descendingChanged(descending) { $element.toggleClass('sort-descending', descending); }); // Update sort order when clicked $element[0].addEventListener('click', function clicked() { $scope.$evalAsync(function updateSortOrder() { sortOrder.togglePrimary(sortProperty); }); }); } // end guacSortOrder link function }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/list/directives/guacPager.js0100664 0001751 0001751 00000024420 12612226251 031606 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which provides pagination controls, along with a paginated * subset of the elements of some given array. */ angular.module('list').directive('guacPager', [function guacPager() { return { restrict: 'E', replace: true, scope: { /** * The property to which a subset of the provided array will be * assigned. * * @type Array */ page : '=', /** * The maximum number of items per page. * * @type Number */ pageSize : '&', /** * The maximum number of page choices to provide, regardless of the * total number of pages. * * @type Number */ pageCount : '&', /** * An array objects to paginate. Subsets of this array will be * exposed as pages. * * @type Array */ items : '&' }, templateUrl: 'app/list/templates/guacPager.html', controller: ['$scope', function guacPagerController($scope) { /** * The default size of a page, if not provided via the pageSize * attribute. * * @type Number */ var DEFAULT_PAGE_SIZE = 10; /** * The default maximum number of page choices to provide, if a * value is not providede via the pageCount attribute. * * @type Number */ var DEFAULT_PAGE_COUNT = 11; /** * An array of arrays, where the Nth array contains the contents of * the Nth page. * * @type Array[] */ var pages = []; /** * The number of the first selectable page. * * @type Number; */ $scope.firstPage = 1; /** * The number of the page immediately before the currently-selected * page. * * @type Number; */ $scope.previousPage = 1; /** * The number of the currently-selected page. * * @type Number; */ $scope.currentPage = 1; /** * The number of the page immediately after the currently-selected * page. * * @type Number; */ $scope.nextPage = 1; /** * The number of the last selectable page. * * @type Number; */ $scope.lastPage = 1; /** * An array of relevant page numbers that the user may want to jump * to directly. * * @type Number[] */ $scope.pageNumbers = []; /** * Updates the displayed page number choices. */ var updatePageNumbers = function updatePageNumbers() { // Get page count var pageCount = $scope.pageCount() || DEFAULT_PAGE_COUNT; // Determine start/end of page window var windowStart = $scope.currentPage - (pageCount - 1) / 2; var windowEnd = windowStart + pageCount - 1; // Shift window as necessary if it extends beyond the first page if (windowStart < $scope.firstPage) { windowEnd = Math.min($scope.lastPage, windowEnd - windowStart + $scope.firstPage); windowStart = $scope.firstPage; } // Shift window as necessary if it extends beyond the last page else if (windowEnd > $scope.lastPage) { windowStart = Math.max(1, windowStart - windowEnd + $scope.lastPage); windowEnd = $scope.lastPage; } // Generate list of relevant page numbers $scope.pageNumbers = []; for (var pageNumber = windowStart; pageNumber <= windowEnd; pageNumber++) $scope.pageNumbers.push(pageNumber); }; /** * Iterates through the bound items array, splitting it into pages * based on the current page size. */ var updatePages = function updatePages() { // Get current items and page size var items = $scope.items(); var pageSize = $scope.pageSize() || DEFAULT_PAGE_SIZE; // Clear current pages pages = []; // Only split into pages if items actually exist if (items) { // Split into pages of pageSize items each for (var i = 0; i < items.length; i += pageSize) pages.push(items.slice(i, i + pageSize)); } // Update minimum and maximum values $scope.firstPage = 1; $scope.lastPage = pages.length; // Select an appropriate page var adjustedCurrentPage = Math.min($scope.lastPage, Math.max($scope.firstPage, $scope.currentPage)); $scope.selectPage(adjustedCurrentPage); }; /** * Selects the page having the given number, assigning that page to * the property bound to the page attribute. If no such page * exists, the property will be set to undefined instead. Valid * page numbers begin at 1. * * @param {Number} page * The number of the page to select. Valid page numbers begin * at 1. */ $scope.selectPage = function selectPage(page) { // Select the chosen page $scope.currentPage = page; $scope.page = pages[page-1]; // Update next/previous page numbers $scope.nextPage = Math.min($scope.lastPage, $scope.currentPage + 1); $scope.previousPage = Math.max($scope.firstPage, $scope.currentPage - 1); // Update which page numbers are shown updatePageNumbers(); }; /** * Returns whether the given page number can be legally selected * via selectPage(), resulting in a different page being shown. * * @param {Number} page * The page number to check. * * @returns {Boolean} * true if the page having the given number can be selected, * false otherwise. */ $scope.canSelectPage = function canSelectPage(page) { return page !== $scope.currentPage && page >= $scope.firstPage && page <= $scope.lastPage; }; /** * Returns whether the page having the given number is currently * selected. * * @param {Number} page * The page number to check. * * @returns {Boolean} * true if the page having the given number is currently * selected, false otherwise. */ $scope.isSelected = function isSelected(page) { return page === $scope.currentPage; }; /** * Returns whether pages exist before the first page listed in the * pageNumbers array. * * @returns {Boolean} * true if pages exist before the first page listed in the * pageNumbers array, false otherwise. */ $scope.hasMorePagesBefore = function hasMorePagesBefore() { var firstPageNumber = $scope.pageNumbers[0]; return firstPageNumber !== $scope.firstPage; }; /** * Returns whether pages exist after the last page listed in the * pageNumbers array. * * @returns {Boolean} * true if pages exist after the last page listed in the * pageNumbers array, false otherwise. */ $scope.hasMorePagesAfter = function hasMorePagesAfter() { var lastPageNumber = $scope.pageNumbers[$scope.pageNumbers.length - 1]; return lastPageNumber !== $scope.lastPage; }; // Update available pages when available items are changed $scope.$watchCollection($scope.items, function itemsChanged() { updatePages(); }); // Update available pages when page size is changed $scope.$watch($scope.pageSize, function pageSizeChanged() { updatePages(); }); // Update available page numbers when page count is changed $scope.$watch($scope.pageCount, function pageCountChanged() { updatePageNumbers(); }); }] }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/list/directives/guacFilter.js0100664 0001751 0001751 00000007703 12612226251 032002 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which provides a filtering text input field which automatically * produces a filtered subset of the elements of some given array. */ angular.module('list').directive('guacFilter', [function guacFilter() { return { restrict: 'E', replace: true, scope: { /** * The property to which a subset of the provided array will be * assigned. * * @type Array */ filteredItems : '=', /** * The placeholder text to display within the filter input field * when no filter has been provided. * * @type String */ placeholder : '&', /** * An array of objects to filter. A subset of this array will be * exposed as filteredItems. * * @type Array */ items : '&', /** * An array of expressions to filter against for each object in the * items array. These expressions must be Angular expressions * which resolve to properties on the objects in the items array. * * @type String[] */ properties : '&' }, templateUrl: 'app/list/templates/guacFilter.html', controller: ['$scope', '$injector', function guacFilterController($scope, $injector) { // Required types var FilterPattern = $injector.get('FilterPattern'); /** * The pattern object to use when filtering items. * * @type FilterPattern */ var filterPattern = new FilterPattern($scope.properties()); /** * The filter search string to use to restrict the displayed items. * * @type String */ $scope.searchString = null; /** * Applies the current filter predicate, filtering all provided * items and storing the result in filteredItems. */ var updateFilteredItems = function updateFilteredItems() { var items = $scope.items(); if (items) $scope.filteredItems = items.filter(filterPattern.predicate); else $scope.filteredItems = []; }; // Recompile and refilter when pattern is changed $scope.$watch('searchString', function searchStringChanged(searchString) { filterPattern.compile(searchString); updateFilteredItems(); }); // Refilter when items change $scope.$watchCollection($scope.items, function itemsChanged() { updateFilteredItems(); }); }] }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/list/listModule.js0100664 0001751 0001751 00000002352 12612226251 027670 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Module for displaying, sorting, and filtering the contents of a list, split * into multiple pages. */ angular.module('list', []); guacamole-client-0.9.9/guacamole/src/main/webapp/app/list/styles/filter.css0100664 0001751 0001751 00000002612 12612226251 030532 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .filter { margin: 0.5em 0; } .filter .search-string { background-image: url('images/magnifier.png'); background-repeat: no-repeat; background-size: 1.75em; background-position: 0.25em center; padding: 0.5em; padding-left: 2.25em; width: 100%; max-width: none; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/list/styles/pager.css0100664 0001751 0001751 00000004641 12612226251 030347 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .pager { text-align: center; margin: 1em; } .pager .page-numbers { display: inline-block; margin: 0; padding: 0; } .pager .first-page, .pager .prev-page, .pager .set-page, .pager .next-page, .pager .last-page { cursor: pointer; vertical-align: middle; } .pager .first-page.disabled, .pager .prev-page.disabled, .pager .set-page.disabled, .pager .next-page.disabled, .pager .last-page.disabled { cursor: auto; opacity: 0.25; } .pager .set-page, .pager .more-pages { display: inline-block; padding: 0.25em; text-align: center; min-width: 1.25em; } .pager .set-page { text-decoration: underline; } .pager .set-page.current { cursor: auto; text-decoration: none; font-weight: bold; background: rgba(0, 0, 0, 0.1); border: 1px solid rgba(0, 0, 0, 0.1); -moz-border-radius: 0.2em; -webkit-border-radius: 0.2em; -khtml-border-radius: 0.2em; border-radius: 0.2em; } .pager .icon.first-page { background-image: url('images/action-icons/guac-first-page.png'); } .pager .icon.prev-page { background-image: url('images/action-icons/guac-prev-page.png'); } .pager .icon.next-page { background-image: url('images/action-icons/guac-next-page.png'); } .pager .icon.last-page { background-image: url('images/action-icons/guac-last-page.png'); } guacamole-client-0.9.9/guacamole/src/main/webapp/app/list/types/SortOrder.js0100664 0001751 0001751 00000011761 12612226251 030642 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for defining the SortOrder class. */ angular.module('list').factory('SortOrder', [ function defineSortOrder() { /** * Maintains a sorting predicate as required by the Angular orderBy filter. * The order of properties sorted by the predicate can be altered while * otherwise maintaining the sort order. * * @constructor * @param {String[]} predicate * The properties to sort by, in order of precidence. */ var SortOrder = function SortOrder(predicate) { /** * Reference to this instance. * * @type SortOrder */ var sortOrder = this; /** * The current sorting predicate. * * @type String[] */ this.predicate = predicate; /** * The name of the highest-precedence sorting property. * * @type String */ this.primary = predicate[0]; /** * Whether the highest-precedence sorting property is sorted in * descending order. * * @type Boolean */ this.descending = false; // Handle initially-descending primary properties if (this.primary.charAt(0) === '-') { this.primary = this.primary.substring(1); this.descending = true; } /** * Reorders the currently-defined predicate such that the named * property takes precidence over all others. The property will be * sorted in ascending order unless otherwise specified. * * @param {String} name * The name of the property to reorder by. * * @param {Boolean} [descending=false] * Whether the property should be sorted in descending order. By * default, all properties are sorted in ascending order. */ this.reorder = function reorder(name, descending) { // Build ascending and descending predicate components var ascendingName = name; var descendingName = '-' + name; // Remove requested property from current predicate sortOrder.predicate = sortOrder.predicate.filter(function notRequestedProperty(current) { return current !== ascendingName && current !== descendingName; }); // Add property to beginning of predicate if (descending) sortOrder.predicate.unshift(descendingName); else sortOrder.predicate.unshift(ascendingName); // Update sorted state sortOrder.primary = name; sortOrder.descending = !!descending; }; /** * Returns whether the sort order is primarily determined by the given * property. * * @param {String} property * The name of the property to check. * * @returns {Boolean} * true if the sort order is primarily determined by the given * property, false otherwise. */ this.isSortedBy = function isSortedBy(property) { return sortOrder.primary === property; }; /** * Sets the primary sorting property to the given property, if not already * set. If already set, the ascending/descending sort order is toggled. * * @param {String} property * The name of the property to assign as the primary sorting property. */ this.togglePrimary = function togglePrimary(property) { // Sort in ascending order by new property, if different if (!sortOrder.isSortedBy(property)) sortOrder.reorder(property, false); // Otherwise, toggle sort order else sortOrder.reorder(property, !sortOrder.descending); }; }; return SortOrder; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/list/types/FilterToken.js0100664 0001751 0001751 00000015023 12612226251 031140 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for defining the FilterToken class. */ angular.module('list').factory('FilterToken', ['$injector', function defineFilterToken($injector) { // Required types var IPv4Network = $injector.get('IPv4Network'); var IPv6Network = $injector.get('IPv6Network'); /** * An arbitrary token having an associated type and value. * * @constructor * @param {String} consumed * The input string consumed to produce this token. * * @param {String} type * The type of this token. Each legal type name is a property within * FilterToken.Types. * * @param {Object} value * The value of this token. The type of this value is determined by * the token type. */ var FilterToken = function FilterToken(consumed, type, value) { /** * The input string that was consumed to produce this token. * * @type String */ this.consumed = consumed; /** * The type of this token. Each legal type name is a property within * FilterToken.Types. * * @type String */ this.type = type; /** * The value of this token. * * @type Object */ this.value = value; }; /** * All legal token types, and corresponding functions which match them. * Each function returns the parsed token, or null if no such token was * found. * * @type Object. */ FilterToken.Types = { /** * An IPv4 address or subnet. The value of an IPV4_NETWORK token is an * IPv4Network. */ IPV4_NETWORK: function parseIPv4(str) { var pattern = /^\S+/; // Read first word via regex var matches = pattern.exec(str); if (!matches) return null; // Validate and parse as IPv4 address var network = IPv4Network.parse(matches[0]); if (!network) return null; return new FilterToken(matches[0], 'IPV4_NETWORK', network); }, /** * An IPv6 address or subnet. The value of an IPV6_NETWORK token is an * IPv6Network. */ IPV6_NETWORK: function parseIPv6(str) { var pattern = /^\S+/; // Read first word via regex var matches = pattern.exec(str); if (!matches) return null; // Validate and parse as IPv6 address var network = IPv6Network.parse(matches[0]); if (!network) return null; return new FilterToken(matches[0], 'IPV6_NETWORK', network); }, /** * A string literal, which may be quoted. The value of a LITERAL token * is a String. */ LITERAL: function parseLiteral(str) { var pattern = /^"([^"]*)"|^\S+/; // Validate against pattern var matches = pattern.exec(str); if (!matches) return null; // If literal is quoted, parse within the quotes if (matches[1]) return new FilterToken(matches[0], 'LITERAL', matches[1]); // Otherwise, literal is unquoted return new FilterToken(matches[0], 'LITERAL', matches[0]); }, /** * Arbitrary contiguous whitespace. The value of a WHITESPACE token is * a String. */ WHITESPACE: function parseWhitespace(str) { var pattern = /^\s+/; // Validate against pattern var matches = pattern.exec(str); if (!matches) return null; // Generate token from matching whitespace return new FilterToken(matches[0], 'WHITESPACE', matches[0]); } }; /** * Tokenizes the given string, returning an array of tokens. Whitespace * tokens are dropped. * * @param {String} str * The string to tokenize. * * @returns {FilterToken[]} * All tokens identified within the given string, in order. */ FilterToken.tokenize = function tokenize(str) { var tokens = []; /** * Returns the first token on the current string, removing the token * from that string. * * @returns FilterToken * The first token on the string, or null if no tokens match. */ var popToken = function popToken() { // Attempt to find a matching token for (var type in FilterToken.Types) { // Get matching function for current type var matcher = FilterToken.Types[type]; // If token matches, return the matching group var token = matcher(str); if (token) { str = str.substring(token.consumed.length); return token; } } // No match return null; }; // Tokenize input until no input remains while (str) { // Remove first token var token = popToken(); if (!token) break; // Add token to tokens array, if not whitespace if (token.type !== 'WHITESPACE') tokens.push(token); } return tokens; }; return FilterToken; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/list/types/IPv4Network.js0100664 0001751 0001751 00000010062 12612226251 031044 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for defining the IPv4Network class. */ angular.module('list').factory('IPv4Network', [ function defineIPv4Network() { /** * Represents an IPv4 network as a pairing of base address and netmask, * both of which are in binary form. To obtain an IPv4Network from * standard CIDR or dot-decimal notation, use IPv4Network.parse(). * * @constructor * @param {Number} address * The IPv4 address of the network in binary form. * * @param {Number} netmask * The IPv4 netmask of the network in binary form. */ var IPv4Network = function IPv4Network(address, netmask) { /** * Reference to this IPv4Network. * * @type IPv4Network */ var network = this; /** * The binary address of this network. This will be a 32-bit quantity. * * @type Number */ this.address = address; /** * The binary netmask of this network. This will be a 32-bit quantity. * * @type Number */ this.netmask = netmask; /** * Tests whether the given network is entirely within this network, * taking into account the base addresses and netmasks of both. * * @param {IPv4Network} other * The network to test. * * @returns {Boolean} * true if the other network is entirely within this network, false * otherwise. */ this.contains = function contains(other) { return network.address === (other.address & other.netmask & network.netmask); }; }; /** * Parses the given string as an IPv4 address or subnet, returning an * IPv4Network object which describes that address or subnet. * * @param {String} str * The string to parse. * * @returns {IPv4Network} * The parsed network, or null if the given string is not valid. */ IPv4Network.parse = function parse(str) { // Regex which matches the general form of IPv4 addresses var pattern = /^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})(?:\/([0-9]{1,2}))?$/; // Parse IPv4 address via regex var match = pattern.exec(str); if (!match) return null; // Parse netmask, if given var netmask = 0xFFFFFFFF; if (match[5]) { var bits = parseInt(match[5]); if (bits > 0 && bits <= 32) netmask = 0xFFFFFFFF << (32 - bits); } // Read each octet onto address var address = 0; for (var i=1; i <= 4; i++) { // Validate octet range var octet = parseInt(match[i]); if (octet > 255) return null; // Shift on octet address = (address << 8) | octet; } return new IPv4Network(address, netmask); }; return IPv4Network; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/list/types/FilterPattern.js0100664 0001751 0001751 00000020120 12630155314 031470 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for defining the FilterPattern class. */ angular.module('list').factory('FilterPattern', ['$injector', function defineFilterPattern($injector) { // Required types var FilterToken = $injector.get('FilterToken'); var IPv4Network = $injector.get('IPv4Network'); var IPv6Network = $injector.get('IPv6Network'); // Required services var $parse = $injector.get('$parse'); /** * Object which handles compilation of filtering predicates as used by * the Angular "filter" filter. Predicates are compiled from a user- * specified search string. * * @constructor * @param {String[]} expressions * The Angular expressions whose values are to be filtered. */ var FilterPattern = function FilterPattern(expressions) { /** * Reference to this instance. * * @type FilterPattern */ var filterPattern = this; /** * Filter predicate which simply matches everything. This function * always returns true. * * @returns {Boolean} * true. */ var nullPredicate = function nullPredicate() { return true; }; /** * Array of getters corresponding to the Angular expressions provided * to the constructor of this class. The functions returns are those * produced by the $parse service. * * @type Function[] */ var getters = []; // Parse all expressions angular.forEach(expressions, function parseExpression(expression) { getters.push($parse(expression)); }); /** * Determines whether the given object contains properties that match * the given string, according to the provided getters. * * @param {Object} object * The object to match against. * * @param {String} str * The string to match. * * @returns {Boolean} * true if the object matches the given string, false otherwise. */ var matchesString = function matchesString(object, str) { // For each defined getter for (var i=0; i < getters.length; i++) { // Retrieve value of current getter var value = getters[i](object); // If the value matches the pattern, the whole object matches if (String(value).toLowerCase().indexOf(str) !== -1) return true; } // No matches found return false; }; /** * Determines whether the given object contains properties that match * the given IPv4 network, according to the provided getters. * * @param {Object} object * The object to match against. * * @param {IPv4Network} network * The IPv4 network to match. * * @returns {Boolean} * true if the object matches the given network, false otherwise. */ var matchesIPv4 = function matchesIPv4(object, network) { // For each defined getter for (var i=0; i < getters.length; i++) { // Test value against IPv4 network var value = IPv4Network.parse(String(getters[i](object))); if (value && network.contains(value)) return true; } // No matches found return false; }; /** * Determines whether the given object contains properties that match * the given IPv6 network, according to the provided getters. * * @param {Object} object * The object to match against. * * @param {IPv6Network} network * The IPv6 network to match. * * @returns {Boolean} * true if the object matches the given network, false otherwise. */ var matchesIPv6 = function matchesIPv6(object, network) { // For each defined getter for (var i=0; i < getters.length; i++) { // Test value against IPv6 network var value = IPv6Network.parse(String(getters[i](object))); if (value && network.contains(value)) return true; } // No matches found return false; }; /** * Determines whether the given object matches the given filter pattern * token. * * @param {Object} object * The object to match the token against. * * @param {FilterToken} token * The token from the tokenized filter pattern to match aginst the * given object. * * @returns {Boolean} * true if the object matches the token, false otherwise. */ var matchesToken = function matchesToken(object, token) { // Match depending on token type switch (token.type) { // Simple string literal case 'LITERAL': return matchesString(object, token.value); // IPv4 network address / subnet case 'IPV4_NETWORK': return matchesIPv4(object, token.value); // IPv6 network address / subnet case 'IPV6_NETWORK': return matchesIPv6(object, token.value); // Unsupported token type default: return false; } }; /** * The current filtering predicate. * * @type Function */ this.predicate = nullPredicate; /** * Compiles the given pattern string, assigning the resulting filter * predicate. The resulting predicate will accept only objects that * match the given pattern. * * @param {String} pattern * The pattern to compile. */ this.compile = function compile(pattern) { // If no pattern provided, everything matches if (!pattern) { filterPattern.predicate = nullPredicate; return; } // Tokenize pattern, converting to lower case for case-insensitive matching var tokens = FilterToken.tokenize(pattern.toLowerCase()); // Return predicate which matches against the value of any getter in the getters array filterPattern.predicate = function matchesAllTokens(object) { // False if any token does not match for (var i=0; i < tokens.length; i++) { if (!matchesToken(object, tokens[i])) return false; } // True if all tokens matched return true; }; }; }; return FilterPattern; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/list/types/IPv6Network.js0100664 0001751 0001751 00000016223 12612226251 031053 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for defining the IPv6Network class. */ angular.module('list').factory('IPv6Network', [ function defineIPv6Network() { /** * Represents an IPv6 network as a pairing of base address and netmask, * both of which are in binary form. To obtain an IPv6Network from * standard CIDR notation, use IPv6Network.parse(). * * @constructor * @param {Number[]} addressGroups * Array of eight IPv6 address groups in binary form, each group being * 16-bit number. * * @param {Number[]} netmaskGroups * Array of eight IPv6 netmask groups in binary form, each group being * 16-bit number. */ var IPv6Network = function IPv6Network(addressGroups, netmaskGroups) { /** * Reference to this IPv6Network. * * @type IPv6Network */ var network = this; /** * The 128-bit binary address of this network as an array of eight * 16-bit numbers. * * @type Number[] */ this.addressGroups = addressGroups; /** * The 128-bit binary netmask of this network as an array of eight * 16-bit numbers. * * @type Number */ this.netmaskGroups = netmaskGroups; /** * Tests whether the given network is entirely within this network, * taking into account the base addresses and netmasks of both. * * @param {IPv6Network} other * The network to test. * * @returns {Boolean} * true if the other network is entirely within this network, false * otherwise. */ this.contains = function contains(other) { // Test that each masked 16-bit quantity matches the address for (var i=0; i < 8; i++) { if (network.addressGroups[i] !== (other.addressGroups[i] & other.netmaskGroups[i] & network.netmaskGroups[i])) return false; } // All 16-bit numbers match return true; }; }; /** * Generates a netmask having the given number of ones on the left side. * All other bits within the netmask will be zeroes. The resulting netmask * will be an array of eight numbers, where each number corresponds to a * 16-bit group of an IPv6 netmask. * * @param {Number} bits * The number of ones to include on the left side of the netmask. All * other bits will be zeroes. * * @returns {Number[]} * The generated netmask, having the given number of ones. */ var generateNetmask = function generateNetmask(bits) { var netmask = []; // Only generate up to 128 bits bits = Math.min(128, bits); // Add any contiguous 16-bit sections of ones while (bits >= 16) { netmask.push(0xFFFF); bits -= 16; } // Add remaining ones if (bits > 0 && bits <= 16) netmask.push(0xFFFF & (0xFFFF << (16 - bits))); // Add remaining zeroes while (netmask.length < 8) netmask.push(0); return netmask; }; /** * Splits the given IPv6 address or partial address into its corresponding * 16-bit groups. * * @param {String} str * The IPv6 address or partial address to split. * * @returns Number[] * The numeric values of all 16-bit groups within the given IPv6 * address. */ var splitAddress = function splitAddress(str) { var address = []; // Split address into groups var groups = str.split(':'); // Parse the numeric value of each group angular.forEach(groups, function addGroup(group) { var value = parseInt(group || '0', 16); address.push(value); }); return address; }; /** * Parses the given string as an IPv6 address or subnet, returning an * IPv6Network object which describes that address or subnet. * * @param {String} str * The string to parse. * * @returns {IPv6Network} * The parsed network, or null if the given string is not valid. */ IPv6Network.parse = function parse(str) { // Regex which matches the general form of IPv6 addresses var pattern = /^([0-9a-f]{0,4}(?::[0-9a-f]{0,4}){0,7})(?:\/([0-9]{1,3}))?$/; // Parse rudimentary IPv6 address via regex var match = pattern.exec(str); if (!match) return null; // Extract address and netmask from parse results var unparsedAddress = match[1]; var unparsedNetmask = match[2]; // Parse netmask var netmask; if (unparsedNetmask) netmask = generateNetmask(parseInt(unparsedNetmask)); else netmask = generateNetmask(128); var address; // Separate based on the double-colon, if present var doubleColon = unparsedAddress.indexOf('::'); // If no double colon, just split into groups if (doubleColon === -1) address = splitAddress(unparsedAddress); // Otherwise, split either side of the double colon and pad with zeroes else { // Parse either side of the double colon var leftAddress = splitAddress(unparsedAddress.substring(0, doubleColon)); var rightAddress = splitAddress(unparsedAddress.substring(doubleColon + 2)); // Pad with zeroes up to address length var remaining = 8 - leftAddress.length - rightAddress.length; while (remaining > 0) { leftAddress.push(0); remaining--; } address = leftAddress.concat(rightAddress); } // Validate length of address if (address.length !== 8) return null; return new IPv6Network(address, netmask); }; return IPv6Network; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/list/templates/guacPager.html0100664 0001751 0001751 00000004462 12612226251 031777 0ustar00mjumpermjumper0000000 0000000
...
  • {{pageNumber}}
...
guacamole-client-0.9.9/guacamole/src/main/webapp/app/list/templates/guacFilter.html0100664 0001751 0001751 00000002502 12612226251 032157 0ustar00mjumpermjumper0000000 0000000
guacamole-client-0.9.9/guacamole/src/main/webapp/app/home/directives/guacRecentConnections.js0100664 0001751 0001751 00000017737 12612226251 034165 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which displays the contents of a connection group. */ angular.module('home').directive('guacRecentConnections', [function guacRecentConnections() { return { restrict: 'E', replace: true, scope: { /** * The root connection groups to display, and all visible * descendants, as a map of data source identifier to the root * connection group within that data source. Recent connections * will only be shown if they exist within this hierarchy, * regardless of their existence within the history. * * @type Object. */ rootGroups : '=' }, templateUrl: 'app/home/templates/guacRecentConnections.html', controller: ['$scope', '$injector', function guacRecentConnectionsController($scope, $injector) { // Required types var ActiveConnection = $injector.get('ActiveConnection'); var ClientIdentifier = $injector.get('ClientIdentifier'); var RecentConnection = $injector.get('RecentConnection'); // Required services var guacClientManager = $injector.get('guacClientManager'); var guacHistory = $injector.get('guacHistory'); /** * Array of all known and visible active connections. * * @type ActiveConnection[] */ $scope.activeConnections = []; /** * Array of all known and visible recently-used connections. * * @type RecentConnection[] */ $scope.recentConnections = []; /** * Returns whether recent connections are available for display. * Note that, for the sake of this directive, recent connections * include any currently-active connections, even if they are not * yet in the history. * * @returns {Boolean} * true if recent (or active) connections are present, false * otherwise. */ $scope.hasRecentConnections = function hasRecentConnections() { return !!($scope.activeConnections.length || $scope.recentConnections.length); }; /** * Map of all visible objects, connections or connection groups, by * object identifier. * * @type Object. */ var visibleObjects = {}; /** * Adds the given connection to the internal set of visible * objects. * * @param {String} dataSource * The identifier of the data source associated with the * given connection group. * * @param {Connection} connection * The connection to add to the internal set of visible objects. */ var addVisibleConnection = function addVisibleConnection(dataSource, connection) { // Add given connection to set of visible objects visibleObjects[ClientIdentifier.toString({ dataSource : dataSource, type : ClientIdentifier.Types.CONNECTION, id : connection.identifier })] = connection; }; /** * Adds the given connection group to the internal set of visible * objects, along with any descendants. * * @param {String} dataSource * The identifier of the data source associated with the * given connection group. * * @param {ConnectionGroup} connectionGroup * The connection group to add to the internal set of visible * objects, along with any descendants. */ var addVisibleConnectionGroup = function addVisibleConnectionGroup(dataSource, connectionGroup) { // Add given connection group to set of visible objects visibleObjects[ClientIdentifier.toString({ dataSource : dataSource, type : ClientIdentifier.Types.CONNECTION_GROUP, id : connectionGroup.identifier })] = connectionGroup; // Add all child connections if (connectionGroup.childConnections) connectionGroup.childConnections.forEach(function addChildConnection(child) { addVisibleConnection(dataSource, child); }); // Add all child connection groups if (connectionGroup.childConnectionGroups) connectionGroup.childConnectionGroups.forEach(function addChildConnectionGroup(child) { addVisibleConnectionGroup(dataSource, child); }); }; // Update visible objects when root groups are set $scope.$watch("rootGroups", function setRootGroups(rootGroups) { // Clear connection arrays $scope.activeConnections = []; $scope.recentConnections = []; // Produce collection of visible objects visibleObjects = {}; if (rootGroups) { angular.forEach(rootGroups, function addConnectionGroup(rootGroup, dataSource) { addVisibleConnectionGroup(dataSource, rootGroup); }); } var managedClients = guacClientManager.getManagedClients(); // Add all active connections for (var id in managedClients) { // Get corresponding managed client var client = managedClients[id]; // Add active connections for clients with associated visible objects if (id in visibleObjects) { var object = visibleObjects[id]; $scope.activeConnections.push(new ActiveConnection(object.name, client)); } } // Add any recent connections that are visible guacHistory.recentConnections.forEach(function addRecentConnection(historyEntry) { // Add recent connections for history entries with associated visible objects if (historyEntry.id in visibleObjects && !(historyEntry.id in managedClients)) { var object = visibleObjects[historyEntry.id]; $scope.recentConnections.push(new RecentConnection(object.name, historyEntry)); } }); }); // end rootGroup scope watch }] }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/home/homeModule.js0100664 0001751 0001751 00000002261 12612226251 027621 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ angular.module('home', ['client', 'groupList', 'history', 'navigation', 'rest']); guacamole-client-0.9.9/guacamole/src/main/webapp/app/home/styles/home.css0100664 0001751 0001751 00000003273 12631654532 030166 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .history-unavailable div.recent-connections { display: none; } div.recent-connections, div.clipboardDiv, div.settings, div.all-connections { margin: 1em; padding: 0; } .all-connections .list-buttons { text-align: center; padding: 0; } div.recent-connections { text-align: center; } div.recent-connections div.connection { -moz-border-radius: 0.5em; -webkit-border-radius: 0.5em; -khtml-border-radius: 0.5em; border-radius: 0.5em; display: inline-block; padding: 1em; margin: 1em; text-align: center; max-width: 75%; overflow: hidden; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/home/controllers/homeController.js0100664 0001751 0001751 00000011125 12645510255 033072 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The controller for the home page. */ angular.module('home').controller('homeController', ['$scope', '$injector', function homeController($scope, $injector) { // Get required types var ConnectionGroup = $injector.get('ConnectionGroup'); var ClientIdentifier = $injector.get('ClientIdentifier'); // Get required services var authenticationService = $injector.get('authenticationService'); var connectionGroupService = $injector.get('connectionGroupService'); var dataSourceService = $injector.get('dataSourceService'); /** * Map of data source identifier to the root connection group of that data * source, or null if the connection group hierarchy has not yet been * loaded. * * @type Object. */ $scope.rootConnectionGroups = null; /** * Array of all connection properties that are filterable. * * @type String[] */ $scope.filteredConnectionProperties = [ 'name' ]; /** * Array of all connection group properties that are filterable. * * @type String[] */ $scope.filteredConnectionGroupProperties = [ 'name' ]; /** * Returns whether critical data has completed being loaded. * * @returns {Boolean} * true if enough data has been loaded for the user interface to be * useful, false otherwise. */ $scope.isLoaded = function isLoaded() { return $scope.rootConnectionGroup !== null; }; /** * Object passed to the guacGroupList directive, providing context-specific * functions or data. */ $scope.context = { /** * Returns the unique string identifier which must be used when * connecting to a connection or connection group represented by the * given GroupListItem. * * @param {GroupListItem} item * The GroupListItem to determine the client identifier of. * * @returns {String} * The client identifier associated with the connection or * connection group represented by the given GroupListItem, or null * if the GroupListItem cannot have an associated client * identifier. */ getClientIdentifier : function getClientIdentifier(item) { // If the item is a connection, generate a connection identifier if (item.isConnection) return ClientIdentifier.toString({ dataSource : item.dataSource, type : ClientIdentifier.Types.CONNECTION, id : item.identifier }); // If the item is a connection, generate a connection group identifier if (item.isConnectionGroup) return ClientIdentifier.toString({ dataSource : item.dataSource, type : ClientIdentifier.Types.CONNECTION_GROUP, id : item.identifier }); // Otherwise, no such identifier can exist return null; } }; // Retrieve root groups and all descendants dataSourceService.apply( connectionGroupService.getConnectionGroupTree, authenticationService.getAvailableDataSources(), ConnectionGroup.ROOT_IDENTIFIER ) .then(function rootGroupsRetrieved(rootConnectionGroups) { $scope.rootConnectionGroups = rootConnectionGroups; }); }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/home/types/ActiveConnection.js0100664 0001751 0001751 00000003514 12612226251 032124 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the ActiveConnection class used by the guacRecentConnections * directive. */ angular.module('home').factory('ActiveConnection', [function defineActiveConnection() { /** * A recently-user connection, visible to the current user, with an * associated history entry. * * @constructor */ var ActiveConnection = function ActiveConnection(name, client) { /** * The human-readable name of this connection. * * @type String */ this.name = name; /** * The client associated with this active connection. * * @type ManagedClient */ this.client = client; }; return ActiveConnection; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/home/types/RecentConnection.js0100664 0001751 0001751 00000003516 12612226251 032133 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the RecentConnection class used by the guacRecentConnections * directive. */ angular.module('home').factory('RecentConnection', [function defineRecentConnection() { /** * A recently-user connection, visible to the current user, with an * associated history entry. * * @constructor */ var RecentConnection = function RecentConnection(name, entry) { /** * The human-readable name of this connection. * * @type String */ this.name = name; /** * The history entry associated with this recent connection. * * @type HistoryEntry */ this.entry = entry; }; return RecentConnection; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/home/templates/guacRecentConnections.html0100664 0001751 0001751 00000004653 12612226251 034343 0ustar00mjumpermjumper0000000 0000000

{{'HOME.INFO_NO_RECENT_CONNECTIONS' | translate}}

guacamole-client-0.9.9/guacamole/src/main/webapp/app/home/templates/connection.html0100664 0001751 0001751 00000003431 12612226251 032210 0ustar00mjumpermjumper0000000 0000000
{{item.name}}
guacamole-client-0.9.9/guacamole/src/main/webapp/app/home/templates/connectionGroup.html0100664 0001751 0001751 00000002547 12612226251 033234 0ustar00mjumpermjumper0000000 0000000 {{item.name}} {{item.name}} guacamole-client-0.9.9/guacamole/src/main/webapp/app/home/templates/home.html0100664 0001751 0001751 00000005002 12645510255 031003 0ustar00mjumpermjumper0000000 0000000

{{'HOME.SECTION_HEADER_RECENT_CONNECTIONS' | translate}}

{{'HOME.SECTION_HEADER_ALL_CONNECTIONS' | translate}}

guacamole-client-0.9.9/guacamole/src/main/webapp/app/element/directives/guacScroll.js0100664 0001751 0001751 00000005777 12612226251 032502 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which allows elements to be manually scrolled, and for their * scroll state to be observed. */ angular.module('element').directive('guacScroll', [function guacScroll() { return { restrict: 'A', link: function linkGuacScroll($scope, $element, $attrs) { /** * The current scroll state of the element. * * @type ScrollState */ var guacScroll = $scope.$eval($attrs.guacScroll); /** * The element which is being scrolled, or monitored for changes * in scroll. * * @type Element */ var element = $element[0]; /** * Returns the current left edge of the scrolling rectangle. * * @returns {Number} * The current left edge of the scrolling rectangle. */ var getScrollLeft = function getScrollLeft() { return guacScroll.left; }; /** * Returns the current top edge of the scrolling rectangle. * * @returns {Number} * The current top edge of the scrolling rectangle. */ var getScrollTop = function getScrollTop() { return guacScroll.top; }; // Update underlying scrollLeft property when left changes $scope.$watch(getScrollLeft, function scrollLeftChanged(left) { element.scrollLeft = left; guacScroll.left = element.scrollLeft; }); // Update underlying scrollTop property when top changes $scope.$watch(getScrollTop, function scrollTopChanged(top) { element.scrollTop = top; guacScroll.top = element.scrollTop; }); } // end guacScroll link function }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/element/directives/guacResize.js0100664 0001751 0001751 00000010267 12612226251 032473 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which calls a given callback when its associated element is * resized. This will modify the internal DOM tree of the associated element, * and the associated element MUST have position (for example, * "position: relative"). */ angular.module('element').directive('guacResize', ['$document', function guacResize($document) { return { restrict: 'A', link: function linkGuacResize($scope, $element, $attrs) { /** * The function to call whenever the associated element is * resized. The function will be passed the width and height of * the element, in pixels. * * @type Function */ var guacResize = $scope.$eval($attrs.guacResize); /** * The element which will monitored for size changes. * * @type Element */ var element = $element[0]; /** * The resize sensor - an HTML object element. * * @type HTMLObjectElement */ var resizeSensor = $document[0].createElement('object'); /** * The width of the associated element, in pixels. * * @type Number */ var lastWidth = element.offsetWidth; /** * The height of the associated element, in pixels. * * @type Number */ var lastHeight = element.offsetHeight; /** * Checks whether the size of the associated element has changed * and, if so, calls the resize callback with the new width and * height as parameters. */ var checkSize = function checkSize() { // Call callback only if size actually changed if (element.offsetWidth !== lastWidth || element.offsetHeight !== lastHeight) { // Call resize callback, if defined if (guacResize) { $scope.$evalAsync(function elementSizeChanged() { guacResize(element.offsetWidth, element.offsetHeight); }); } // Update stored size lastWidth = element.offsetWidth; lastHeight = element.offsetHeight; } }; // Register event listener once window object exists resizeSensor.onload = function resizeSensorReady() { resizeSensor.contentDocument.defaultView.addEventListener('resize', checkSize); checkSize(); }; // Load blank contents resizeSensor.className = 'resize-sensor'; resizeSensor.type = 'text/html'; resizeSensor.data = 'app/element/templates/blank.html'; // Add resize sensor to associated element element.insertBefore(resizeSensor, element.firstChild); } // end guacResize link function }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/element/directives/guacUpload.js0100664 0001751 0001751 00000006415 12612226251 032456 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which allows multiple files to be uploaded. Clicking on the * associated element will result in a file selector dialog, which then calls * the provided callback function with any chosen files. */ angular.module('element').directive('guacUpload', ['$document', function guacUpload($document) { return { restrict: 'A', link: function linkGuacUpload($scope, $element, $attrs) { /** * The function to call whenever files are chosen. The callback is * provided a single parameter: the FileList containing all chosen * files. * * @type Function */ var guacUpload = $scope.$eval($attrs.guacUpload); /** * The element which will register the drag gesture. * * @type Element */ var element = $element[0]; /** * Internal form, containing a single file input element. * * @type HTMLFormElement */ var form = $document[0].createElement('form'); /** * Internal file input element. * * @type HTMLInputElement */ var input = $document[0].createElement('input'); // Init input element input.type = 'file'; input.multiple = true; // Add input element to internal form form.appendChild(input); // Notify of any chosen files input.addEventListener('change', function filesSelected() { $scope.$apply(function setSelectedFiles() { // Only set chosen files selection is not canceled if (guacUpload && input.files.length > 0) guacUpload(input.files); // Reset selection form.reset(); }); }); // Open file chooser when element is clicked element.addEventListener('click', function elementClicked() { input.click(); }); } // end guacUpload link function }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/element/directives/guacFocus.js0100664 0001751 0001751 00000005332 12612226251 032306 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which allows elements to be manually focused / blurred. */ angular.module('element').directive('guacFocus', ['$parse', function guacFocus($parse) { return { restrict: 'A', link: function linkGuacFocus($scope, $element, $attrs) { /** * Whether the element associated with this directive should be * focussed. * * @type Boolean */ var guacFocus = $parse($attrs.guacFocus); /** * The element which will be focused / blurred. * * @type Element */ var element = $element[0]; // Set/unset focus depending on value of guacFocus $scope.$watch(guacFocus, function updateFocus(value) { $scope.$evalAsync(function updateFocusAsync() { if (value) element.focus(); else element.blur(); }); }); // Set focus flag when focus is received element.addEventListener('focus', function focusReceived() { $scope.$evalAsync(function setGuacFocusAsync() { guacFocus.assign($scope, true); }); }); // Unset focus flag when focus is lost element.addEventListener('blur', function focusLost() { $scope.$evalAsync(function unsetGuacFocusAsync() { guacFocus.assign($scope, false); }); }); } // end guacFocus link function }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/element/directives/guacMarker.js0100664 0001751 0001751 00000004206 12612226251 032447 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which stores a marker which refers to a specific element, * allowing that element to be scrolled into view when desired. */ angular.module('element').directive('guacMarker', ['$injector', function guacMarker($injector) { // Required types var Marker = $injector.get('Marker'); // Required services var $parse = $injector.get('$parse'); return { restrict: 'A', link: function linkGuacMarker($scope, $element, $attrs) { /** * The property in which a new Marker should be stored. The new * Marker will refer to the element associated with this directive. * * @type Marker */ var guacMarker = $parse($attrs.guacMarker); /** * The element to associate with the new Marker. * * @type Element */ var element = $element[0]; // Assign new marker guacMarker.assign($scope, new Marker(element)); } }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/element/elementModule.js0100664 0001751 0001751 00000002371 12612226251 031025 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Module for manipulating element state, such as focus or scroll position, as * well as handling browser events. */ angular.module('element', []); guacamole-client-0.9.9/guacamole/src/main/webapp/app/element/styles/resize-sensor.css0100664 0001751 0001751 00000002417 12612226251 032536 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .resize-sensor { height: 100%; width: 100%; position: absolute; left: 0; top: 0; overflow: hidden; border: none; opacity: 0; z-index: -1; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/element/types/Marker.js0100664 0001751 0001751 00000003436 12612226251 030616 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the Marker class definition. */ angular.module('element').factory('Marker', [function defineMarker() { /** * Creates a new Marker which allows its associated element to be scolled * into view as desired. * * @constructor * @param {Element} element * The element to associate with this marker. */ var Marker = function Marker(element) { /** * Scrolls scrollable elements, or the window, as needed to bring the * element associated with this marker into view. */ this.scrollIntoView = function scrollIntoView() { element.scrollIntoView(); }; }; return Marker; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/element/types/ScrollState.js0100664 0001751 0001751 00000004435 12612226251 031634 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the ScrollState class definition. */ angular.module('element').factory('ScrollState', [function defineScrollState() { /** * Creates a new ScrollState, representing the current scroll position of * an arbitrary element. This constructor initializes the properties of the * new ScrollState with the corresponding properties of the given template. * * @constructor * @param {ScrollState|Object} [template={}] * The object whose properties should be copied within the new * ScrollState. */ var ScrollState = function ScrollState(template) { // Use empty object by default template = template || {}; /** * The left edge of the view rectangle within the scrollable area. This * value naturally increases as the user scrolls right. * * @type Number */ this.left = template.left || 0; /** * The top edge of the view rectangle within the scrollable area. This * value naturally increases as the user scrolls down. * * @type Number */ this.top = template.top || 0; }; return ScrollState; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/element/templates/blank.html0100664 0001751 0001751 00000002517 12612226251 031645 0ustar00mjumpermjumper0000000 0000000 _ guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/services/formService.js0100664 0001751 0001751 00000020637 12612226251 031654 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for maintaining form-related metadata and linking that data to * corresponding controllers and templates. */ angular.module('form').provider('formService', function formServiceProvider() { /** * Reference to the provider itself. * * @type formServiceProvider */ var provider = this; /** * Map of all registered field type definitions by name. * * @type Object. */ this.fieldTypes = { /** * Text field type. * * @see {@link Field.Type.TEXT} * @type FieldType */ 'TEXT' : { templateUrl : 'app/form/templates/textField.html' }, /** * Numeric field type. * * @see {@link Field.Type.NUMERIC} * @type FieldType */ 'NUMERIC' : { module : 'form', controller : 'numberFieldController', templateUrl : 'app/form/templates/numberField.html' }, /** * Boolean field type. * * @see {@link Field.Type.BOOLEAN} * @type FieldType */ 'BOOLEAN' : { module : 'form', controller : 'checkboxFieldController', templateUrl : 'app/form/templates/checkboxField.html' }, /** * Username field type. Identical in principle to a text field, but may * have different semantics. * * @see {@link Field.Type.USERNAME} * @type FieldType */ 'USERNAME' : { templateUrl : 'app/form/templates/textField.html' }, /** * Password field type. Similar to a text field, but the contents of * the field are masked. * * @see {@link Field.Type.PASSWORD} * @type FieldType */ 'PASSWORD' : { module : 'form', controller : 'passwordFieldController', templateUrl : 'app/form/templates/passwordField.html' }, /** * Enumerated field type. The user is presented a finite list of values * to choose from. * * @see {@link Field.Type.ENUM} * @type FieldType */ 'ENUM' : { module : 'form', controller : 'selectFieldController', templateUrl : 'app/form/templates/selectField.html' }, /** * Multiline field type. The user may enter multiple lines of text. * * @see {@link Field.Type.MULTILINE} * @type FieldType */ 'MULTILINE' : { templateUrl : 'app/form/templates/textAreaField.html' }, /** * Field type which allows selection of time zones. * * @see {@link Field.Type.TIMEZONE} * @type FieldType */ 'TIMEZONE' : { module : 'form', controller : 'timeZoneFieldController', templateUrl : 'app/form/templates/timeZoneField.html' }, /** * Field type which allows selection of individual dates. * * @see {@link Field.Type.DATE} * @type FieldType */ 'DATE' : { module : 'form', controller : 'dateFieldController', templateUrl : 'app/form/templates/dateField.html' }, /** * Field type which allows selection of times of day. * * @see {@link Field.Type.TIME} * @type FieldType */ 'TIME' : { module : 'form', controller : 'timeFieldController', templateUrl : 'app/form/templates/timeField.html' } }; /** * Registers a new field type under the given name. * * @param {String} fieldTypeName * The name which uniquely identifies the field type being registered. * * @param {FieldType} fieldType * The field type definition to associate with the given name. */ this.registerFieldType = function registerFieldType(fieldTypeName, fieldType) { // Store field type provider.fieldTypes[fieldTypeName] = fieldType; }; // Factory method required by provider this.$get = ['$injector', function formServiceFactory($injector) { // Required services var $compile = $injector.get('$compile'); var $q = $injector.get('$q'); var $templateRequest = $injector.get('$templateRequest'); var service = {}; service.fieldTypes = provider.fieldTypes; /** * Compiles and links the field associated with the given name to the given * scope, producing a distinct and independent DOM Element which functions * as an instance of that field. The scope object provided must include at * least the following properties: * * namespace: * A String which defines the unique namespace associated the * translation strings used by the form using a field of this type. * * field: * The Field object that is being rendered, representing a field of * this type. * * model: * The current String value of the field, if any. * * @param {String} fieldTypeName * The name of the field type defining the nature of the element to be * created. * * @param {Object} scope * The scope to which the new element will be linked. * * @return {Promise.} * A Promise which resolves to the compiled Element. If an error occurs * while retrieving the field type, this Promise will be rejected. */ service.createFieldElement = function createFieldElement(fieldTypeName, scope) { // Ensure field type is defined var fieldType = provider.fieldTypes[fieldTypeName]; if (!fieldType) return $q.reject(); // Populate scope using defined controller if (fieldType.module && fieldType.controller) { var $controller = angular.injector(['ng', fieldType.module]).get('$controller'); $controller(fieldType.controller, {'$scope' : scope}); } // Defer compilation of template pending successful retrieval var compiledTemplate = $q.defer(); // Use raw HTML template if provided if (fieldType.template) compiledTemplate.resolve($compile(fieldType.template)(scope)); // If no raw HTML template is provided, retrieve template from URL else { // Attempt to retrieve template HTML $templateRequest(fieldType.templateUrl) // Resolve with compiled HTML upon success .then(function templateRetrieved(html) { compiledTemplate.resolve($compile(html)(scope)); }) // Reject on failure ['catch'](function templateError() { compiledTemplate.reject(); }); } // Return promise which resolves to the compiled template return compiledTemplate.promise; }; return service; }]; }); guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/directives/formField.js0100664 0001751 0001751 00000010642 12612226251 031610 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive that allows editing of a field. */ angular.module('form').directive('guacFormField', [function formField() { return { // Element only restrict: 'E', replace: true, scope: { /** * The translation namespace of the translation strings that will * be generated for this field. This namespace is absolutely * required. If this namespace is omitted, all generated * translation strings will be placed within the MISSING_NAMESPACE * namespace, as a warning. * * @type String */ namespace : '=', /** * The field to display. * * @type Field */ field : '=', /** * The property which contains this fields current value. When this * field changes, the property will be updated accordingly. * * @type String */ model : '=' }, templateUrl: 'app/form/templates/formField.html', controller: ['$scope', '$injector', '$element', function formFieldController($scope, $injector, $element) { // Required services var formService = $injector.get('formService'); var translationStringService = $injector.get('translationStringService'); /** * The element which should contain any compiled field content. The * actual content of a field is dynamically determined by its type. * * @type Element[] */ var fieldContent = $element.find('.form-field'); /** * Produces the translation string for the header of the current * field. The translation string will be of the form: * * NAMESPACE.FIELD_HEADER_NAME * * where NAMESPACE is the namespace provided to the * directive and NAME is the field name transformed * via translationStringService.canonicalize(). * * @returns {String} * The translation string which produces the translated header * of the field. */ $scope.getFieldHeader = function getFieldHeader() { // If no field, or no name, then no header if (!$scope.field || !$scope.field.name) return ''; return translationStringService.canonicalize($scope.namespace || 'MISSING_NAMESPACE') + '.FIELD_HEADER_' + translationStringService.canonicalize($scope.field.name); }; // Update field contents when field definition is changed $scope.$watch('field', function setField(field) { // Reset contents fieldContent.innerHTML = ''; // Append field content if (field) { formService.createFieldElement(field.type, $scope) .then(function fieldElementCreated(element) { fieldContent.append(element); }); } }); }] // end controller }; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/directives/form.js0100664 0001751 0001751 00000014122 12612226251 030641 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive that allows editing of a collection of fields. */ angular.module('form').directive('guacForm', [function form() { return { // Element only restrict: 'E', replace: true, scope: { /** * The translation namespace of the translation strings that will * be generated for all fields. This namespace is absolutely * required. If this namespace is omitted, all generated * translation strings will be placed within the MISSING_NAMESPACE * namespace, as a warning. * * @type String */ namespace : '=', /** * The form content to display. This may be a form, an array of * forms, or a simple array of fields. * * @type Form[]|Form|Field[]|Field */ content : '=', /** * The object which will receive all field values. Each field value * will be assigned to the property of this object having the same * name. * * @type Object. */ model : '=' }, templateUrl: 'app/form/templates/form.html', controller: ['$scope', '$injector', function formController($scope, $injector) { // Required services var translationStringService = $injector.get('translationStringService'); /** * The array of all forms to display. * * @type Form[] */ $scope.forms = []; /** * The object which will receive all field values. Normally, this * will be the object provided within the "model" attribute. If * no such object has been provided, a blank model will be used * instead as a placeholder, such that the fields of this form * will have something to bind to. * * @type Object. */ $scope.values = {}; /** * Produces the translation string for the section header of the * given form. The translation string will be of the form: * * NAMESPACE.SECTION_HEADER_NAME * * where NAMESPACE is the namespace provided to the * directive and NAME is the form name transformed * via translationStringService.canonicalize(). * * @param {Form} form * The form for which to produce the translation string. * * @returns {String} * The translation string which produces the translated header * of the form. */ $scope.getSectionHeader = function getSectionHeader(form) { // If no form, or no name, then no header if (!form || !form.name) return ''; return translationStringService.canonicalize($scope.namespace || 'MISSING_NAMESPACE') + '.SECTION_HEADER_' + translationStringService.canonicalize(form.name); }; /** * Determines whether the given object is a form, under the * assumption that the object is either a form or a field. * * @param {Form|Field} obj * The object to test. * * @returns {Boolean} * true if the given object appears to be a form, false * otherwise. */ var isForm = function isForm(obj) { return !!('name' in obj && 'fields' in obj); }; // Produce set of forms from any given content $scope.$watch('content', function setContent(content) { // If no content provided, there are no forms if (!content) { $scope.forms = []; return; } // Ensure content is an array if (!angular.isArray(content)) content = [content]; // If content is an array of fields, convert to an array of forms if (content.length && !isForm(content[0])) { content = [{ fields : content }]; } // Content is now an array of forms $scope.forms = content; }); // Update string value and re-assign to model when field is changed $scope.$watch('model', function setModel(model) { // Assign new model only if provided if (model) $scope.values = model; // Otherwise, use blank model else $scope.values = {}; }); }] // end controller }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/directives/guacLenientTime.js0100664 0001751 0001751 00000006640 12612226251 032761 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which modifies the parsing and formatting of ngModel when used * on an HTML5 time input field, relaxing the otherwise strict parsing and * validation behavior. The behavior of this directive for other input elements * is undefined. */ angular.module('form').directive('guacLenientTime', ['$injector', function guacLenientTime($injector) { // Required services var $filter = $injector.get('$filter'); /** * Directive configuration object. * * @type Object. */ var config = { restrict : 'A', require : 'ngModel' }; // Linking function config.link = function linkGuacLenientTIme($scope, $element, $attrs, ngModel) { // Parse time strings leniently ngModel.$parsers = [function parse(viewValue) { // If blank, return null if (!viewValue) return null; // Match basic time pattern var match = /([0-9]*)(?::([0-9]*)(?::([0-9]*))?)?(?:\s*(a|p))?/.exec(viewValue.toLowerCase()); if (!match) return null; // Determine hour, minute, and second based on pattern var hour = parseInt(match[1] || '0'); var minute = parseInt(match[2] || '0'); var second = parseInt(match[3] || '0'); // Handle AM/PM if (match[4]) { // Interpret 12 AM as 00:00 and 12 PM as 12:00 if (hour === 12) hour = 0; // Increment hour to evening if PM if (match[4] === 'p') hour += 12; } // Wrap seconds and minutes into minutes and hours minute += second / 60; second %= 60; hour += minute / 60; minute %= 60; // Constrain hours to 0 - 23 hour %= 24; // Convert to Date object var parsedDate = new Date(Date.UTC(1970, 0, 1, hour, minute, second)); if (isNaN(parsedDate.getTime())) return null; return parsedDate; }]; // Format time strings as "HH:mm:ss" ngModel.$formatters = [function format(modelValue) { return modelValue ? $filter('date')(modelValue, 'HH:mm:ss', 'UTC') : ''; }]; }; return config; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/directives/guacLenientDate.js0100664 0001751 0001751 00000005567 12612226251 032747 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which modifies the parsing and formatting of ngModel when used * on an HTML5 date input field, relaxing the otherwise strict parsing and * validation behavior. The behavior of this directive for other input elements * is undefined. */ angular.module('form').directive('guacLenientDate', ['$injector', function guacLenientDate($injector) { // Required services var $filter = $injector.get('$filter'); /** * Directive configuration object. * * @type Object. */ var config = { restrict : 'A', require : 'ngModel' }; // Linking function config.link = function linkGuacLenientDate($scope, $element, $attrs, ngModel) { // Parse date strings leniently ngModel.$parsers = [function parse(viewValue) { // If blank, return null if (!viewValue) return null; // Match basic date pattern var match = /([0-9]*)(?:-([0-9]*)(?:-([0-9]*))?)?/.exec(viewValue); if (!match) return null; // Determine year, month, and day based on pattern var year = parseInt(match[1] || '0') || new Date().getFullYear(); var month = parseInt(match[2] || '0') || 1; var day = parseInt(match[3] || '0') || 1; // Convert to Date object var parsedDate = new Date(Date.UTC(year, month - 1, day)); if (isNaN(parsedDate.getTime())) return null; return parsedDate; }]; // Format date strings as "yyyy-MM-dd" ngModel.$formatters = [function format(modelValue) { return modelValue ? $filter('date')(modelValue, 'yyyy-MM-dd', 'UTC') : ''; }]; }; return config; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/styles/form.css0100664 0001751 0001751 00000002300 12612226251 030172 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .form table.fields th { text-align: left; font-weight: normal; padding-right: 1em; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/styles/form-field.css0100664 0001751 0001751 00000003444 12612226251 031265 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /* Keep toggle-password icon on same line */ .form-field .password-field { white-space: nowrap; } /* Generic 1x1em icon/button */ .form-field .password-field .icon.toggle-password { display: inline-block; opacity: 0.5; cursor: default; background-repeat: no-repeat; background-size: 1em; width: 1em; height: 1em; } /* Icon for unmasking passwords */ .form-field .password-field input[type=password] ~ .icon.toggle-password { background-image: url('images/action-icons/guac-show-pass.png'); } /* Icon for masking passwords */ .form-field .password-field input[type=text] ~ .icon.toggle-password { background-image: url('images/action-icons/guac-hide-pass.png'); } guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/controllers/timeFieldController.js0100664 0001751 0001751 00000006067 12612226251 034062 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Controller for time fields. */ angular.module('form').controller('timeFieldController', ['$scope', '$injector', function timeFieldController($scope, $injector) { // Required services var $filter = $injector.get('$filter'); /** * Options which dictate the behavior of the input field model, as defined * by https://docs.angularjs.org/api/ng/directive/ngModelOptions * * @type Object. */ $scope.modelOptions = { /** * Space-delimited list of events on which the model will be updated. * * @type String */ updateOn : 'blur', /** * The time zone to use when reading/writing the Date object of the * model. * * @type String */ timezone : 'UTC' }; /** * Parses the time components of the given string into a Date with only the * time components set. The resulting Date will be in the UTC timezone, * with the date left as 1970-01-01. The input string must be in the format * HH:MM:SS (zero-padded, 24-hour). * * @param {String} str * The time string to parse. * * @returns {Date} * A Date object, in the UTC timezone, with only the time components * set. */ var parseTime = function parseTime(str) { // Parse time, return blank if invalid var parsedDate = new Date('1970-01-01T' + str + 'Z'); if (isNaN(parsedDate.getTime())) return null; return parsedDate; }; // Update typed value when model is changed $scope.$watch('model', function modelChanged(model) { $scope.typedValue = (model ? parseTime(model) : null); }); // Update string value in model when typed value is changed $scope.$watch('typedValue', function typedValueChanged(typedValue) { $scope.model = (typedValue ? $filter('date')(typedValue, 'HH:mm:ss', 'UTC') : ''); }); }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/controllers/timeZoneFieldController.js0100664 0001751 0001751 00000077754 12612226251 034731 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Controller for time zone fields. Time zone fields use Java IDs as the * standard representation for each supported time zone. */ angular.module('form').controller('timeZoneFieldController', ['$scope', '$injector', function timeZoneFieldController($scope, $injector) { /** * Map of time zone regions to the map of all time zone name/ID pairs * within those regions. * * @type Object.> */ $scope.timeZones = { "Africa" : { "Abidjan" : "Africa/Abidjan", "Accra" : "Africa/Accra", "Addis Ababa" : "Africa/Addis_Ababa", "Algiers" : "Africa/Algiers", "Asmara" : "Africa/Asmara", "Asmera" : "Africa/Asmera", "Bamako" : "Africa/Bamako", "Bangui" : "Africa/Bangui", "Banjul" : "Africa/Banjul", "Bissau" : "Africa/Bissau", "Blantyre" : "Africa/Blantyre", "Brazzaville" : "Africa/Brazzaville", "Bujumbura" : "Africa/Bujumbura", "Cairo" : "Africa/Cairo", "Casablanca" : "Africa/Casablanca", "Ceuta" : "Africa/Ceuta", "Conakry" : "Africa/Conakry", "Dakar" : "Africa/Dakar", "Dar es Salaam" : "Africa/Dar_es_Salaam", "Djibouti" : "Africa/Djibouti", "Douala" : "Africa/Douala", "El Aaiun" : "Africa/El_Aaiun", "Freetown" : "Africa/Freetown", "Gaborone" : "Africa/Gaborone", "Harare" : "Africa/Harare", "Johannesburg" : "Africa/Johannesburg", "Juba" : "Africa/Juba", "Kampala" : "Africa/Kampala", "Khartoum" : "Africa/Khartoum", "Kigali" : "Africa/Kigali", "Kinshasa" : "Africa/Kinshasa", "Lagos" : "Africa/Lagos", "Libreville" : "Africa/Libreville", "Lome" : "Africa/Lome", "Luanda" : "Africa/Luanda", "Lubumbashi" : "Africa/Lubumbashi", "Lusaka" : "Africa/Lusaka", "Malabo" : "Africa/Malabo", "Maputo" : "Africa/Maputo", "Maseru" : "Africa/Maseru", "Mbabane" : "Africa/Mbabane", "Mogadishu" : "Africa/Mogadishu", "Monrovia" : "Africa/Monrovia", "Nairobi" : "Africa/Nairobi", "Ndjamena" : "Africa/Ndjamena", "Niamey" : "Africa/Niamey", "Nouakchott" : "Africa/Nouakchott", "Ouagadougou" : "Africa/Ouagadougou", "Porto-Novo" : "Africa/Porto-Novo", "Sao Tome" : "Africa/Sao_Tome", "Timbuktu" : "Africa/Timbuktu", "Tripoli" : "Africa/Tripoli", "Tunis" : "Africa/Tunis", "Windhoek" : "Africa/Windhoek" }, "America" : { "Adak" : "America/Adak", "Anchorage" : "America/Anchorage", "Anguilla" : "America/Anguilla", "Antigua" : "America/Antigua", "Araguaina" : "America/Araguaina", "Argentina / Buenos Aires" : "America/Argentina/Buenos_Aires", "Argentina / Catamarca" : "America/Argentina/Catamarca", "Argentina / Comodoro Rivadavia" : "America/Argentina/ComodRivadavia", "Argentina / Cordoba" : "America/Argentina/Cordoba", "Argentina / Jujuy" : "America/Argentina/Jujuy", "Argentina / La Rioja" : "America/Argentina/La_Rioja", "Argentina / Mendoza" : "America/Argentina/Mendoza", "Argentina / Rio Gallegos" : "America/Argentina/Rio_Gallegos", "Argentina / Salta" : "America/Argentina/Salta", "Argentina / San Juan" : "America/Argentina/San_Juan", "Argentina / San Luis" : "America/Argentina/San_Luis", "Argentina / Tucuman" : "America/Argentina/Tucuman", "Argentina / Ushuaia" : "America/Argentina/Ushuaia", "Aruba" : "America/Aruba", "Asuncion" : "America/Asuncion", "Atikokan" : "America/Atikokan", "Atka" : "America/Atka", "Bahia" : "America/Bahia", "Bahia Banderas" : "America/Bahia_Banderas", "Barbados" : "America/Barbados", "Belem" : "America/Belem", "Belize" : "America/Belize", "Blanc-Sablon" : "America/Blanc-Sablon", "Boa Vista" : "America/Boa_Vista", "Bogota" : "America/Bogota", "Boise" : "America/Boise", "Buenos Aires" : "America/Buenos_Aires", "Cambridge Bay" : "America/Cambridge_Bay", "Campo Grande" : "America/Campo_Grande", "Cancun" : "America/Cancun", "Caracas" : "America/Caracas", "Catamarca" : "America/Catamarca", "Cayenne" : "America/Cayenne", "Cayman" : "America/Cayman", "Chicago" : "America/Chicago", "Chihuahua" : "America/Chihuahua", "Coral Harbour" : "America/Coral_Harbour", "Cordoba" : "America/Cordoba", "Costa Rica" : "America/Costa_Rica", "Creston" : "America/Creston", "Cuiaba" : "America/Cuiaba", "Curacao" : "America/Curacao", "Danmarkshavn" : "America/Danmarkshavn", "Dawson" : "America/Dawson", "Dawson Creek" : "America/Dawson_Creek", "Denver" : "America/Denver", "Detroit" : "America/Detroit", "Dominica" : "America/Dominica", "Edmonton" : "America/Edmonton", "Eirunepe" : "America/Eirunepe", "El Salvador" : "America/El_Salvador", "Ensenada" : "America/Ensenada", "Fort Wayne" : "America/Fort_Wayne", "Fortaleza" : "America/Fortaleza", "Glace Bay" : "America/Glace_Bay", "Godthab" : "America/Godthab", "Goose Bay" : "America/Goose_Bay", "Grand Turk" : "America/Grand_Turk", "Grenada" : "America/Grenada", "Guadeloupe" : "America/Guadeloupe", "Guatemala" : "America/Guatemala", "Guayaquil" : "America/Guayaquil", "Guyana" : "America/Guyana", "Halifax" : "America/Halifax", "Havana" : "America/Havana", "Hermosillo" : "America/Hermosillo", "Indiana / Indianapolis" : "America/Indiana/Indianapolis", "Indiana / Knox" : "America/Indiana/Knox", "Indiana / Marengo" : "America/Indiana/Marengo", "Indiana / Petersburg" : "America/Indiana/Petersburg", "Indiana / Tell City" : "America/Indiana/Tell_City", "Indiana / Vevay" : "America/Indiana/Vevay", "Indiana / Vincennes" : "America/Indiana/Vincennes", "Indiana / Winamac" : "America/Indiana/Winamac", "Indianapolis" : "America/Indianapolis", "Inuvik" : "America/Inuvik", "Iqaluit" : "America/Iqaluit", "Jamaica" : "America/Jamaica", "Jujuy" : "America/Jujuy", "Juneau" : "America/Juneau", "Kentucky / Louisville" : "America/Kentucky/Louisville", "Kentucky / Monticello" : "America/Kentucky/Monticello", "Kralendijk" : "America/Kralendijk", "La Paz" : "America/La_Paz", "Lima" : "America/Lima", "Los Angeles" : "America/Los_Angeles", "Louisville" : "America/Louisville", "Lower Princes" : "America/Lower_Princes", "Maceio" : "America/Maceio", "Managua" : "America/Managua", "Manaus" : "America/Manaus", "Marigot" : "America/Marigot", "Martinique" : "America/Martinique", "Matamoros" : "America/Matamoros", "Mazatlan" : "America/Mazatlan", "Mendoza" : "America/Mendoza", "Menominee" : "America/Menominee", "Merida" : "America/Merida", "Metlakatla" : "America/Metlakatla", "Mexico City" : "America/Mexico_City", "Miquelon" : "America/Miquelon", "Moncton" : "America/Moncton", "Monterrey" : "America/Monterrey", "Montevideo" : "America/Montevideo", "Montreal" : "America/Montreal", "Montserrat" : "America/Montserrat", "Nassau" : "America/Nassau", "New York" : "America/New_York", "Nipigon" : "America/Nipigon", "Nome" : "America/Nome", "Noronha" : "America/Noronha", "North Dakota / Beulah" : "America/North_Dakota/Beulah", "North Dakota / Center" : "America/North_Dakota/Center", "North Dakota / New Salem" : "America/North_Dakota/New_Salem", "Ojinaga" : "America/Ojinaga", "Panama" : "America/Panama", "Pangnirtung" : "America/Pangnirtung", "Paramaribo" : "America/Paramaribo", "Phoenix" : "America/Phoenix", "Port-au-Prince" : "America/Port-au-Prince", "Port of Spain" : "America/Port_of_Spain", "Porto Acre" : "America/Porto_Acre", "Porto Velho" : "America/Porto_Velho", "Puerto Rico" : "America/Puerto_Rico", "Rainy River" : "America/Rainy_River", "Rankin Inlet" : "America/Rankin_Inlet", "Recife" : "America/Recife", "Regina" : "America/Regina", "Resolute" : "America/Resolute", "Rio Branco" : "America/Rio_Branco", "Rosario" : "America/Rosario", "Santa Isabel" : "America/Santa_Isabel", "Santarem" : "America/Santarem", "Santiago" : "America/Santiago", "Santo Domingo" : "America/Santo_Domingo", "Sao Paulo" : "America/Sao_Paulo", "Scoresbysund" : "America/Scoresbysund", "Shiprock" : "America/Shiprock", "Sitka" : "America/Sitka", "St. Barthelemy" : "America/St_Barthelemy", "St. Johns" : "America/St_Johns", "St. Kitts" : "America/St_Kitts", "St. Lucia" : "America/St_Lucia", "St. Thomas" : "America/St_Thomas", "St. Vincent" : "America/St_Vincent", "Swift Current" : "America/Swift_Current", "Tegucigalpa" : "America/Tegucigalpa", "Thule" : "America/Thule", "Thunder Bay" : "America/Thunder_Bay", "Tijuana" : "America/Tijuana", "Toronto" : "America/Toronto", "Tortola" : "America/Tortola", "Vancouver" : "America/Vancouver", "Virgin" : "America/Virgin", "Whitehorse" : "America/Whitehorse", "Winnipeg" : "America/Winnipeg", "Yakutat" : "America/Yakutat", "Yellowknife" : "America/Yellowknife" }, "Antarctica" : { "Casey" : "Antarctica/Casey", "Davis" : "Antarctica/Davis", "Dumont d'Urville" : "Antarctica/DumontDUrville", "Macquarie" : "Antarctica/Macquarie", "Mawson" : "Antarctica/Mawson", "McMurdo" : "Antarctica/McMurdo", "Palmer" : "Antarctica/Palmer", "Rothera" : "Antarctica/Rothera", "South Pole" : "Antarctica/South_Pole", "Syowa" : "Antarctica/Syowa", "Troll" : "Antarctica/Troll", "Vostok" : "Antarctica/Vostok" }, "Arctic" : { "Longyearbyen" : "Arctic/Longyearbyen" }, "Asia" : { "Aden" : "Asia/Aden", "Almaty" : "Asia/Almaty", "Amman" : "Asia/Amman", "Anadyr" : "Asia/Anadyr", "Aqtau" : "Asia/Aqtau", "Aqtobe" : "Asia/Aqtobe", "Ashgabat" : "Asia/Ashgabat", "Ashkhabad" : "Asia/Ashkhabad", "Baghdad" : "Asia/Baghdad", "Bahrain" : "Asia/Bahrain", "Baku" : "Asia/Baku", "Bangkok" : "Asia/Bangkok", "Beirut" : "Asia/Beirut", "Bishkek" : "Asia/Bishkek", "Brunei" : "Asia/Brunei", "Calcutta" : "Asia/Calcutta", "Chita" : "Asia/Chita", "Choibalsan" : "Asia/Choibalsan", "Chongqing" : "Asia/Chongqing", "Colombo" : "Asia/Colombo", "Dacca" : "Asia/Dacca", "Damascus" : "Asia/Damascus", "Dhaka" : "Asia/Dhaka", "Dili" : "Asia/Dili", "Dubai" : "Asia/Dubai", "Dushanbe" : "Asia/Dushanbe", "Gaza" : "Asia/Gaza", "Harbin" : "Asia/Harbin", "Hebron" : "Asia/Hebron", "Ho Chi Minh" : "Asia/Ho_Chi_Minh", "Hong Kong" : "Asia/Hong_Kong", "Hovd" : "Asia/Hovd", "Irkutsk" : "Asia/Irkutsk", "Istanbul" : "Asia/Istanbul", "Jakarta" : "Asia/Jakarta", "Jayapura" : "Asia/Jayapura", "Jerusalem" : "Asia/Jerusalem", "Kabul" : "Asia/Kabul", "Kamchatka" : "Asia/Kamchatka", "Karachi" : "Asia/Karachi", "Kashgar" : "Asia/Kashgar", "Kathmandu" : "Asia/Kathmandu", "Katmandu" : "Asia/Katmandu", "Khandyga" : "Asia/Khandyga", "Kolkata" : "Asia/Kolkata", "Krasnoyarsk" : "Asia/Krasnoyarsk", "Kuala Lumpur" : "Asia/Kuala_Lumpur", "Kuching" : "Asia/Kuching", "Kuwait" : "Asia/Kuwait", "Macao" : "Asia/Macao", "Macau" : "Asia/Macau", "Magadan" : "Asia/Magadan", "Makassar" : "Asia/Makassar", "Manila" : "Asia/Manila", "Muscat" : "Asia/Muscat", "Nicosia" : "Asia/Nicosia", "Novokuznetsk" : "Asia/Novokuznetsk", "Novosibirsk" : "Asia/Novosibirsk", "Omsk" : "Asia/Omsk", "Oral" : "Asia/Oral", "Phnom Penh" : "Asia/Phnom_Penh", "Pontianak" : "Asia/Pontianak", "Pyongyang" : "Asia/Pyongyang", "Qatar" : "Asia/Qatar", "Qyzylorda" : "Asia/Qyzylorda", "Rangoon" : "Asia/Rangoon", "Riyadh" : "Asia/Riyadh", "Saigon" : "Asia/Saigon", "Sakhalin" : "Asia/Sakhalin", "Samarkand" : "Asia/Samarkand", "Seoul" : "Asia/Seoul", "Shanghai" : "Asia/Shanghai", "Singapore" : "Asia/Singapore", "Srednekolymsk" : "Asia/Srednekolymsk", "Taipei" : "Asia/Taipei", "Tashkent" : "Asia/Tashkent", "Tbilisi" : "Asia/Tbilisi", "Tehran" : "Asia/Tehran", "Tel Aviv" : "Asia/Tel_Aviv", "Thimbu" : "Asia/Thimbu", "Thimphu" : "Asia/Thimphu", "Tokyo" : "Asia/Tokyo", "Ujung Pandang" : "Asia/Ujung_Pandang", "Ulaanbaatar" : "Asia/Ulaanbaatar", "Ulan Bator" : "Asia/Ulan_Bator", "Urumqi" : "Asia/Urumqi", "Ust-Nera" : "Asia/Ust-Nera", "Vientiane" : "Asia/Vientiane", "Vladivostok" : "Asia/Vladivostok", "Yakutsk" : "Asia/Yakutsk", "Yekaterinburg" : "Asia/Yekaterinburg", "Yerevan" : "Asia/Yerevan" }, "Atlantic" : { "Azores" : "Atlantic/Azores", "Bermuda" : "Atlantic/Bermuda", "Canary" : "Atlantic/Canary", "Cape Verde" : "Atlantic/Cape_Verde", "Faeroe" : "Atlantic/Faeroe", "Faroe" : "Atlantic/Faroe", "Jan Mayen" : "Atlantic/Jan_Mayen", "Madeira" : "Atlantic/Madeira", "Reykjavik" : "Atlantic/Reykjavik", "South Georgia" : "Atlantic/South_Georgia", "St. Helena" : "Atlantic/St_Helena", "Stanley" : "Atlantic/Stanley" }, "Australia" : { "Adelaide" : "Australia/Adelaide", "Brisbane" : "Australia/Brisbane", "Broken Hill" : "Australia/Broken_Hill", "Canberra" : "Australia/Canberra", "Currie" : "Australia/Currie", "Darwin" : "Australia/Darwin", "Eucla" : "Australia/Eucla", "Hobart" : "Australia/Hobart", "Lindeman" : "Australia/Lindeman", "Lord Howe" : "Australia/Lord_Howe", "Melbourne" : "Australia/Melbourne", "North" : "Australia/North", "Perth" : "Australia/Perth", "Queensland" : "Australia/Queensland", "South" : "Australia/South", "Sydney" : "Australia/Sydney", "Tasmania" : "Australia/Tasmania", "Victoria" : "Australia/Victoria", "West" : "Australia/West", "Yancowinna" : "Australia/Yancowinna" }, "Brazil" : { "Acre" : "Brazil/Acre", "Fernando de Noronha" : "Brazil/DeNoronha", "East" : "Brazil/East", "West" : "Brazil/West" }, "Canada" : { "Atlantic" : "Canada/Atlantic", "Central" : "Canada/Central", "East-Saskatchewan" : "Canada/East-Saskatchewan", "Eastern" : "Canada/Eastern", "Mountain" : "Canada/Mountain", "Newfoundland" : "Canada/Newfoundland", "Pacific" : "Canada/Pacific", "Saskatchewan" : "Canada/Saskatchewan", "Yukon" : "Canada/Yukon" }, "Chile" : { "Continental" : "Chile/Continental", "Easter Island" : "Chile/EasterIsland" }, "Europe" : { "Amsterdam" : "Europe/Amsterdam", "Andorra" : "Europe/Andorra", "Athens" : "Europe/Athens", "Belfast" : "Europe/Belfast", "Belgrade" : "Europe/Belgrade", "Berlin" : "Europe/Berlin", "Bratislava" : "Europe/Bratislava", "Brussels" : "Europe/Brussels", "Bucharest" : "Europe/Bucharest", "Budapest" : "Europe/Budapest", "Busingen" : "Europe/Busingen", "Chisinau" : "Europe/Chisinau", "Copenhagen" : "Europe/Copenhagen", "Dublin" : "Europe/Dublin", "Gibraltar" : "Europe/Gibraltar", "Guernsey" : "Europe/Guernsey", "Helsinki" : "Europe/Helsinki", "Isle of Man" : "Europe/Isle_of_Man", "Istanbul" : "Europe/Istanbul", "Jersey" : "Europe/Jersey", "Kaliningrad" : "Europe/Kaliningrad", "Kiev" : "Europe/Kiev", "Lisbon" : "Europe/Lisbon", "Ljubljana" : "Europe/Ljubljana", "London" : "Europe/London", "Luxembourg" : "Europe/Luxembourg", "Madrid" : "Europe/Madrid", "Malta" : "Europe/Malta", "Mariehamn" : "Europe/Mariehamn", "Minsk" : "Europe/Minsk", "Monaco" : "Europe/Monaco", "Moscow" : "Europe/Moscow", "Nicosia" : "Europe/Nicosia", "Oslo" : "Europe/Oslo", "Paris" : "Europe/Paris", "Podgorica" : "Europe/Podgorica", "Prague" : "Europe/Prague", "Riga" : "Europe/Riga", "Rome" : "Europe/Rome", "Samara" : "Europe/Samara", "San Marino" : "Europe/San_Marino", "Sarajevo" : "Europe/Sarajevo", "Simferopol" : "Europe/Simferopol", "Skopje" : "Europe/Skopje", "Sofia" : "Europe/Sofia", "Stockholm" : "Europe/Stockholm", "Tallinn" : "Europe/Tallinn", "Tirane" : "Europe/Tirane", "Tiraspol" : "Europe/Tiraspol", "Uzhgorod" : "Europe/Uzhgorod", "Vaduz" : "Europe/Vaduz", "Vatican" : "Europe/Vatican", "Vienna" : "Europe/Vienna", "Vilnius" : "Europe/Vilnius", "Volgograd" : "Europe/Volgograd", "Warsaw" : "Europe/Warsaw", "Zagreb" : "Europe/Zagreb", "Zaporozhye" : "Europe/Zaporozhye", "Zurich" : "Europe/Zurich" }, "GMT" : { "GMT-14" : "Etc/GMT-14", "GMT-13" : "Etc/GMT-13", "GMT-12" : "Etc/GMT-12", "GMT-11" : "Etc/GMT-11", "GMT-10" : "Etc/GMT-10", "GMT-9" : "Etc/GMT-9", "GMT-8" : "Etc/GMT-8", "GMT-7" : "Etc/GMT-7", "GMT-6" : "Etc/GMT-6", "GMT-5" : "Etc/GMT-5", "GMT-4" : "Etc/GMT-4", "GMT-3" : "Etc/GMT-3", "GMT-2" : "Etc/GMT-2", "GMT-1" : "Etc/GMT-1", "GMT+0" : "Etc/GMT+0", "GMT+1" : "Etc/GMT+1", "GMT+2" : "Etc/GMT+2", "GMT+3" : "Etc/GMT+3", "GMT+4" : "Etc/GMT+4", "GMT+5" : "Etc/GMT+5", "GMT+6" : "Etc/GMT+6", "GMT+7" : "Etc/GMT+7", "GMT+8" : "Etc/GMT+8", "GMT+9" : "Etc/GMT+9", "GMT+10" : "Etc/GMT+10", "GMT+11" : "Etc/GMT+11", "GMT+12" : "Etc/GMT+12" }, "Indian" : { "Antananarivo" : "Indian/Antananarivo", "Chagos" : "Indian/Chagos", "Christmas" : "Indian/Christmas", "Cocos" : "Indian/Cocos", "Comoro" : "Indian/Comoro", "Kerguelen" : "Indian/Kerguelen", "Mahe" : "Indian/Mahe", "Maldives" : "Indian/Maldives", "Mauritius" : "Indian/Mauritius", "Mayotte" : "Indian/Mayotte", "Reunion" : "Indian/Reunion" }, "Mexico" : { "Baja Norte" : "Mexico/BajaNorte", "Baja Sur" : "Mexico/BajaSur", "General" : "Mexico/General" }, "Pacific" : { "Apia" : "Pacific/Apia", "Auckland" : "Pacific/Auckland", "Bougainville" : "Pacific/Bougainville", "Chatham" : "Pacific/Chatham", "Chuuk" : "Pacific/Chuuk", "Easter" : "Pacific/Easter", "Efate" : "Pacific/Efate", "Enderbury" : "Pacific/Enderbury", "Fakaofo" : "Pacific/Fakaofo", "Fiji" : "Pacific/Fiji", "Funafuti" : "Pacific/Funafuti", "Galapagos" : "Pacific/Galapagos", "Gambier" : "Pacific/Gambier", "Guadalcanal" : "Pacific/Guadalcanal", "Guam" : "Pacific/Guam", "Honolulu" : "Pacific/Honolulu", "Johnston" : "Pacific/Johnston", "Kiritimati" : "Pacific/Kiritimati", "Kosrae" : "Pacific/Kosrae", "Kwajalein" : "Pacific/Kwajalein", "Majuro" : "Pacific/Majuro", "Marquesas" : "Pacific/Marquesas", "Midway" : "Pacific/Midway", "Nauru" : "Pacific/Nauru", "Niue" : "Pacific/Niue", "Norfolk" : "Pacific/Norfolk", "Noumea" : "Pacific/Noumea", "Pago Pago" : "Pacific/Pago_Pago", "Palau" : "Pacific/Palau", "Pitcairn" : "Pacific/Pitcairn", "Pohnpei" : "Pacific/Pohnpei", "Ponape" : "Pacific/Ponape", "Port Moresby" : "Pacific/Port_Moresby", "Rarotonga" : "Pacific/Rarotonga", "Saipan" : "Pacific/Saipan", "Samoa" : "Pacific/Samoa", "Tahiti" : "Pacific/Tahiti", "Tarawa" : "Pacific/Tarawa", "Tongatapu" : "Pacific/Tongatapu", "Truk" : "Pacific/Truk", "Wake" : "Pacific/Wake", "Wallis" : "Pacific/Wallis", "Yap" : "Pacific/Yap" } }; /** * All selectable regions. * * @type String[] */ $scope.regions = (function collectRegions() { // Start with blank entry var regions = [ '' ]; // Add each available region for (var region in $scope.timeZones) regions.push(region); return regions; })(); /** * Direct mapping of all time zone IDs to the region containing that ID. * * @type Object. */ var timeZoneRegions = (function mapRegions() { var regions = {}; // For each available region for (var region in $scope.timeZones) { // Get time zones within that region var timeZonesInRegion = $scope.timeZones[region]; // For each of those time zones for (var timeZoneName in timeZonesInRegion) { // Get corresponding ID var timeZoneID = timeZonesInRegion[timeZoneName]; // Store region in map regions[timeZoneID] = region; } } return regions; })(); /** * Map of regions to the currently selected time zone for that region. * Initially, all regions will be set to default selections (the first * time zone, sorted lexicographically). * * @type Object. */ var selectedTimeZone = (function produceDefaultTimeZones() { var defaultTimeZone = {}; // For each available region for (var region in $scope.timeZones) { // Get time zones within that region var timeZonesInRegion = $scope.timeZones[region]; // No default initially var defaultZoneName = null; var defaultZoneID = null; // For each of those time zones for (var timeZoneName in timeZonesInRegion) { // Get corresponding ID var timeZoneID = timeZonesInRegion[timeZoneName]; // Set as default if earlier than existing default if (!defaultZoneName || timeZoneName < defaultZoneName) { defaultZoneName = timeZoneName; defaultZoneID = timeZoneID; } } // Store default zone defaultTimeZone[region] = defaultZoneID; } return defaultTimeZone; })(); /** * The name of the region currently selected. The selected region narrows * which time zones are selectable. * * @type String */ $scope.region = ''; // Ensure corresponding region is selected $scope.$watch('model', function setModel(model) { $scope.region = timeZoneRegions[model] || ''; selectedTimeZone[$scope.region] = model; }); // Restore time zone selection when region changes $scope.$watch('region', function restoreSelection(region) { $scope.model = selectedTimeZone[region] || null; }); }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/controllers/checkboxFieldController.js0100664 0001751 0001751 00000003222 12612226251 034700 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Controller for checkbox fields. */ angular.module('form').controller('checkboxFieldController', ['$scope', function checkboxFieldController($scope) { // Update typed value when model is changed $scope.$watch('model', function modelChanged(model) { $scope.typedValue = (model === $scope.field.options[0]); }); // Update string value in model when typed value is changed $scope.$watch('typedValue', function typedValueChanged(typedValue) { $scope.model = (typedValue ? $scope.field.options[0] : ''); }); }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/controllers/selectFieldController.js0100664 0001751 0001751 00000005406 12612226251 034377 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Controller for select fields. */ angular.module('form').controller('selectFieldController', ['$scope', '$injector', function selectFieldController($scope, $injector) { // Required services var translationStringService = $injector.get('translationStringService'); // Interpret undefined/null as empty string $scope.$watch('model', function setModel(model) { if (!model && model !== '') $scope.model = ''; }); /** * Produces the translation string for the given field option * value. The translation string will be of the form: * * NAMESPACE.FIELD_OPTION_NAME_VALUE * * where NAMESPACE is the namespace provided to the * directive, NAME is the field name transformed * via translationStringService.canonicalize(), and * VALUE is the option value transformed via * translationStringService.canonicalize() * * @param {String} value * The name of the option value. * * @returns {String} * The translation string which produces the translated name of the * value specified. */ $scope.getFieldOption = function getFieldOption(value) { // If no field, or no value, then no corresponding translation string if (!$scope.field || !$scope.field.name || !value) return ''; return translationStringService.canonicalize($scope.namespace || 'MISSING_NAMESPACE') + '.FIELD_OPTION_' + translationStringService.canonicalize($scope.field.name) + '_' + translationStringService.canonicalize(value || 'EMPTY'); }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/controllers/dateFieldController.js0100664 0001751 0001751 00000006036 12612226251 034035 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Controller for date fields. */ angular.module('form').controller('dateFieldController', ['$scope', '$injector', function dateFieldController($scope, $injector) { // Required services var $filter = $injector.get('$filter'); /** * Options which dictate the behavior of the input field model, as defined * by https://docs.angularjs.org/api/ng/directive/ngModelOptions * * @type Object. */ $scope.modelOptions = { /** * Space-delimited list of events on which the model will be updated. * * @type String */ updateOn : 'blur', /** * The time zone to use when reading/writing the Date object of the * model. * * @type String */ timezone : 'UTC' }; /** * Parses the date components of the given string into a Date with only the * date components set. The resulting Date will be in the UTC timezone, * with the time left as midnight. The input string must be in the format * YYYY-MM-DD (zero-padded). * * @param {String} str * The date string to parse. * * @returns {Date} * A Date object, in the UTC timezone, with only the date components * set. */ var parseDate = function parseDate(str) { // Parse date, return blank if invalid var parsedDate = new Date(str + 'T00:00Z'); if (isNaN(parsedDate.getTime())) return null; return parsedDate; }; // Update typed value when model is changed $scope.$watch('model', function modelChanged(model) { $scope.typedValue = (model ? parseDate(model) : null); }); // Update string value in model when typed value is changed $scope.$watch('typedValue', function typedValueChanged(typedValue) { $scope.model = (typedValue ? $filter('date')(typedValue, 'yyyy-MM-dd', 'UTC') : ''); }); }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/controllers/numberFieldController.js0100664 0001751 0001751 00000003233 12612226251 034404 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Controller for number fields. */ angular.module('form').controller('numberFieldController', ['$scope', function numberFieldController($scope) { // Update typed value when model is changed $scope.$watch('model', function modelChanged(model) { $scope.typedValue = (model ? Number(model) : null); }); // Update string value in model when typed value is changed $scope.$watch('typedValue', function typedValueChanged(typedValue) { $scope.model = ((typedValue || typedValue === 0) ? typedValue.toString() : ''); }); }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/controllers/passwordFieldController.js0100664 0001751 0001751 00000005154 12612226251 034762 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Controller for password fields. */ angular.module('form').controller('passwordFieldController', ['$scope', function passwordFieldController($scope) { /** * The type to use for the input field. By default, the input field will * have the type 'password', and thus will be masked. * * @type String * @default 'password' */ $scope.passwordInputType = 'password'; /** * Returns a string which describes the action the next call to * togglePassword() will have. * * @return {String} * A string which describes the action the next call to * togglePassword() will have. */ $scope.getTogglePasswordHelpText = function getTogglePasswordHelpText() { // If password is hidden, togglePassword() will show the password if ($scope.passwordInputType === 'password') return 'FORM.HELP_SHOW_PASSWORD'; // If password is shown, togglePassword() will hide the password return 'FORM.HELP_HIDE_PASSWORD'; }; /** * Toggles visibility of the field contents, if this field is a * password field. Initially, password contents are masked * (invisible). */ $scope.togglePassword = function togglePassword() { // If password is hidden, show the password if ($scope.passwordInputType === 'password') $scope.passwordInputType = 'text'; // If password is shown, hide the password else $scope.passwordInputType = 'password'; }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/types/FieldType.js0100664 0001751 0001751 00000006366 12612226251 030601 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service which defines the FieldType class. */ angular.module('form').factory('FieldType', [function defineFieldType() { /** * The object used by the formService for describing field types. * * @constructor * @param {FieldType|Object} [template={}] * The object whose properties should be copied within the new * FieldType. */ var FieldType = function FieldType(template) { // Use empty object by default template = template || {}; /** * The raw HTML of the template that should be injected into the DOM of * a form using this field type. If provided, this will be used instead * of templateUrl. * * @type String */ this.template = template.template; /** * The URL of the template that should be injected into the DOM of a * form using this field type. This property will be ignored if a raw * HTML template is supplied via the template property. * * @type String */ this.templateUrl = template.templateUrl; /** * The name of the AngularJS module defining the controller for this * field type. This is optional, as not all field types will need * controllers. * * @type String */ this.module = template.module; /** * The name of the controller for this field type. This is optional, as * not all field types will need controllers. If a controller is * specified, it will receive the following properties on the scope: * * namespace: * A String which defines the unique namespace associated the * translation strings used by the form using a field of this type. * * field: * The Field object that is being rendered, representing a field of * this type. * * model: * The current String value of the field, if any. * * @type String */ this.controller = template.controller; }; return FieldType; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/formModule.js0100664 0001751 0001751 00000002263 12612226251 027651 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Module for displaying dynamic forms. */ angular.module('form', ['locale']); guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/templates/checkboxField.html0100664 0001751 0001751 00000000125 12612226251 032613 0ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/templates/numberField.html0100664 0001751 0001751 00000000123 12612226251 032313 0ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/templates/passwordField.html0100664 0001751 0001751 00000000422 12612226251 032667 0ustar00mjumpermjumper0000000 0000000
guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/templates/dateField.html0100664 0001751 0001751 00000000446 12612226251 031750 0ustar00mjumpermjumper0000000 0000000
guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/templates/textField.html0100664 0001751 0001751 00000000114 12612226251 032007 0ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/templates/timeZoneField.html0100664 0001751 0001751 00000000713 12612226251 032622 0ustar00mjumpermjumper0000000 0000000
guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/templates/form.html0100664 0001751 0001751 00000003272 12612226251 031032 0ustar00mjumpermjumper0000000 0000000

{{getSectionHeader(form) | translate}}

guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/templates/selectField.html0100664 0001751 0001751 00000000211 12612226251 032300 0ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/templates/formField.html0100664 0001751 0001751 00000002630 12612226251 031773 0ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/templates/timeField.html0100664 0001751 0001751 00000000446 12612226251 031771 0ustar00mjumpermjumper0000000 0000000
guacamole-client-0.9.9/guacamole/src/main/webapp/app/form/templates/textAreaField.html0100664 0001751 0001751 00000000115 12612226251 032601 0ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/navigation/services/userPageService.js0100664 0001751 0001751 00000041471 12645510255 033665 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A service for generating all the important pages a user can visit. */ angular.module('navigation').factory('userPageService', ['$injector', function userPageService($injector) { // Get required types var ClientIdentifier = $injector.get('ClientIdentifier'); var ConnectionGroup = $injector.get('ConnectionGroup'); var PageDefinition = $injector.get('PageDefinition'); var PermissionSet = $injector.get('PermissionSet'); // Get required services var $q = $injector.get('$q'); var authenticationService = $injector.get('authenticationService'); var connectionGroupService = $injector.get('connectionGroupService'); var dataSourceService = $injector.get('dataSourceService'); var permissionService = $injector.get('permissionService'); var translationStringService = $injector.get('translationStringService'); var service = {}; /** * The home page to assign to a user if they can navigate to more than one * page. * * @type PageDefinition */ var SYSTEM_HOME_PAGE = new PageDefinition({ name : 'USER_MENU.ACTION_NAVIGATE_HOME', url : '/' }); /** * Returns an appropriate home page for the current user. * * @param {Object.} rootGroups * A map of all root connection groups visible to the current user, * where each key is the identifier of the corresponding data source. * * @returns {PageDefinition} * The user's home page. */ var generateHomePage = function generateHomePage(rootGroups) { var homePage = null; // Determine whether a connection or balancing group should serve as // the home page for (var dataSource in rootGroups) { // Get corresponding root group var rootGroup = rootGroups[dataSource]; // Get children var connections = rootGroup.childConnections || []; var connectionGroups = rootGroup.childConnectionGroups || []; // Calculate total number of root-level objects var totalRootObjects = connections.length + connectionGroups.length; // If exactly one connection or balancing group is available, use // that as the home page if (homePage === null && totalRootObjects === 1) { var connection = connections[0]; var connectionGroup = connectionGroups[0]; // Only one connection present, use as home page if (connection) { homePage = new PageDefinition({ name : connection.name, url : '/client/' + ClientIdentifier.toString({ dataSource : dataSource, type : ClientIdentifier.Types.CONNECTION, id : connection.identifier }) }); } // Only one balancing group present, use as home page if (connectionGroup && connectionGroup.type === ConnectionGroup.Type.BALANCING && _.isEmpty(connectionGroup.childConnections) && _.isEmpty(connectionGroup.childConnectionGroups)) { homePage = new PageDefinition({ name : connectionGroup.name, url : '/client/' + ClientIdentifier.toString({ dataSource : dataSource, type : ClientIdentifier.Types.CONNECTION_GROUP, id : connectionGroup.identifier }) }); } } // Otherwise, a connection or balancing group cannot serve as the // home page else if (totalRootObjects >= 1) { homePage = null; break; } } // end for each data source // Use default home page if no other is available return homePage || SYSTEM_HOME_PAGE; }; /** * Returns a promise which resolves with an appropriate home page for the * current user. * * @returns {Promise.} * A promise which resolves with the user's default home page. */ service.getHomePage = function getHomePage() { var deferred = $q.defer(); // Resolve promise using home page derived from root connection groups dataSourceService.apply( connectionGroupService.getConnectionGroupTree, authenticationService.getAvailableDataSources(), ConnectionGroup.ROOT_IDENTIFIER ) .then(function rootConnectionGroupsRetrieved(rootGroups) { deferred.resolve(generateHomePage(rootGroups)); }); return deferred.promise; }; /** * Returns all settings pages that the current user can visit. This can * include any of the various manage pages. * * @param {Object.} permissionSets * A map of all permissions granted to the current user, where each * key is the identifier of the corresponding data source. * * @returns {Page[]} * An array of all settings pages that the current user can visit. */ var generateSettingsPages = function generateSettingsPages(permissionSets) { var pages = []; var canManageUsers = []; var canManageConnections = []; var canViewConnectionRecords = []; var canManageSessions = []; // Inspect the contents of each provided permission set angular.forEach(authenticationService.getAvailableDataSources(), function inspectPermissions(dataSource) { // Get permissions for current data source, skipping if non-existent var permissions = permissionSets[dataSource]; if (!permissions) return; // Do not modify original object permissions = angular.copy(permissions); // Ignore permission to update root group PermissionSet.removeConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, ConnectionGroup.ROOT_IDENTIFIER); // Ignore permission to update self PermissionSet.removeUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, authenticationService.getCurrentUsername()); // Determine whether the current user needs access to the user management UI if ( // System permissions PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_USER) // Permission to update users || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) // Permission to delete users || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) // Permission to administer users || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) ) { canManageUsers.push(dataSource); } // Determine whether the current user needs access to the connection management UI if ( // System permissions PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION) || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP) // Permission to update connections or connection groups || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) // Permission to delete connections or connection groups || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) // Permission to administer connections or connection groups || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) ) { canManageConnections.push(dataSource); } // Determine whether the current user needs access to the session management UI or view connection history if ( // A user must be a system administrator to manage sessions PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) ) { canManageSessions.push(dataSource); canViewConnectionRecords.push(dataSource); } }); // If user can manage sessions, add link to sessions management page if (canManageSessions.length) { pages.push(new PageDefinition({ name : 'USER_MENU.ACTION_MANAGE_SESSIONS', url : '/settings/sessions' })); } // If user can manage connections, add links for connection management pages angular.forEach(canViewConnectionRecords, function addConnectionHistoryLink(dataSource) { pages.push(new PageDefinition({ name : [ 'USER_MENU.ACTION_VIEW_HISTORY', translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME' ], url : '/settings/' + encodeURIComponent(dataSource) + '/history' })); }); // If user can manage users, add link to user management page if (canManageUsers.length) { pages.push(new PageDefinition({ name : 'USER_MENU.ACTION_MANAGE_USERS', url : '/settings/users' })); } // If user can manage connections, add links for connection management pages angular.forEach(canManageConnections, function addConnectionManagementLink(dataSource) { pages.push(new PageDefinition({ name : [ 'USER_MENU.ACTION_MANAGE_CONNECTIONS', translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME' ], url : '/settings/' + encodeURIComponent(dataSource) + '/connections' })); }); // Add link to user preferences (always accessible) pages.push(new PageDefinition({ name : 'USER_MENU.ACTION_MANAGE_PREFERENCES', url : '/settings/preferences' })); return pages; }; /** * Returns a promise which resolves to an array of all settings pages that * the current user can visit. This can include any of the various manage * pages. * * @returns {Promise.} * A promise which resolves to an array of all settings pages that the * current user can visit. */ service.getSettingsPages = function getSettingsPages() { var deferred = $q.defer(); // Retrieve current permissions dataSourceService.apply( permissionService.getPermissions, authenticationService.getAvailableDataSources(), authenticationService.getCurrentUsername() ) // Resolve promise using settings pages derived from permissions .then(function permissionsRetrieved(permissions) { deferred.resolve(generateSettingsPages(permissions)); }); return deferred.promise; }; /** * Returns all the main pages that the current user can visit. This can * include the home page, manage pages, etc. In the case that there are no * applicable pages of this sort, it may return a client page. * * @param {Object.} rootGroups * A map of all root connection groups visible to the current user, * where each key is the identifier of the corresponding data source. * * @param {Object.} permissions * A map of all permissions granted to the current user, where each * key is the identifier of the corresponding data source. * * @returns {Page[]} * An array of all main pages that the current user can visit. */ var generateMainPages = function generateMainPages(rootGroups, permissions) { var pages = []; // Get home page and settings pages var homePage = generateHomePage(rootGroups); var settingsPages = generateSettingsPages(permissions); // Only include the home page in the list of main pages if the user // can navigate elsewhere. if (homePage === SYSTEM_HOME_PAGE || settingsPages.length) pages.push(homePage); // Add generic link to the first-available settings page if (settingsPages.length) { pages.push(new PageDefinition({ name : 'USER_MENU.ACTION_MANAGE_SETTINGS', url : settingsPages[0].url })); } return pages; }; /** * Returns a promise which resolves to an array of all main pages that the * current user can visit. This can include the home page, manage pages, * etc. In the case that there are no applicable pages of this sort, it may * return a client page. * * @returns {Promise.} * A promise which resolves to an array of all main pages that the * current user can visit. */ service.getMainPages = function getMainPages() { var deferred = $q.defer(); var rootGroups = null; var permissions = null; /** * Resolves the main pages retrieval promise, if possible. If * insufficient data is available, this function does nothing. */ var resolveMainPages = function resolveMainPages() { if (rootGroups && permissions) deferred.resolve(generateMainPages(rootGroups, permissions)); }; // Retrieve root group, resolving main pages if possible dataSourceService.apply( connectionGroupService.getConnectionGroupTree, authenticationService.getAvailableDataSources(), ConnectionGroup.ROOT_IDENTIFIER ) .then(function rootConnectionGroupsRetrieved(retrievedRootGroups) { rootGroups = retrievedRootGroups; resolveMainPages(); }); // Retrieve current permissions dataSourceService.apply( permissionService.getPermissions, authenticationService.getAvailableDataSources(), authenticationService.getCurrentUsername() ) // Resolving main pages if possible .then(function permissionsRetrieved(retrievedPermissions) { permissions = retrievedPermissions; resolveMainPages(); }); return deferred.promise; }; return service; }]);guacamole-client-0.9.9/guacamole/src/main/webapp/app/navigation/directives/guacPageList.js0100664 0001751 0001751 00000021462 12612226251 033447 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which provides a list of links to specific pages. */ angular.module('navigation').directive('guacPageList', [function guacPageList() { return { restrict: 'E', replace: true, scope: { /** * The array of pages to display. * * @type PageDefinition[] */ pages : '=' }, templateUrl: 'app/navigation/templates/guacPageList.html', controller: ['$scope', '$injector', function guacPageListController($scope, $injector) { // Required types var PageDefinition = $injector.get('PageDefinition'); // Required services var $location = $injector.get('$location'); /** * The URL of the currently-displayed page. * * @type String */ var currentURL = $location.url(); /** * The names associated with the current page, if the current page * is known. The value of this property corresponds to the value of * PageDefinition.name. Though PageDefinition.name may be a String, * this will always be an Array. * * @type String[] */ var currentPageName = []; /** * Array of each level of the page list, where a level is defined * by a mapping of names (translation strings) to the * PageDefinitions corresponding to those names. * * @type Object.[] */ $scope.levels = []; /** * Returns the names associated with the given page, in * hierarchical order. If the page is only associated with a single * name, and that name is not stored as an array, it will be still * be returned as an array containing a single item. * * @param {PageDefinition} page * The page to return the names of. * * @return {String[]} * An array of all names associated with the given page, in * hierarchical order. */ var getPageNames = function getPageNames(page) { // If already an array, simply return the name if (angular.isArray(page.name)) return page.name; // Otherwise, transform into array return [page.name]; }; /** * Adds the given PageDefinition to the overall set of pages * displayed by this guacPageList, automatically updating the * available levels ($scope.levels) and the contents of those * levels. * * @param {PageDefinition} page * The PageDefinition to add. * * @param {Number} weight * The sorting weight to use for the page if it does not * already have an associated weight. */ var addPage = function addPage(page, weight) { // Pull all names for page var names = getPageNames(page); // Copy the hierarchy of this page into the displayed levels // as far as is relevant for the currently-displayed page for (var i = 0; i < names.length; i++) { // Create current level, if it doesn't yet exist var pages = $scope.levels[i]; if (!pages) pages = $scope.levels[i] = {}; // Get the name at the current level var name = names[i]; // Determine whether this page definition is part of the // hierarchy containing the current page var isCurrentPage = (currentPageName[i] === name); // Store new page if it doesn't yet exist at this level if (!pages[name]) { pages[name] = new PageDefinition({ name : name, url : isCurrentPage ? currentURL : page.url, className : page.className, weight : page.weight || (weight + i) }); } // If the name at this level no longer matches the // hierarchy of the current page, do not go any deeper if (currentPageName[i] !== name) break; } }; /** * Navigate to the given page. * * @param {PageDefinition} page * The page to navigate to. */ $scope.navigateToPage = function navigateToPage(page) { $location.path(page.url); }; /** * Tests whether the given page is the page currently being viewed. * * @param {PageDefinition} page * The page to test. * * @returns {Boolean} * true if the given page is the current page, false otherwise. */ $scope.isCurrentPage = function isCurrentPage(page) { return currentURL === page.url; }; /** * Given an arbitrary map of PageDefinitions, returns an array of * those PageDefinitions, sorted by weight. * * @param {Object.<*, PageDefinition>} level * A map of PageDefinitions with arbitrary keys. The value of * each key is ignored. * * @returns {PageDefinition[]} * An array of all PageDefinitions in the given map, sorted by * weight. */ $scope.getPages = function getPages(level) { var pages = []; // Convert contents of level to a flat array of pages angular.forEach(level, function addPageFromLevel(page) { pages.push(page); }); // Sort page array by weight pages.sort(function comparePages(a, b) { return a.weight - b.weight; }); return pages; }; // Update page levels whenever pages changes $scope.$watch('pages', function setPages(pages) { // Determine current page name currentPageName = []; angular.forEach(pages, function findCurrentPageName(page) { // If page is current page, store its names if ($scope.isCurrentPage(page)) currentPageName = getPageNames(page); }); // Reset contents of levels $scope.levels = []; // Add all page definitions angular.forEach(pages, addPage); // Filter to only relevant levels $scope.levels = $scope.levels.filter(function isRelevant(level) { // Determine relevancy by counting the number of pages var pageCount = 0; for (var name in level) { // Level is relevant if it has two or more pages if (++pageCount === 2) return true; } // Otherwise, the level is not relevant return false; }); }); }] // end controller }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/navigation/directives/guacUserMenu.js0100664 0001751 0001751 00000012073 12612226251 033500 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which provides a user-oriented menu containing options for * navigation and configuration. */ angular.module('navigation').directive('guacUserMenu', [function guacUserMenu() { return { restrict: 'E', replace: true, scope: { /** * Optional array of actions which are specific to this particular * location, as these actions may not be appropriate for other * locations which contain the user menu. * * @type MenuAction[] */ localActions : '=' }, templateUrl: 'app/navigation/templates/guacUserMenu.html', controller: ['$scope', '$injector', '$element', function guacUserMenuController($scope, $injector, $element) { // Get required services var $document = $injector.get('$document'); var $location = $injector.get('$location'); var $route = $injector.get('$route'); var authenticationService = $injector.get('authenticationService'); var userPageService = $injector.get('userPageService'); /** * The outermost element of the user menu directive. * * @type Element */ var element = $element[0]; /** * The main document object. * * @type Document */ var document = $document[0]; /** * Whether the contents of the user menu are currently shown. * * @type Boolean */ $scope.menuShown = false; /** * The username of the current user. * * @type String */ $scope.username = authenticationService.getCurrentUsername(); /** * The available main pages for the current user. * * @type Page[] */ $scope.pages = null; // Retrieve the main pages from the user page service userPageService.getMainPages() .then(function retrievedMainPages(pages) { $scope.pages = pages; }); /** * Toggles visibility of the user menu. */ $scope.toggleMenu = function toggleMenu() { $scope.menuShown = !$scope.menuShown; }; /** * Logs out the current user, redirecting them to back to the root * after logout completes. */ $scope.logout = function logout() { authenticationService.logout()['finally'](function logoutComplete() { if ($location.path() !== '/') $location.url('/'); else $route.reload(); }); }; /** * Action which logs out the current user, redirecting them to back * to the login screen after logout completes. */ var LOGOUT_ACTION = { name : 'USER_MENU.ACTION_LOGOUT', className : 'logout', callback : $scope.logout }; /** * All available actions for the current user. */ $scope.actions = [ LOGOUT_ACTION ]; // Close menu when use clicks anywhere else document.body.addEventListener('click', function clickOutsideMenu() { $scope.$apply(function closeMenu() { $scope.menuShown = false; }); }, false); // Prevent click within menu from triggering the outside-menu handler element.addEventListener('click', function clickInsideMenu(e) { e.stopPropagation(); }, false); }] // end controller }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/navigation/styles/page-tabs.css0100664 0001751 0001751 00000003431 12612226251 032274 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .page-tabs .page-list ul { margin: 0; padding: 0; background: rgba(0, 0, 0, 0.0125); border-bottom: 1px solid rgba(0, 0, 0, 0.05); } .page-tabs .page-list ul + ul { font-size: 0.75em; } .page-tabs .page-list li { display: inline-block; list-style: none; } .page-tabs .page-list li a[href] { display: block; color: black; text-decoration: none; padding: 0.75em 1em; } .page-tabs .page-list li a[href]:visited { color: black; } .page-tabs .page-list li a[href]:hover { background-color: #CDA; } .page-tabs .page-list li a[href].current, .page-tabs .page-list li a[href].current:hover { background: rgba(0,0,0,0.3); cursor: default; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/navigation/styles/user-menu.css0100664 0001751 0001751 00000011653 12612226251 032356 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .user-menu { /* IE10 */ display: -ms-flexbox; -ms-flex-align: stretch; -ms-flex-direction: row; /* Ancient Mozilla */ display: -moz-box; -moz-box-align: stretch; -moz-box-orient: horizontal; /* Ancient WebKit */ display: -webkit-box; -webkit-box-align: stretch; -webkit-box-orient: horizontal; /* Old WebKit */ display: -webkit-flex; -webkit-align-items: stretch; -webkit-flex-direction: row; /* W3C */ display: flex; align-items: stretch; flex-direction: row; } .user-menu .user-menu-dropdown { /* IE10 */ display: -ms-flexbox; -ms-flex-align: center; -ms-flex-direction: row; /* Ancient Mozilla */ display: -moz-box; -moz-box-align: center; -moz-box-orient: horizontal; /* Ancient WebKit */ display: -webkit-box; -webkit-box-align: center; -webkit-box-orient: horizontal; /* Old WebKit */ display: -webkit-flex; -webkit-align-items: center; -webkit-flex-direction: row; /* W3C */ display: flex; align-items: center; flex-direction: row; } .user-menu .user-menu-dropdown { position: relative; border-left: 1px solid rgba(0,0,0,0.125); background: rgba(0,0,0,0.04); } .user-menu .user-menu-dropdown:hover { background: rgba(0,0,0,0.01); } .user-menu .user-menu-dropdown.open, .user-menu .user-menu-dropdown.open:hover { background: rgba(0,0,0,0.3); } .user-menu .username { cursor: default; margin: 0; min-width: 2in; font-size: 1.25em; font-weight: bold; padding: 0.5em 2em; background-repeat: no-repeat; background-size: 1em; background-position: 0.5em center; background-image: url('images/user-icons/guac-user.png'); -ms-flex: 0 0 auto; -moz-box-flex: 0; -webkit-box-flex: 0; -webkit-flex: 0 0 auto; flex: 0 0 auto; } .user-menu .menu-indicator { position: absolute; right: 0; top: 0; bottom: 0; width: 2em; background-repeat: no-repeat; background-size: 1em; background-position: center center; background-image: url('images/arrows/down.png'); } .user-menu .options { visibility: hidden; position: absolute; top: 100%; right: 0; left: -1px; background: #EEE; box-shadow: 0 2px 2px rgba(0, 0, 0, 0.125); border-left: 1px solid rgba(0,0,0,0.125); border-bottom: 1px solid rgba(0,0,0,0.125); z-index: 5; } .user-menu .options ul { margin: 0; padding: 0; } .user-menu .user-menu-dropdown.open .options { visibility: visible; } .user-menu .options li { padding: 0; list-style-type: none; } .user-menu .options li a { display: block; cursor: pointer; color: black; text-decoration: none; padding: 0.75em; background-repeat: no-repeat; background-size: 1em; background-position: 0.75em center; padding-left: 2.5em; background-image: url('images/protocol-icons/guac-monitor.png'); } .user-menu .options li a:hover { background-color: #CDA; } .user-menu .options li a.current, .user-menu .options li a.current:hover { background-color: transparent; cursor: default; opacity: 0.25; } .user-menu .options li a[href="#/"] { background-image: url('images/action-icons/guac-home-dark.png'); } .user-menu .options li a[href="#/settings/users"], .user-menu .options li a[href="#/settings/connections"], .user-menu .options li a[href="#/settings/sessions"], .user-menu .options li a[href="#/settings/preferences"] { background-image: url('images/action-icons/guac-config-dark.png'); } .user-menu .options li a.logout { background-image: url('images/action-icons/guac-logout-dark.png'); } .user-menu .options li a.danger { color: white; font-weight: bold; background-color: #A43; } .user-menu .options li a.danger:hover { background-color: #C54; } guacamole-client-0.9.9/guacamole/src/main/webapp/app/navigation/types/PageDefinition.js0100664 0001751 0001751 00000005347 12612226251 032773 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the PageDefinition class definition. */ angular.module('navigation').factory('PageDefinition', [function definePageDefinition() { /** * Creates a new PageDefinition object which pairs the URL of a page with * an arbitrary, human-readable name. * * @constructor * @param {PageDefinition|Object} template * The object whose properties should be copied within the new * PageDefinition. */ var PageDefinition = function PageDefinition(template) { /** * The the name of the page, which should be a translation table key. * Alternatively, this may also be a list of names, where the final * name represents the page and earlier names represent categorization. * Those categorical names may be rendered hierarchically as a system * of menus, tabs, etc. * * @type String|String[] */ this.name = template.name; /** * The URL of the page. * * @type String */ this.url = template.url; /** * The CSS class name to associate with this page, if any. This will be * an empty string by default. * * @type String */ this.className = template.className || ''; /** * A numeric value denoting the relative sort order when compared to * other sibling PageDefinitions. If unspecified, sort order is * determined by the system using the PageDefinition. * * @type Number */ this.weight = template.weight; }; return PageDefinition; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/navigation/types/ClientIdentifier.js0100664 0001751 0001751 00000012150 12612226251 033315 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the ClientIdentifier class definition. */ angular.module('client').factory('ClientIdentifier', ['$injector', function defineClientIdentifier($injector) { // Required services var authenticationService = $injector.get('authenticationService'); var $window = $injector.get('$window'); /** * Object which uniquely identifies a particular connection or connection * group within Guacamole. This object can be converted to/from a string to * generate a guaranteed-unique, deterministic identifier for client URLs. * * @constructor * @param {ClientIdentifier|Object} [template={}] * The object whose properties should be copied within the new * ClientIdentifier. */ var ClientIdentifier = function ClientIdentifier(template) { // Use empty object by default template = template || {}; /** * The identifier of the data source associated with the object to * which the client will connect. This identifier will be the * identifier of an AuthenticationProvider within the Guacamole web * application. * * @type String */ this.dataSource = template.dataSource; /** * The type of object to which the client will connect. Possible values * are defined within ClientIdentifier.Types. * * @type String */ this.type = template.type; /** * The unique identifier of the object to which the client will * connect. * * @type String */ this.id = template.id; }; /** * All possible ClientIdentifier types. * * @type Object. */ ClientIdentifier.Types = { /** * The type string for a Guacamole connection. * * @type String */ CONNECTION : 'c', /** * The type string for a Guacamole connection group. * * @type String */ CONNECTION_GROUP : 'g' }; /** * Converts the given ClientIdentifier or ClientIdentifier-like object to * a String representation. Any object having the same properties as * ClientIdentifier may be used, but only those properties will be taken * into account when producing the resulting String. * * @param {ClientIdentifier|Object} id * The ClientIdentifier or ClientIdentifier-like object to convert to * a String representation. * * @returns {String} * A deterministic String representation of the given ClientIdentifier * or ClientIdentifier-like object. */ ClientIdentifier.toString = function toString(id) { return $window.btoa([ id.id, id.type, id.dataSource ].join('\0')); }; /** * Converts the given String into the corresponding ClientIdentifier. If * the provided String is not a valid identifier, it will be interpreted * as the identifier of a connection within the data source that * authenticated the current user. * * @param {String} str * The String to convert to a ClientIdentifier. * * @returns {ClientIdentifier} * The ClientIdentifier represented by the given String. */ ClientIdentifier.fromString = function fromString(str) { try { var values = $window.atob(str).split('\0'); return new ClientIdentifier({ id : values[0], type : values[1], dataSource : values[2] }); } // If the provided string is invalid, transform into a reasonable guess catch (e) { return new ClientIdentifier({ id : str, type : ClientIdentifier.Types.CONNECTION, dataSource : authenticationService.getDataSource() || 'default' }); } }; return ClientIdentifier; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/navigation/types/MenuAction.js0100664 0001751 0001751 00000004662 12612226251 032147 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the MenuAction class definition. */ angular.module('navigation').factory('MenuAction', [function defineMenuAction() { /** * Creates a new MenuAction, which pairs an arbitrary callback with * an action name. The name of this action will ultimately be presented to * the user when the user when this action's associated menu is open. * * @constructor * @param {String} name * The name of this action. * * @param {Function} callback * The callback to call when the user elects to perform this action. * * @param {String} className * The CSS class to associate with this action, if any. */ var MenuAction = function MenuAction(name, callback, className) { /** * Reference to this MenuAction. * * @type MenuAction */ var action = this; /** * The CSS class associated with this action. * * @type String */ this.className = className; /** * The name of this action. * * @type String */ this.name = name; /** * The callback to call when this action is performed. * * @type Function */ this.callback = callback; }; return MenuAction; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/navigation/templates/guacUserMenu.html0100664 0001751 0001751 00000004313 12612226251 033663 0ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/navigation/templates/guacPageList.html0100664 0001751 0001751 00000003160 12612226251 033627 0ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/navigation/navigationModule.js0100664 0001751 0001751 00000002370 12612226251 032240 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Module for generating and implementing user navigation options. */ angular.module('navigation', [ 'auth', 'notification', 'rest' ]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/textInput/directives/guacTextInput.js0100664 0001751 0001751 00000031243 12612226251 033546 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive which displays the Guacamole text input method. */ angular.module('textInput').directive('guacTextInput', [function guacTextInput() { return { restrict: 'E', replace: true, scope: { /** * Whether the text input UI should have focus. Setting this value * is not guaranteed to work, due to browser limitations. * * @type Boolean */ needsFocus : '=?' }, templateUrl: 'app/textInput/templates/guacTextInput.html', controller: ['$scope', '$rootScope', '$element', '$timeout', function guacTextInput($scope, $rootScope, $element, $timeout) { /** * The number of characters to include on either side of text input * content, to allow the user room to use backspace and delete. * * @type Number */ var TEXT_INPUT_PADDING = 4; /** * The Unicode codepoint of the character to use for padding on * either side of text input content. * * @type Number */ var TEXT_INPUT_PADDING_CODEPOINT = 0x200B; /** * Keys which should be allowed through to the client when in text * input mode, providing corresponding key events are received. * Keys in this set will be allowed through to the server. * * @type Object. */ var ALLOWED_KEYS = { 0xFE03: true, /* AltGr */ 0xFF08: true, /* Backspace */ 0xFF09: true, /* Tab */ 0xFF0D: true, /* Enter */ 0xFF1B: true, /* Escape */ 0xFF50: true, /* Home */ 0xFF51: true, /* Left */ 0xFF52: true, /* Up */ 0xFF53: true, /* Right */ 0xFF54: true, /* Down */ 0xFF57: true, /* End */ 0xFF64: true, /* Insert */ 0xFFBE: true, /* F1 */ 0xFFBF: true, /* F2 */ 0xFFC0: true, /* F3 */ 0xFFC1: true, /* F4 */ 0xFFC2: true, /* F5 */ 0xFFC3: true, /* F6 */ 0xFFC4: true, /* F7 */ 0xFFC5: true, /* F8 */ 0xFFC6: true, /* F9 */ 0xFFC7: true, /* F10 */ 0xFFC8: true, /* F11 */ 0xFFC9: true, /* F12 */ 0xFFE1: true, /* Left shift */ 0xFFE2: true, /* Right shift */ 0xFFE3: true, /* Left ctrl */ 0xFFE4: true, /* Right ctrl */ 0xFFE9: true, /* Left alt */ 0xFFEA: true, /* Right alt */ 0xFFFF: true /* Delete */ }; /** * Recently-sent text, ordered from oldest to most recent. * * @type String[] */ $scope.sentText = []; /** * Whether the "Alt" key is currently pressed within the text input * interface. * * @type Boolean */ $scope.altPressed = false; /** * Whether the "Ctrl" key is currently pressed within the text * input interface. * * @type Boolean */ $scope.ctrlPressed = false; /** * The text area input target. * * @type Element */ var target = $element.find('.target')[0]; /** * Whether the text input target currently has focus. Setting this * attribute has no effect, but any bound property will be updated * as focus is gained or lost. * * @type Boolean */ var hasFocus = false; target.onfocus = function targetFocusGained() { hasFocus = true; resetTextInputTarget(TEXT_INPUT_PADDING); }; target.onblur = function targetFocusLost() { hasFocus = false; target.focus(); }; /** * Whether composition is currently active within the text input * target element, such as when an IME is in use. * * @type Boolean */ var composingText = false; target.addEventListener("compositionstart", function targetComposeStart(e) { composingText = true; }, false); target.addEventListener("compositionend", function targetComposeEnd(e) { composingText = false; }, false); /** * Translates a given Unicode codepoint into the corresponding X11 * keysym. * * @param {Number} codepoint * The Unicode codepoint to translate. * * @returns {Number} * The X11 keysym that corresponds to the given Unicode * codepoint, or null if no such keysym exists. */ var keysymFromCodepoint = function keysymFromCodepoint(codepoint) { // Keysyms for control characters if (codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F)) return 0xFF00 | codepoint; // Keysyms for ASCII chars if (codepoint >= 0x0000 && codepoint <= 0x00FF) return codepoint; // Keysyms for Unicode if (codepoint >= 0x0100 && codepoint <= 0x10FFFF) return 0x01000000 | codepoint; return null; }; /** * Presses and releases the key corresponding to the given keysym, * as if typed by the user. * * @param {Number} keysym The keysym of the key to send. */ var sendKeysym = function sendKeysym(keysym) { $rootScope.$broadcast('guacSyntheticKeydown', keysym); $rootScope.$broadcast('guacSyntheticKeyup', keysym); }; /** * Presses and releases the key having the keysym corresponding to * the Unicode codepoint given, as if typed by the user. * * @param {Number} codepoint * The Unicode codepoint of the key to send. */ var sendCodepoint = function sendCodepoint(codepoint) { if (codepoint === 10) { sendKeysym(0xFF0D); releaseStickyKeys(); return; } var keysym = keysymFromCodepoint(codepoint); if (keysym) { sendKeysym(keysym); releaseStickyKeys(); } }; /** * Translates each character within the given string to keysyms and * sends each, in order, as if typed by the user. * * @param {String} content * The string to send. */ var sendString = function sendString(content) { var sentText = ""; // Send each codepoint within the string for (var i=0; i
{{text}}
guacamole-client-0.9.9/guacamole/src/main/webapp/app/textInput/templates/guacKey.html0100664 0001751 0001751 00000002440 12612226251 032514 0ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/app/notification/services/guacNotification.js0100664 0001751 0001751 00000006064 12612226251 034377 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Service for displaying notifications and modal status dialogs. */ angular.module('notification').factory('guacNotification', ['$injector', function guacNotification($injector) { // Required services var $rootScope = $injector.get('$rootScope'); var sessionStorageFactory = $injector.get('sessionStorageFactory'); var service = {}; /** * Getter/setter which retrieves or sets the current status notification, * which may simply be false if no status is currently shown. * * @type Function */ var storedStatus = sessionStorageFactory.create(false); /** * Retrieves the current status notification, which may simply be false if * no status is currently shown. * * @type Notification|Boolean */ service.getStatus = function getStatus() { return storedStatus(); }; /** * Shows or hides the given notification as a modal status. If a status * notification is currently shown, no further statuses will be shown * until the current status is hidden. * * @param {Notification|Boolean|Object} status * The status notification to show. * * @example * * // To show a status message with actions * guacNotification.showStatus({ * 'title' : 'Disconnected', * 'text' : 'You have been disconnected!', * 'actions' : { * 'name' : 'reconnect', * 'callback' : function () { * // Reconnection code goes here * } * } * }); * * // To hide the status message * guacNotification.showStatus(false); */ service.showStatus = function showStatus(status) { if (!storedStatus() || !status) storedStatus(status); }; // Hide status upon navigation $rootScope.$on('$routeChangeSuccess', function() { service.showStatus(false); }); return service; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/notification/directives/guacNotification.js0100664 0001751 0001751 00000006003 12612226251 034706 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * A directive for the guacamole client. */ angular.module('notification').directive('guacNotification', [function guacNotification() { return { restrict: 'E', replace: true, scope: { /** * The notification to display. * * @type Notification|Object */ notification : '=' }, templateUrl: 'app/notification/templates/guacNotification.html', controller: ['$scope', '$interval', function guacNotificationController($scope, $interval) { // Update progress bar if end known $scope.$watch("notification.progress.ratio", function updateProgress(ratio) { $scope.progressPercent = ratio * 100; }); $scope.$watch("notification", function resetTimeRemaining(notification) { var countdown = notification.countdown; // Clean up any existing interval if ($scope.interval) $interval.cancel($scope.interval); // Update and handle countdown, if provided if (countdown) { $scope.timeRemaining = countdown.remaining; $scope.interval = $interval(function updateTimeRemaining() { // Update time remaining $scope.timeRemaining--; // Call countdown callback when time remaining expires if ($scope.timeRemaining === 0 && countdown.callback) countdown.callback(); }, 1000, $scope.timeRemaining); } }); // Clean up interval upon destruction $scope.$on("$destroy", function destroyNotification() { if ($scope.interval) $interval.cancel($scope.interval); }); }] }; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/notification/notificationModule.js0100664 0001751 0001751 00000002332 12612226251 033114 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The module for code used to display arbitrary notifications. */ angular.module('notification', [ 'storage' ]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/notification/styles/notification.css0100664 0001751 0001751 00000005641 12612226251 033453 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ .notification { border: 1px solid rgba(0, 0, 0, 0.125); box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.125); background: white; color: black; } .notification.error { background: #FDD; } .notification .body { margin: 0.5em; } .notification .buttons { margin: 0.5em; } @keyframes notification-progress { from {background-position: 0px 0px;} to {background-position: 64px 0px;} } @-webkit-keyframes notification-progress { from {background-position: 0px 0px;} to {background-position: 64px 0px;} } .notification .title-bar { font-size: 1.25em; font-weight: bold; text-transform: uppercase; border-bottom: 1px solid rgba(0, 0, 0, 0.125); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.125); background: rgba(0, 0, 0, 0.04); padding: 0.5em; margin-bottom: 1em; } .notification .progress .bar { background: #A3D655; position: absolute; top: 0; left: 0; height: 100%; width: 0; box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.5), inset -1px -1px 0 rgba( 0, 0, 0, 0.1), 1px 1px 0 gray; } .notification .progress { width: 100%; background: #C2C2C2 url('images/progress.png'); background-size: 16px 16px; -moz-background-size: 16px 16px; -webkit-background-size: 16px 16px; -khtml-background-size: 16px 16px; animation-name: notification-progress; animation-duration: 2s; animation-timing-function: linear; animation-iteration-count: infinite; -webkit-animation-name: notification-progress; -webkit-animation-duration: 2s; -webkit-animation-timing-function: linear; -webkit-animation-iteration-count: infinite; padding: 0.25em; border: 1px solid gray; position: relative; } .notification .progress .text { position: relative; }guacamole-client-0.9.9/guacamole/src/main/webapp/app/notification/types/Notification.js0100664 0001751 0001751 00000005531 12612226251 033056 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the Notification class definition. */ angular.module('notification').factory('Notification', [function defineNotification() { /** * Creates a new Notification, initializing the properties of that * Notification with the corresponding properties of the given template. * * @constructor * @param {Notification|Object} [template={}] * The object whose properties should be copied within the new * Notification. */ var Notification = function Notification(template) { // Use empty object by default template = template || {}; /** * The CSS class to associate with the notification, if any. * * @type String */ this.className = template.className; /** * The title of the notification. * * @type String */ this.title = template.title; /** * The body text of the notification. * * @type String */ this.text = template.text; /** * An array of all actions available to the user in response to this * notification. * * @type NotificationAction[] */ this.actions = template.actions || []; /** * The current progress state of the ongoing action associated with this * notification. * * @type NotificationProgress */ this.progress = template.progress; /** * The countdown and corresponding default action which applies to * this notification, if any. * * @type NotificationCountdown */ this.countdown = template.countdown; }; return Notification; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/notification/types/NotificationCountdown.js0100664 0001751 0001751 00000005345 12612226251 034762 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the NotificationCountdown class definition. */ angular.module('notification').factory('NotificationCountdown', [function defineNotificationCountdown() { /** * Creates a new NotificationCountdown which describes an action that * should be performed after a specific number of seconds has elapsed. * * @constructor * @param {String} text The body text of the notification countdown. * * @param {Number} remaining * The number of seconds remaining in the countdown. * * @param {Function} [callback] * The callback to call when the countdown elapses. */ var NotificationCountdown = function NotificationCountdown(text, remaining, callback) { /** * Reference to this NotificationCountdown. * * @type NotificationCountdown */ var countdown = this; /** * The body text of the notification countdown. For the sake of i18n, * the variable REMAINING should be applied within the translation * string for formatting plurals, etc. * * @type String */ this.text = text; /** * The number of seconds remaining in the countdown. After this number * of seconds elapses, the callback associated with this * NotificationCountdown will be called. * * @type Number */ this.remaining = remaining; /** * The callback to call when this countdown expires. * * @type Function */ this.callback = callback; }; return NotificationCountdown; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/notification/types/NotificationProgress.js0100664 0001751 0001751 00000006146 12612226251 034606 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the NotificationProgress class definition. */ angular.module('notification').factory('NotificationProgress', [function defineNotificationProgress() { /** * Creates a new NotificationProgress which describes the current status * of an operation, and how much of that operation remains to be performed. * * @constructor * @param {String} text The text describing the operation progress. * * @param {Number} value * The current state of operation progress, as an arbitrary number * which increases as the operation continues. * * @param {String} [unit] * The unit of the arbitrary value, if that value has an associated * unit. * * @param {Number} [ratio] * If known, the current status of the operation as a value between 0 * and 1 inclusive, where 0 is not yet started, and 1 is complete. */ var NotificationProgress = function NotificationProgress(text, value, unit, ratio) { /** * The text describing the operation progress. For the sake of i18n, * the variable PROGRESS should be applied within the translation * string for formatting plurals, etc., while UNIT should be used * for the progress unit, if any. * * @type String */ this.text = text; /** * The current state of operation progress, as an arbitrary number which * increases as the operation continues. * * @type Number */ this.value = value; /** * The unit of the arbitrary value, if that value has an associated * unit. * * @type String */ this.unit = unit; /** * If known, the current status of the operation as a value between 0 * and 1 inclusive, where 0 is not yet started, and 1 is complete. * * @type String */ this.ratio = ratio; }; return NotificationProgress; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/notification/types/NotificationAction.js0100664 0001751 0001751 00000004764 12612226251 034223 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides the NotificationAction class definition. */ angular.module('notification').factory('NotificationAction', [function defineNotificationAction() { /** * Creates a new NotificationAction, which pairs an arbitrary callback with * an action name. The name of this action will ultimately be presented to * the user when the user is prompted to choose among available actions. * * @constructor * @param {String} name The name of this action. * * @param {Function} callback * The callback to call when the user elects to perform this action. * * @param {String} className * The CSS class to associate with this action, if any. */ var NotificationAction = function NotificationAction(name, callback, className) { /** * Reference to this NotificationAction. * * @type NotificationAction */ var action = this; /** * The CSS class associated with this action. * * @type String */ this.className = className; /** * The name of this action. * * @type String */ this.name = name; /** * The callback to call when this action is performed. * * @type Function */ this.callback = callback; }; return NotificationAction; }]); guacamole-client-0.9.9/guacamole/src/main/webapp/app/notification/templates/guacNotification.html0100664 0001751 0001751 00000004721 12612226251 035100 0ustar00mjumpermjumper0000000 0000000
{{notification.title | translate}}

{{notification.text | translate}}

guacamole-client-0.9.9/guacamole/src/main/webapp/layouts/it-it-qwerty.json0100664 0001751 0001751 00000037766 12612226251 030451 0ustar00mjumpermjumper0000000 0000000 { "language" : "it_IT", "type" : "qwerty", "width" : 23, "keys" : { "Esc" : 65307, "F1" : 65470, "F2" : 65471, "F3" : 65472, "F4" : 65473, "F5" : 65474, "F6" : 65475, "F7" : 65476, "F8" : 65477, "F9" : 65478, "F10" : 65479, "F11" : 65480, "F12" : 65481, "Space" : " ", "Back" : [{ "title" : "⟵", "keysym" : 65288 }], "Tab" : [{ "title" : "Tab ↹", "keysym" : 65289 }], "Enter" : [{ "title" : "↵", "keysym" : 65293 }], "Home" : [{ "title" : "Home", "keysym" : 65360 }], "PgUp" : [{ "title" : "PgUp ↑", "keysym" : 65365 }], "PgDn" : [{ "title" : "PgDn ↓", "keysym" : 65366 }], "End" : [{ "title" : "End", "keysym" : 65367 }], "Ins" : [{ "title" : "Ins", "keysym" : 65379 }], "Del" : [{ "title" : "Del", "keysym" : 65535 }], "Left" : [{ "title" : "â†", "keysym" : 65361 }], "Up" : [{ "title" : "↑", "keysym" : 65362 }], "Right" : [{ "title" : "→", "keysym" : 65363 }], "Down" : [{ "title" : "↓", "keysym" : 65364 }], "Menu" : [{ "title" : "Menu", "modifier" : "super", "keysym" : 65383 }], "LShift" : [{ "title" : "Shift", "modifier" : "shift", "keysym" : 65505 }], "RShift" : [{ "title" : "Shift", "modifier" : "shift", "keysym" : 65506 }], "LCtrl" : [{ "title" : "Ctrl", "modifier" : "control", "keysym" : 65507 }], "RCtrl" : [{ "title" : "Ctrl", "modifier" : "control", "keysym" : 65508 }], "Caps" : [{ "title" : "Caps", "modifier" : "caps", "keysym" : 65509 }], "LAlt" : [{ "title" : "Alt", "modifier" : "alt", "keysym" : 65513 }], "AltGr" : [{ "title" : "AltGr", "modifier" : "alt-gr", "keysym" : 65027 }], "Super" : [{ "title" : "Super", "modifier" : "super", "keysym" : 65515 }], "\\" : [ { "title" : "\\", "requires" : [ ] }, { "title" : "|", "requires" : [ "shift" ] } ], "1" : [ { "title" : "1", "requires" : [ ] }, { "title" : "!", "requires" : [ "shift" ] } ], "2" : [ { "title" : "2", "requires" : [ ] }, { "title" : "\"", "requires" : [ "shift" ] } ], "3" : [ { "title" : "3", "requires" : [ ] }, { "title" : "£", "requires" : [ "shift" ] } ], "4" : [ { "title" : "4", "requires" : [ ] }, { "title" : "$", "requires" : [ "shift" ] } ], "5" : [ { "title" : "5", "requires" : [ ] }, { "title" : "%", "requires" : [ "shift" ] }, { "title" : "€", "requires" : [ "alt-gr" ] } ], "6" : [ { "title" : "6", "requires" : [ ] }, { "title" : "&", "requires" : [ "shift" ] } ], "7" : [ { "title" : "7", "requires" : [ ] }, { "title" : "/", "requires" : [ "shift" ] } ], "8" : [ { "title" : "8", "requires" : [ ] }, { "title" : "(", "requires" : [ "shift" ] } ], "9" : [ { "title" : "9", "requires" : [ ] }, { "title" : ")", "requires" : [ "shift" ] } ], "0" : [ { "title" : "0", "requires" : [ ] }, { "title" : "=", "requires" : [ "shift" ] } ], "'" : [ { "title" : "'", "requires" : [ ] }, { "title" : "?", "requires" : [ "shift" ] }, { "title" : "`", "requires" : [ "alt-gr", "shift" ] } ], "ì" : [ { "title" : "ì", "requires" : [ ] }, { "title" : "^", "requires" : [ "shift" ] }, { "title" : "~", "requires" : [ "alt-gr", "shift" ] } ], "q" : [ { "title" : "q", "requires" : [ ] }, { "title" : "Q", "requires" : [ "caps" ] }, { "title" : "Q", "requires" : [ "shift" ] }, { "title" : "q", "requires" : [ "caps", "shift" ] } ], "w" : [ { "title" : "w", "requires" : [ ] }, { "title" : "W", "requires" : [ "caps" ] }, { "title" : "W", "requires" : [ "shift" ] }, { "title" : "w", "requires" : [ "caps", "shift" ] } ], "e" : [ { "title" : "e", "requires" : [ ] }, { "title" : "E", "requires" : [ "caps" ] }, { "title" : "E", "requires" : [ "shift" ] }, { "title" : "e", "requires" : [ "caps", "shift" ] }, { "title" : "€", "requires" : [ "alt-gr" ] } ], "r" : [ { "title" : "r", "requires" : [ ] }, { "title" : "R", "requires" : [ "caps" ] }, { "title" : "R", "requires" : [ "shift" ] }, { "title" : "r", "requires" : [ "caps", "shift" ] } ], "t" : [ { "title" : "t", "requires" : [ ] }, { "title" : "T", "requires" : [ "caps" ] }, { "title" : "T", "requires" : [ "shift" ] }, { "title" : "t", "requires" : [ "caps", "shift" ] } ], "y" : [ { "title" : "y", "requires" : [ ] }, { "title" : "Y", "requires" : [ "caps" ] }, { "title" : "Y", "requires" : [ "shift" ] }, { "title" : "y", "requires" : [ "caps", "shift" ] } ], "u" : [ { "title" : "u", "requires" : [ ] }, { "title" : "U", "requires" : [ "caps" ] }, { "title" : "U", "requires" : [ "shift" ] }, { "title" : "u", "requires" : [ "caps", "shift" ] } ], "i" : [ { "title" : "i", "requires" : [ ] }, { "title" : "I", "requires" : [ "caps" ] }, { "title" : "I", "requires" : [ "shift" ] }, { "title" : "i", "requires" : [ "caps", "shift" ] } ], "o" : [ { "title" : "o", "requires" : [ ] }, { "title" : "O", "requires" : [ "caps" ] }, { "title" : "O", "requires" : [ "shift" ] }, { "title" : "o", "requires" : [ "caps", "shift" ] } ], "p" : [ { "title" : "p", "requires" : [ ] }, { "title" : "P", "requires" : [ "caps" ] }, { "title" : "P", "requires" : [ "shift" ] }, { "title" : "p", "requires" : [ "caps", "shift" ] } ], "è" : [ { "title" : "è", "requires" : [ ] }, { "title" : "è", "requires" : [ "caps" ] }, { "title" : "é", "requires" : [ "shift" ] }, { "title" : "é", "requires" : [ "caps", "shift" ] }, { "title" : "[", "requires" : [ "alt-gr" ] }, { "title" : "{", "requires" : [ "alt-gr", "shift" ] } ], "+" : [ { "title" : "+", "requires" : [ ] }, { "title" : "+", "requires" : [ "caps" ] }, { "title" : "*", "requires" : [ "shift" ] }, { "title" : "*", "requires" : [ "caps", "shift" ] }, { "title" : "]", "requires" : [ "alt-gr" ] }, { "title" : "}", "requires" : [ "alt-gr", "shift" ] } ], "a" : [ { "title" : "a", "requires" : [ ] }, { "title" : "A", "requires" : [ "caps" ] }, { "title" : "A", "requires" : [ "shift" ] }, { "title" : "a", "requires" : [ "caps", "shift" ] } ], "s" : [ { "title" : "s", "requires" : [ ] }, { "title" : "S", "requires" : [ "caps" ] }, { "title" : "S", "requires" : [ "shift" ] }, { "title" : "s", "requires" : [ "caps", "shift" ] } ], "d" : [ { "title" : "d", "requires" : [ ] }, { "title" : "D", "requires" : [ "caps" ] }, { "title" : "D", "requires" : [ "shift" ] }, { "title" : "d", "requires" : [ "caps", "shift" ] } ], "f" : [ { "title" : "f", "requires" : [ ] }, { "title" : "F", "requires" : [ "caps" ] }, { "title" : "F", "requires" : [ "shift" ] }, { "title" : "f", "requires" : [ "caps", "shift" ] } ], "g" : [ { "title" : "g", "requires" : [ ] }, { "title" : "G", "requires" : [ "caps" ] }, { "title" : "G", "requires" : [ "shift" ] }, { "title" : "g", "requires" : [ "caps", "shift" ] } ], "h" : [ { "title" : "h", "requires" : [ ] }, { "title" : "H", "requires" : [ "caps" ] }, { "title" : "H", "requires" : [ "shift" ] }, { "title" : "h", "requires" : [ "caps", "shift" ] } ], "j" : [ { "title" : "j", "requires" : [ ] }, { "title" : "J", "requires" : [ "caps" ] }, { "title" : "J", "requires" : [ "shift" ] }, { "title" : "j", "requires" : [ "caps", "shift" ] } ], "k" : [ { "title" : "k", "requires" : [ ] }, { "title" : "K", "requires" : [ "caps" ] }, { "title" : "K", "requires" : [ "shift" ] }, { "title" : "k", "requires" : [ "caps", "shift" ] } ], "l" : [ { "title" : "l", "requires" : [ ] }, { "title" : "L", "requires" : [ "caps" ] }, { "title" : "L", "requires" : [ "shift" ] }, { "title" : "l", "requires" : [ "caps", "shift" ] } ], "ò" : [ { "title" : "ò", "requires" : [ ] }, { "title" : "ò", "requires" : [ "caps" ] }, { "title" : "ç", "requires" : [ "shift" ] }, { "title" : "ç", "requires" : [ "caps", "shift" ] }, { "title" : "@", "requires" : [ "alt-gr" ] } ], "à" : [ { "title" : "à", "requires" : [ ] }, { "title" : "à", "requires" : [ "caps" ] }, { "title" : "°", "requires" : [ "shift" ] }, { "title" : "°", "requires" : [ "caps", "shift" ] }, { "title" : "#", "requires" : [ "alt-gr" ] } ], "ù" : [ { "title" : "ù", "requires" : [ ] }, { "title" : "ù", "requires" : [ "caps" ] }, { "title" : "§", "requires" : [ "shift" ] }, { "title" : "§", "requires" : [ "caps", "shift" ] } ], "<" : [ { "title" : "<", "requires" : [ ] }, { "title" : ">", "requires" : [ "shift" ] } ], "z" : [ { "title" : "z", "requires" : [ ] }, { "title" : "Z", "requires" : [ "caps" ] }, { "title" : "Z", "requires" : [ "shift" ] }, { "title" : "z", "requires" : [ "caps", "shift" ] } ], "x" : [ { "title" : "x", "requires" : [ ] }, { "title" : "X", "requires" : [ "caps" ] }, { "title" : "X", "requires" : [ "shift" ] }, { "title" : "x", "requires" : [ "caps", "shift" ] } ], "c" : [ { "title" : "c", "requires" : [ ] }, { "title" : "C", "requires" : [ "caps" ] }, { "title" : "C", "requires" : [ "shift" ] }, { "title" : "c", "requires" : [ "caps", "shift" ] } ], "v" : [ { "title" : "v", "requires" : [ ] }, { "title" : "V", "requires" : [ "caps" ] }, { "title" : "V", "requires" : [ "shift" ] }, { "title" : "v", "requires" : [ "caps", "shift" ] } ], "b" : [ { "title" : "b", "requires" : [ ] }, { "title" : "B", "requires" : [ "caps" ] }, { "title" : "B", "requires" : [ "shift" ] }, { "title" : "b", "requires" : [ "caps", "shift" ] } ], "n" : [ { "title" : "n", "requires" : [ ] }, { "title" : "N", "requires" : [ "caps" ] }, { "title" : "N", "requires" : [ "shift" ] }, { "title" : "n", "requires" : [ "caps", "shift" ] } ], "m" : [ { "title" : "m", "requires" : [ ] }, { "title" : "M", "requires" : [ "caps" ] }, { "title" : "M", "requires" : [ "shift" ] }, { "title" : "m", "requires" : [ "caps", "shift" ] }, { "title" : "µ", "requires" : [ "alt-gr" ] } ], "," : [ { "title" : ",", "requires" : [ ] }, { "title" : ";", "requires" : [ "shift" ] } ], "." : [ { "title" : ".", "requires" : [ ] }, { "title" : ":", "requires" : [ "shift" ] } ], "-" : [ { "title" : "-", "requires" : [ ] }, { "title" : "_", "requires" : [ "shift" ] } ] }, "layout" : [ [ "Esc", 0.8, "F1", "F2", "F3", "F4", 0.8, "F5", "F6", "F7", "F8", 0.8, "F9", "F10", "F11", "F12" ], [ 0.1 ], { "main" : { "alpha" : [ [ "\\", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "'", "ì", "Back" ], [ "Tab", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "è", "+", 1, 0.6 ], [ "Caps", "a", "s", "d", "f", "g", "h", "j", "k", "l", "ò", "à", "ù", "Enter" ], [ "LShift", "<", "z", "x", "c", "v", "b", "n", "m", ",", ".", "-", "RShift" ], [ "LCtrl", "Super", "LAlt", "Space", "AltGr", "Menu", "RCtrl" ] ], "movement" : [ [ "Ins", "Home", "PgUp" ], [ "Del", "End", "PgDn" ], [ 1 ], [ "Up" ], [ "Left", "Down", "Right" ] ] } } ], "keyWidths" : { "Back" : 2, "Tab" : 1.75, "\\" : 1.25, "Caps" : 1.75, "Enter" : 1.5, "LShift" : 2.2, "RShift" : 2.2, "LCtrl" : 1.6, "Super" : 1.6, "LAlt" : 1.6, "Space" : 6.4, "AltGr" : 1.6, "Menu" : 1.6, "RCtrl" : 1.6, "Ins" : 1.6, "Home" : 1.6, "PgUp" : 1.6, "Del" : 1.6, "End" : 1.6, "PgDn" : 1.6 } } guacamole-client-0.9.9/guacamole/src/main/webapp/layouts/ru-ru-qwerty.json0100664 0001751 0001751 00000035543 12612226251 030464 0ustar00mjumpermjumper0000000 0000000 { "language" : "ru_RU", "type" : "qwerty", "width" : 22, "keys" : { "Back" : 65288, "Tab" : 65289, "Enter" : 65293, "Esc" : 65307, "Home" : 65360, "PgUp" : 65365, "PgDn" : 65366, "End" : 65367, "Ins" : 65379, "F1" : 65470, "F2" : 65471, "F3" : 65472, "F4" : 65473, "F5" : 65474, "F6" : 65475, "F7" : 65476, "F8" : 65477, "F9" : 65478, "F10" : 65479, "F11" : 65480, "F12" : 65481, "Del" : 65535, "Space" : " ", "Left" : [{ "title" : "â†", "keysym" : 65361 }], "Up" : [{ "title" : "↑", "keysym" : 65362 }], "Right" : [{ "title" : "→", "keysym" : 65363 }], "Down" : [{ "title" : "↓", "keysym" : 65364 }], "Menu" : [{ "title" : "Menu", "keysym" : 65383 }], "LShift" : [{ "title" : "Shift", "modifier" : "shift", "keysym" : 65505 }], "RShift" : [{ "title" : "Shift", "modifier" : "shift", "keysym" : 65506 }], "LCtrl" : [{ "title" : "Ctrl", "modifier" : "control", "keysym" : 65507 }], "RCtrl" : [{ "title" : "Ctrl", "modifier" : "control", "keysym" : 65508 }], "Caps" : [{ "title" : "Caps", "modifier" : "caps", "keysym" : 65509 }], "LAlt" : [{ "title" : "Alt", "modifier" : "alt", "keysym" : 65513 }], "RAlt" : [{ "title" : "Alt", "modifier" : "alt", "keysym" : 65514 }], "Super" : [{ "title" : "Super", "modifier" : "super", "keysym" : 65515 }], "`" : [ { "title" : "`", "requires" : [ ] }, { "title" : "~", "requires" : [ "shift" ] } ], "1" : [ { "title" : "1", "requires" : [ ] }, { "title" : "!", "requires" : [ "shift" ] } ], "2" : [ { "title" : "2", "requires" : [ ] }, { "title" : "@", "requires" : [ "shift" ] } ], "3" : [ { "title" : "3", "requires" : [ ] }, { "title" : "#", "requires" : [ "shift" ] } ], "4" : [ { "title" : "4", "requires" : [ ] }, { "title" : "$", "requires" : [ "shift" ] } ], "5" : [ { "title" : "5", "requires" : [ ] }, { "title" : "%", "requires" : [ "shift" ] } ], "6" : [ { "title" : "6", "requires" : [ ] }, { "title" : "^", "requires" : [ "shift" ] } ], "7" : [ { "title" : "7", "requires" : [ ] }, { "title" : "&", "requires" : [ "shift" ] } ], "8" : [ { "title" : "8", "requires" : [ ] }, { "title" : "*", "requires" : [ "shift" ] } ], "9" : [ { "title" : "9", "requires" : [ ] }, { "title" : "(", "requires" : [ "shift" ] } ], "0" : [ { "title" : "0", "requires" : [ ] }, { "title" : ")", "requires" : [ "shift" ] } ], "-" : [ { "title" : "-", "requires" : [ ] }, { "title" : "_", "requires" : [ "shift" ] } ], "=" : [ { "title" : "=", "requires" : [ ] }, { "title" : "+", "requires" : [ "shift" ] } ], "б" : [ { "title" : "б", "requires" : [ ] }, { "title" : "Б", "requires" : [ "caps" ] }, { "title" : "Б", "requires" : [ "shift" ] }, { "title" : "б", "requires" : [ "caps", "shift" ] } ], "ÑŽ" : [ { "title" : "ÑŽ", "requires" : [ ] }, { "title" : "Ю", "requires" : [ "caps" ] }, { "title" : "Ю", "requires" : [ "shift" ] }, { "title" : "ÑŽ", "requires" : [ "caps", "shift" ] } ], "/" : [ { "title" : "/", "requires" : [ ] }, { "title" : "?", "requires" : [ "shift" ] } ], "Ñ…" : [ { "title" : "Ñ…", "requires" : [ ] }, { "title" : "Ð¥", "requires" : [ "caps" ] }, { "title" : "Ð¥", "requires" : [ "shift" ] }, { "title" : "Ñ…", "requires" : [ "caps", "shift" ] } ], "ÑŠ" : [ { "title" : "ÑŠ", "requires" : [ ] }, { "title" : "Ъ", "requires" : [ "caps" ] }, { "title" : "Ъ", "requires" : [ "shift" ] }, { "title" : "ÑŠ", "requires" : [ "caps", "shift" ] } ], "\\" : [ { "title" : "\\", "requires" : [ ] }, { "title" : "|", "requires" : [ "shift" ] } ], "ж" : [ { "title" : "ж", "requires" : [ ] }, { "title" : "Ж", "requires" : [ "caps" ] }, { "title" : "Ж", "requires" : [ "shift" ] }, { "title" : "ж", "requires" : [ "caps", "shift" ] } ], "Ñ" : [ { "title" : "Ñ", "requires" : [ ] }, { "title" : "Э", "requires" : [ "caps" ] }, { "title" : "Э", "requires" : [ "shift" ] }, { "title" : "Ñ", "requires" : [ "caps", "shift" ] } ], "й" : [ { "title" : "й", "requires" : [ ] }, { "title" : "Й", "requires" : [ "caps" ] }, { "title" : "Й", "requires" : [ "shift" ] }, { "title" : "й", "requires" : [ "caps", "shift" ] } ], "ц" : [ { "title" : "ц", "requires" : [ ] }, { "title" : "Ц", "requires" : [ "caps" ] }, { "title" : "Ц", "requires" : [ "shift" ] }, { "title" : "ц", "requires" : [ "caps", "shift" ] } ], "у" : [ { "title" : "у", "requires" : [ ] }, { "title" : "У", "requires" : [ "caps" ] }, { "title" : "У", "requires" : [ "shift" ] }, { "title" : "у", "requires" : [ "caps", "shift" ] } ], "к" : [ { "title" : "к", "requires" : [ ] }, { "title" : "К", "requires" : [ "caps" ] }, { "title" : "К", "requires" : [ "shift" ] }, { "title" : "к", "requires" : [ "caps", "shift" ] } ], "е" : [ { "title" : "е", "requires" : [ ] }, { "title" : "Е", "requires" : [ "caps" ] }, { "title" : "Е", "requires" : [ "shift" ] }, { "title" : "е", "requires" : [ "caps", "shift" ] } ], "н" : [ { "title" : "н", "requires" : [ ] }, { "title" : "Ð", "requires" : [ "caps" ] }, { "title" : "Ð", "requires" : [ "shift" ] }, { "title" : "н", "requires" : [ "caps", "shift" ] } ], "г" : [ { "title" : "г", "requires" : [ ] }, { "title" : "Г", "requires" : [ "caps" ] }, { "title" : "Г", "requires" : [ "shift" ] }, { "title" : "г", "requires" : [ "caps", "shift" ] } ], "ш" : [ { "title" : "ш", "requires" : [ ] }, { "title" : "Ш", "requires" : [ "caps" ] }, { "title" : "Ш", "requires" : [ "shift" ] }, { "title" : "ш", "requires" : [ "caps", "shift" ] } ], "щ" : [ { "title" : "щ", "requires" : [ ] }, { "title" : "Щ", "requires" : [ "caps" ] }, { "title" : "Щ", "requires" : [ "shift" ] }, { "title" : "щ", "requires" : [ "caps", "shift" ] } ], "з" : [ { "title" : "з", "requires" : [ ] }, { "title" : "З", "requires" : [ "caps" ] }, { "title" : "З", "requires" : [ "shift" ] }, { "title" : "з", "requires" : [ "caps", "shift" ] } ], "Ñ„" : [ { "title" : "Ñ„", "requires" : [ ] }, { "title" : "Ф", "requires" : [ "caps" ] }, { "title" : "Ф", "requires" : [ "shift" ] }, { "title" : "Ñ„", "requires" : [ "caps", "shift" ] } ], "Ñ‹" : [ { "title" : "Ñ‹", "requires" : [ ] }, { "title" : "Ы", "requires" : [ "caps" ] }, { "title" : "Ы", "requires" : [ "shift" ] }, { "title" : "Ñ‹", "requires" : [ "caps", "shift" ] } ], "в" : [ { "title" : "в", "requires" : [ ] }, { "title" : "Ð’", "requires" : [ "caps" ] }, { "title" : "Ð’", "requires" : [ "shift" ] }, { "title" : "в", "requires" : [ "caps", "shift" ] } ], "а" : [ { "title" : "а", "requires" : [ ] }, { "title" : "Ð", "requires" : [ "caps" ] }, { "title" : "Ð", "requires" : [ "shift" ] }, { "title" : "а", "requires" : [ "caps", "shift" ] } ], "п" : [ { "title" : "п", "requires" : [ ] }, { "title" : "П", "requires" : [ "caps" ] }, { "title" : "П", "requires" : [ "shift" ] }, { "title" : "п", "requires" : [ "caps", "shift" ] } ], "Ñ€" : [ { "title" : "Ñ€", "requires" : [ ] }, { "title" : "Р", "requires" : [ "caps" ] }, { "title" : "Р", "requires" : [ "shift" ] }, { "title" : "Ñ€", "requires" : [ "caps", "shift" ] } ], "о" : [ { "title" : "о", "requires" : [ ] }, { "title" : "О", "requires" : [ "caps" ] }, { "title" : "О", "requires" : [ "shift" ] }, { "title" : "о", "requires" : [ "caps", "shift" ] } ], "л" : [ { "title" : "л", "requires" : [ ] }, { "title" : "Л", "requires" : [ "caps" ] }, { "title" : "Л", "requires" : [ "shift" ] }, { "title" : "л", "requires" : [ "caps", "shift" ] } ], "д" : [ { "title" : "д", "requires" : [ ] }, { "title" : "Д", "requires" : [ "caps" ] }, { "title" : "Д", "requires" : [ "shift" ] }, { "title" : "д", "requires" : [ "caps", "shift" ] } ], "Ñ" : [ { "title" : "Ñ", "requires" : [ ] }, { "title" : "Я", "requires" : [ "caps" ] }, { "title" : "Я", "requires" : [ "shift" ] }, { "title" : "Ñ", "requires" : [ "caps", "shift" ] } ], "ч" : [ { "title" : "ч", "requires" : [ ] }, { "title" : "Ч", "requires" : [ "caps" ] }, { "title" : "Ч", "requires" : [ "shift" ] }, { "title" : "ч", "requires" : [ "caps", "shift" ] } ], "Ñ" : [ { "title" : "Ñ", "requires" : [ ] }, { "title" : "С", "requires" : [ "caps" ] }, { "title" : "С", "requires" : [ "shift" ] }, { "title" : "Ñ", "requires" : [ "caps", "shift" ] } ], "м" : [ { "title" : "м", "requires" : [ ] }, { "title" : "М", "requires" : [ "caps" ] }, { "title" : "М", "requires" : [ "shift" ] }, { "title" : "м", "requires" : [ "caps", "shift" ] } ], "и" : [ { "title" : "и", "requires" : [ ] }, { "title" : "И", "requires" : [ "caps" ] }, { "title" : "И", "requires" : [ "shift" ] }, { "title" : "и", "requires" : [ "caps", "shift" ] } ], "Ñ‚" : [ { "title" : "Ñ‚", "requires" : [ ] }, { "title" : "Т", "requires" : [ "caps" ] }, { "title" : "Т", "requires" : [ "shift" ] }, { "title" : "Ñ‚", "requires" : [ "caps", "shift" ] } ], "ÑŒ" : [ { "title" : "ÑŒ", "requires" : [ ] }, { "title" : "Ь", "requires" : [ "caps" ] }, { "title" : "Ь", "requires" : [ "shift" ] }, { "title" : "ÑŒ", "requires" : [ "caps", "shift" ] } ] }, "layout" : [ [ "Esc", 0.7, "F1", "F2", "F3", "F4", 0.7, "F5", "F6", "F7", "F8", 0.7, "F9", "F10", "F11", "F12" ], [ 0.1 ], { "main" : { "alpha" : [ [ "`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "Back" ], [ "Tab", "й", "ц", "у", "к", "е", "н", "г", "ш", "щ", "з", "Ñ…", "ÑŠ", "\\" ], [ "Caps", "Ñ„", "Ñ‹", "в", "а", "п", "Ñ€", "о", "л", "д", "ж", "Ñ", "Enter" ], [ "LShift", "Ñ", "ч", "Ñ", "м", "и", "Ñ‚", "ÑŒ", "б", "ÑŽ", "/", "RShift" ], [ "LCtrl", "Super", "LAlt", "Space", "RAlt", "Menu", "RCtrl" ] ], "movement" : [ [ "Ins", "Home", "PgUp" ], [ "Del", "End", "PgDn" ], [ 1 ], [ "Up" ], [ "Left", "Down", "Right" ] ] } } ], "keyWidths" : { "Back" : 2, "Tab" : 1.5, "\\" : 1.5, "Caps" : 1.85, "Enter" : 2.25, "LShift" : 2.1, "RShift" : 3.1, "LCtrl" : 1.6, "Super" : 1.6, "LAlt" : 1.6, "Space" : 6.1, "RAlt" : 1.6, "Menu" : 1.6, "RCtrl" : 1.6, "Ins" : 1.6, "Home" : 1.6, "PgUp" : 1.6, "Del" : 1.6, "End" : 1.6, "PgDn" : 1.6 } } guacamole-client-0.9.9/guacamole/src/main/webapp/layouts/fr-fr-azerty.json0100664 0001751 0001751 00000033515 12612226251 030406 0ustar00mjumpermjumper0000000 0000000 { "language" : "fr_FR", "type" : "azerty", "width" : 22, "keys" : { "Back" : 65288, "Tab" : 65289, "Enter" : 65293, "Esc" : 65307, "Home" : 65360, "PgUp" : 65365, "PgDn" : 65366, "End" : 65367, "Ins" : 65379, "F1" : 65470, "F2" : 65471, "F3" : 65472, "F4" : 65473, "F5" : 65474, "F6" : 65475, "F7" : 65476, "F8" : 65477, "F9" : 65478, "F10" : 65479, "F11" : 65480, "F12" : 65481, "Del" : 65535, "Space" : " ", "Left" : [{ "title" : "â†", "keysym" : 65361 }], "Up" : [{ "title" : "↑", "keysym" : 65362 }], "Right" : [{ "title" : "→", "keysym" : 65363 }], "Down" : [{ "title" : "↓", "keysym" : 65364 }], "Menu" : [{ "title" : "Menu", "keysym" : 65383 }], "LShift" : [{ "title" : "Shift", "modifier" : "shift", "keysym" : 65505 }], "RShift" : [{ "title" : "Shift", "modifier" : "shift", "keysym" : 65506 }], "LCtrl" : [{ "title" : "Ctrl", "modifier" : "control", "keysym" : 65507 }], "RCtrl" : [{ "title" : "Ctrl", "modifier" : "control", "keysym" : 65508 }], "Caps" : [{ "title" : "Caps", "modifier" : "caps", "keysym" : 65509 }], "LAlt" : [{ "title" : "Alt", "modifier" : "alt", "keysym" : 65513 }], "RAlt" : [{ "title" : "Alt", "modifier" : "alt", "keysym" : 65514 }], "Super" : [{ "title" : "Super", "modifier" : "super", "keysym" : 65515 }], "`" : [ { "title" : "`", "requires" : [ ] }, { "title" : "~", "requires" : [ "shift" ] } ], "&" : [ { "title" : "&", "requires" : [ ] }, { "title" : "1", "requires" : [ "shift" ] } ], "é" : [ { "title" : "é", "requires" : [ ] }, { "title" : "2", "requires" : [ "shift" ] } ], "\"" : [ { "title" : "\"", "requires" : [ ] }, { "title" : "3", "requires" : [ "shift" ] } ], "'" : [ { "title" : "'", "requires" : [ ] }, { "title" : "4", "requires" : [ "shift" ] } ], "(" : [ { "title" : "(", "requires" : [ ] }, { "title" : "5", "requires" : [ "shift" ] } ], "-" : [ { "title" : "-", "requires" : [ ] }, { "title" : "6", "requires" : [ "shift" ] } ], "è" : [ { "title" : "è", "requires" : [ ] }, { "title" : "7", "requires" : [ "shift" ] } ], "_" : [ { "title" : "_", "requires" : [ ] }, { "title" : "8", "requires" : [ "shift" ] } ], "ç" : [ { "title" : "ç", "requires" : [ ] }, { "title" : "9", "requires" : [ "shift" ] } ], "à" : [ { "title" : "à", "requires" : [ ] }, { "title" : "0", "requires" : [ "shift" ] } ], ")" : [ { "title" : ")", "requires" : [ ] }, { "title" : "°", "requires" : [ "shift" ] } ], "=" : [ { "title" : "=", "requires" : [ ] }, { "title" : "+", "requires" : [ "shift" ] } ], "^" : [ { "title" : "^", "requires" : [ ] }, { "title" : "¨", "requires" : [ "shift" ] } ], "$" : [ { "title" : "$", "requires" : [ ] }, { "title" : "£", "requires" : [ "shift" ] } ], "/" : [ { "title" : "/", "requires" : [ ] }, { "title" : "?", "requires" : [ "shift" ] } ], "[" : [ { "title" : "[", "requires" : [ ] }, { "title" : "{", "requires" : [ "shift" ] } ], "]" : [ { "title" : "]", "requires" : [ ] }, { "title" : "}", "requires" : [ "shift" ] } ], "\\" : [ { "title" : "\\", "requires" : [ ] }, { "title" : "|", "requires" : [ "shift" ] } ], ";" : [ { "title" : ";", "requires" : [ ] }, { "title" : ":", "requires" : [ "shift" ] } ], "'" : [ { "title" : "'", "requires" : [ ] }, { "title" : "\"", "requires" : [ "shift" ] } ], "q" : [ { "title" : "q", "requires" : [ ] }, { "title" : "Q", "requires" : [ "caps" ] }, { "title" : "Q", "requires" : [ "shift" ] }, { "title" : "q", "requires" : [ "caps", "shift" ] } ], "w" : [ { "title" : "w", "requires" : [ ] }, { "title" : "W", "requires" : [ "caps" ] }, { "title" : "W", "requires" : [ "shift" ] }, { "title" : "w", "requires" : [ "caps", "shift" ] } ], "e" : [ { "title" : "e", "requires" : [ ] }, { "title" : "E", "requires" : [ "caps" ] }, { "title" : "E", "requires" : [ "shift" ] }, { "title" : "€", "requires" : [ "caps", "shift" ] } ], "r" : [ { "title" : "r", "requires" : [ ] }, { "title" : "R", "requires" : [ "caps" ] }, { "title" : "R", "requires" : [ "shift" ] }, { "title" : "r", "requires" : [ "caps", "shift" ] } ], "t" : [ { "title" : "t", "requires" : [ ] }, { "title" : "T", "requires" : [ "caps" ] }, { "title" : "T", "requires" : [ "shift" ] }, { "title" : "t", "requires" : [ "caps", "shift" ] } ], "y" : [ { "title" : "y", "requires" : [ ] }, { "title" : "Y", "requires" : [ "caps" ] }, { "title" : "Y", "requires" : [ "shift" ] }, { "title" : "y", "requires" : [ "caps", "shift" ] } ], "u" : [ { "title" : "u", "requires" : [ ] }, { "title" : "U", "requires" : [ "caps" ] }, { "title" : "U", "requires" : [ "shift" ] }, { "title" : "u", "requires" : [ "caps", "shift" ] } ], "i" : [ { "title" : "i", "requires" : [ ] }, { "title" : "I", "requires" : [ "caps" ] }, { "title" : "I", "requires" : [ "shift" ] }, { "title" : "i", "requires" : [ "caps", "shift" ] } ], "o" : [ { "title" : "o", "requires" : [ ] }, { "title" : "O", "requires" : [ "caps" ] }, { "title" : "O", "requires" : [ "shift" ] }, { "title" : "o", "requires" : [ "caps", "shift" ] } ], "p" : [ { "title" : "p", "requires" : [ ] }, { "title" : "P", "requires" : [ "caps" ] }, { "title" : "P", "requires" : [ "shift" ] }, { "title" : "p", "requires" : [ "caps", "shift" ] } ], "a" : [ { "title" : "a", "requires" : [ ] }, { "title" : "A", "requires" : [ "caps" ] }, { "title" : "A", "requires" : [ "shift" ] }, { "title" : "a", "requires" : [ "caps", "shift" ] } ], "s" : [ { "title" : "s", "requires" : [ ] }, { "title" : "S", "requires" : [ "caps" ] }, { "title" : "S", "requires" : [ "shift" ] }, { "title" : "s", "requires" : [ "caps", "shift" ] } ], "d" : [ { "title" : "d", "requires" : [ ] }, { "title" : "D", "requires" : [ "caps" ] }, { "title" : "D", "requires" : [ "shift" ] }, { "title" : "d", "requires" : [ "caps", "shift" ] } ], "f" : [ { "title" : "f", "requires" : [ ] }, { "title" : "F", "requires" : [ "caps" ] }, { "title" : "F", "requires" : [ "shift" ] }, { "title" : "f", "requires" : [ "caps", "shift" ] } ], "g" : [ { "title" : "g", "requires" : [ ] }, { "title" : "G", "requires" : [ "caps" ] }, { "title" : "G", "requires" : [ "shift" ] }, { "title" : "g", "requires" : [ "caps", "shift" ] } ], "h" : [ { "title" : "h", "requires" : [ ] }, { "title" : "H", "requires" : [ "caps" ] }, { "title" : "H", "requires" : [ "shift" ] }, { "title" : "h", "requires" : [ "caps", "shift" ] } ], "j" : [ { "title" : "j", "requires" : [ ] }, { "title" : "J", "requires" : [ "caps" ] }, { "title" : "J", "requires" : [ "shift" ] }, { "title" : "j", "requires" : [ "caps", "shift" ] } ], "k" : [ { "title" : "k", "requires" : [ ] }, { "title" : "K", "requires" : [ "caps" ] }, { "title" : "K", "requires" : [ "shift" ] }, { "title" : "k", "requires" : [ "caps", "shift" ] } ], "l" : [ { "title" : "l", "requires" : [ ] }, { "title" : "L", "requires" : [ "caps" ] }, { "title" : "L", "requires" : [ "shift" ] }, { "title" : "l", "requires" : [ "caps", "shift" ] } ], "z" : [ { "title" : "z", "requires" : [ ] }, { "title" : "Z", "requires" : [ "caps" ] }, { "title" : "Z", "requires" : [ "shift" ] }, { "title" : "z", "requires" : [ "caps", "shift" ] } ], "x" : [ { "title" : "x", "requires" : [ ] }, { "title" : "X", "requires" : [ "caps" ] }, { "title" : "X", "requires" : [ "shift" ] }, { "title" : "x", "requires" : [ "caps", "shift" ] } ], "c" : [ { "title" : "c", "requires" : [ ] }, { "title" : "C", "requires" : [ "caps" ] }, { "title" : "C", "requires" : [ "shift" ] }, { "title" : "c", "requires" : [ "caps", "shift" ] } ], "v" : [ { "title" : "v", "requires" : [ ] }, { "title" : "V", "requires" : [ "caps" ] }, { "title" : "V", "requires" : [ "shift" ] }, { "title" : "v", "requires" : [ "caps", "shift" ] } ], "b" : [ { "title" : "b", "requires" : [ ] }, { "title" : "B", "requires" : [ "caps" ] }, { "title" : "B", "requires" : [ "shift" ] }, { "title" : "b", "requires" : [ "caps", "shift" ] } ], "n" : [ { "title" : "n", "requires" : [ ] }, { "title" : "N", "requires" : [ "caps" ] }, { "title" : "N", "requires" : [ "shift" ] }, { "title" : "n", "requires" : [ "caps", "shift" ] } ], "m" : [ { "title" : "m", "requires" : [ ] }, { "title" : "M", "requires" : [ "caps" ] }, { "title" : "M", "requires" : [ "shift" ] }, { "title" : "m", "requires" : [ "caps", "shift" ] } ] }, "layout" : [ [ "Esc", 0.7, "F1", "F2", "F3", "F4", 0.7, "F5", "F6", "F7", "F8", 0.7, "F9", "F10", "F11", "F12" ], [ 0.1 ], { "main" : { "alpha" : [ [ "`", "&", "é", "\"", "'", "(", "-", "è", "_", "ç", "à", ")", "=", "Back" ], [ "Tab", "a", "z", "e", "r", "t", "y", "u", "i", "o", "p", "^", "$", "\\" ], [ "Caps", "q", "s", "d", "f", "g", "h", "j", "k", "l", ";", "'", "Enter" ], [ "LShift", "w", "x", "c", "v", "b", "n", "m", ",", ".", "/", "RShift" ], [ "LCtrl", "Super", "LAlt", "Space", "RAlt", "Menu", "RCtrl" ] ], "movement" : [ [ "Ins", "Home", "PgUp" ], [ "Del", "End", "PgDn" ], [ 1 ], [ "Up" ], [ "Left", "Down", "Right" ] ] } } ], "keyWidths" : { "Back" : 2, "Tab" : 1.5, "\\" : 1.5, "Caps" : 1.85, "Enter" : 2.25, "LShift" : 2.1, "RShift" : 3.1, "LCtrl" : 1.6, "Super" : 1.6, "LAlt" : 1.6, "Space" : 6.1, "RAlt" : 1.6, "Menu" : 1.6, "RCtrl" : 1.6, "Ins" : 1.6, "Home" : 1.6, "PgUp" : 1.6, "Del" : 1.6, "End" : 1.6, "PgDn" : 1.6 } } guacamole-client-0.9.9/guacamole/src/main/webapp/layouts/en-us-qwerty.json0100664 0001751 0001751 00000033471 12612226251 030437 0ustar00mjumpermjumper0000000 0000000 { "language" : "en_US", "type" : "qwerty", "width" : 22, "keys" : { "Back" : 65288, "Tab" : 65289, "Enter" : 65293, "Esc" : 65307, "Home" : 65360, "PgUp" : 65365, "PgDn" : 65366, "End" : 65367, "Ins" : 65379, "F1" : 65470, "F2" : 65471, "F3" : 65472, "F4" : 65473, "F5" : 65474, "F6" : 65475, "F7" : 65476, "F8" : 65477, "F9" : 65478, "F10" : 65479, "F11" : 65480, "F12" : 65481, "Del" : 65535, "Space" : " ", "Left" : [{ "title" : "â†", "keysym" : 65361 }], "Up" : [{ "title" : "↑", "keysym" : 65362 }], "Right" : [{ "title" : "→", "keysym" : 65363 }], "Down" : [{ "title" : "↓", "keysym" : 65364 }], "Menu" : [{ "title" : "Menu", "keysym" : 65383 }], "LShift" : [{ "title" : "Shift", "modifier" : "shift", "keysym" : 65505 }], "RShift" : [{ "title" : "Shift", "modifier" : "shift", "keysym" : 65506 }], "LCtrl" : [{ "title" : "Ctrl", "modifier" : "control", "keysym" : 65507 }], "RCtrl" : [{ "title" : "Ctrl", "modifier" : "control", "keysym" : 65508 }], "Caps" : [{ "title" : "Caps", "modifier" : "caps", "keysym" : 65509 }], "LAlt" : [{ "title" : "Alt", "modifier" : "alt", "keysym" : 65513 }], "RAlt" : [{ "title" : "Alt", "modifier" : "alt", "keysym" : 65514 }], "Super" : [{ "title" : "Super", "modifier" : "super", "keysym" : 65515 }], "`" : [ { "title" : "`", "requires" : [ ] }, { "title" : "~", "requires" : [ "shift" ] } ], "1" : [ { "title" : "1", "requires" : [ ] }, { "title" : "!", "requires" : [ "shift" ] } ], "2" : [ { "title" : "2", "requires" : [ ] }, { "title" : "@", "requires" : [ "shift" ] } ], "3" : [ { "title" : "3", "requires" : [ ] }, { "title" : "#", "requires" : [ "shift" ] } ], "4" : [ { "title" : "4", "requires" : [ ] }, { "title" : "$", "requires" : [ "shift" ] } ], "5" : [ { "title" : "5", "requires" : [ ] }, { "title" : "%", "requires" : [ "shift" ] } ], "6" : [ { "title" : "6", "requires" : [ ] }, { "title" : "^", "requires" : [ "shift" ] } ], "7" : [ { "title" : "7", "requires" : [ ] }, { "title" : "&", "requires" : [ "shift" ] } ], "8" : [ { "title" : "8", "requires" : [ ] }, { "title" : "*", "requires" : [ "shift" ] } ], "9" : [ { "title" : "9", "requires" : [ ] }, { "title" : "(", "requires" : [ "shift" ] } ], "0" : [ { "title" : "0", "requires" : [ ] }, { "title" : ")", "requires" : [ "shift" ] } ], "-" : [ { "title" : "-", "requires" : [ ] }, { "title" : "_", "requires" : [ "shift" ] } ], "=" : [ { "title" : "=", "requires" : [ ] }, { "title" : "+", "requires" : [ "shift" ] } ], "," : [ { "title" : ",", "requires" : [ ] }, { "title" : "<", "requires" : [ "shift" ] } ], "." : [ { "title" : ".", "requires" : [ ] }, { "title" : ">", "requires" : [ "shift" ] } ], "/" : [ { "title" : "/", "requires" : [ ] }, { "title" : "?", "requires" : [ "shift" ] } ], "[" : [ { "title" : "[", "requires" : [ ] }, { "title" : "{", "requires" : [ "shift" ] } ], "]" : [ { "title" : "]", "requires" : [ ] }, { "title" : "}", "requires" : [ "shift" ] } ], "\\" : [ { "title" : "\\", "requires" : [ ] }, { "title" : "|", "requires" : [ "shift" ] } ], ";" : [ { "title" : ";", "requires" : [ ] }, { "title" : ":", "requires" : [ "shift" ] } ], "'" : [ { "title" : "'", "requires" : [ ] }, { "title" : "\"", "requires" : [ "shift" ] } ], "q" : [ { "title" : "q", "requires" : [ ] }, { "title" : "Q", "requires" : [ "caps" ] }, { "title" : "Q", "requires" : [ "shift" ] }, { "title" : "q", "requires" : [ "caps", "shift" ] } ], "w" : [ { "title" : "w", "requires" : [ ] }, { "title" : "W", "requires" : [ "caps" ] }, { "title" : "W", "requires" : [ "shift" ] }, { "title" : "w", "requires" : [ "caps", "shift" ] } ], "e" : [ { "title" : "e", "requires" : [ ] }, { "title" : "E", "requires" : [ "caps" ] }, { "title" : "E", "requires" : [ "shift" ] }, { "title" : "e", "requires" : [ "caps", "shift" ] } ], "r" : [ { "title" : "r", "requires" : [ ] }, { "title" : "R", "requires" : [ "caps" ] }, { "title" : "R", "requires" : [ "shift" ] }, { "title" : "r", "requires" : [ "caps", "shift" ] } ], "t" : [ { "title" : "t", "requires" : [ ] }, { "title" : "T", "requires" : [ "caps" ] }, { "title" : "T", "requires" : [ "shift" ] }, { "title" : "t", "requires" : [ "caps", "shift" ] } ], "y" : [ { "title" : "y", "requires" : [ ] }, { "title" : "Y", "requires" : [ "caps" ] }, { "title" : "Y", "requires" : [ "shift" ] }, { "title" : "y", "requires" : [ "caps", "shift" ] } ], "u" : [ { "title" : "u", "requires" : [ ] }, { "title" : "U", "requires" : [ "caps" ] }, { "title" : "U", "requires" : [ "shift" ] }, { "title" : "u", "requires" : [ "caps", "shift" ] } ], "i" : [ { "title" : "i", "requires" : [ ] }, { "title" : "I", "requires" : [ "caps" ] }, { "title" : "I", "requires" : [ "shift" ] }, { "title" : "i", "requires" : [ "caps", "shift" ] } ], "o" : [ { "title" : "o", "requires" : [ ] }, { "title" : "O", "requires" : [ "caps" ] }, { "title" : "O", "requires" : [ "shift" ] }, { "title" : "o", "requires" : [ "caps", "shift" ] } ], "p" : [ { "title" : "p", "requires" : [ ] }, { "title" : "P", "requires" : [ "caps" ] }, { "title" : "P", "requires" : [ "shift" ] }, { "title" : "p", "requires" : [ "caps", "shift" ] } ], "a" : [ { "title" : "a", "requires" : [ ] }, { "title" : "A", "requires" : [ "caps" ] }, { "title" : "A", "requires" : [ "shift" ] }, { "title" : "a", "requires" : [ "caps", "shift" ] } ], "s" : [ { "title" : "s", "requires" : [ ] }, { "title" : "S", "requires" : [ "caps" ] }, { "title" : "S", "requires" : [ "shift" ] }, { "title" : "s", "requires" : [ "caps", "shift" ] } ], "d" : [ { "title" : "d", "requires" : [ ] }, { "title" : "D", "requires" : [ "caps" ] }, { "title" : "D", "requires" : [ "shift" ] }, { "title" : "d", "requires" : [ "caps", "shift" ] } ], "f" : [ { "title" : "f", "requires" : [ ] }, { "title" : "F", "requires" : [ "caps" ] }, { "title" : "F", "requires" : [ "shift" ] }, { "title" : "f", "requires" : [ "caps", "shift" ] } ], "g" : [ { "title" : "g", "requires" : [ ] }, { "title" : "G", "requires" : [ "caps" ] }, { "title" : "G", "requires" : [ "shift" ] }, { "title" : "g", "requires" : [ "caps", "shift" ] } ], "h" : [ { "title" : "h", "requires" : [ ] }, { "title" : "H", "requires" : [ "caps" ] }, { "title" : "H", "requires" : [ "shift" ] }, { "title" : "h", "requires" : [ "caps", "shift" ] } ], "j" : [ { "title" : "j", "requires" : [ ] }, { "title" : "J", "requires" : [ "caps" ] }, { "title" : "J", "requires" : [ "shift" ] }, { "title" : "j", "requires" : [ "caps", "shift" ] } ], "k" : [ { "title" : "k", "requires" : [ ] }, { "title" : "K", "requires" : [ "caps" ] }, { "title" : "K", "requires" : [ "shift" ] }, { "title" : "k", "requires" : [ "caps", "shift" ] } ], "l" : [ { "title" : "l", "requires" : [ ] }, { "title" : "L", "requires" : [ "caps" ] }, { "title" : "L", "requires" : [ "shift" ] }, { "title" : "l", "requires" : [ "caps", "shift" ] } ], "z" : [ { "title" : "z", "requires" : [ ] }, { "title" : "Z", "requires" : [ "caps" ] }, { "title" : "Z", "requires" : [ "shift" ] }, { "title" : "z", "requires" : [ "caps", "shift" ] } ], "x" : [ { "title" : "x", "requires" : [ ] }, { "title" : "X", "requires" : [ "caps" ] }, { "title" : "X", "requires" : [ "shift" ] }, { "title" : "x", "requires" : [ "caps", "shift" ] } ], "c" : [ { "title" : "c", "requires" : [ ] }, { "title" : "C", "requires" : [ "caps" ] }, { "title" : "C", "requires" : [ "shift" ] }, { "title" : "c", "requires" : [ "caps", "shift" ] } ], "v" : [ { "title" : "v", "requires" : [ ] }, { "title" : "V", "requires" : [ "caps" ] }, { "title" : "V", "requires" : [ "shift" ] }, { "title" : "v", "requires" : [ "caps", "shift" ] } ], "b" : [ { "title" : "b", "requires" : [ ] }, { "title" : "B", "requires" : [ "caps" ] }, { "title" : "B", "requires" : [ "shift" ] }, { "title" : "b", "requires" : [ "caps", "shift" ] } ], "n" : [ { "title" : "n", "requires" : [ ] }, { "title" : "N", "requires" : [ "caps" ] }, { "title" : "N", "requires" : [ "shift" ] }, { "title" : "n", "requires" : [ "caps", "shift" ] } ], "m" : [ { "title" : "m", "requires" : [ ] }, { "title" : "M", "requires" : [ "caps" ] }, { "title" : "M", "requires" : [ "shift" ] }, { "title" : "m", "requires" : [ "caps", "shift" ] } ] }, "layout" : [ [ "Esc", 0.7, "F1", "F2", "F3", "F4", 0.7, "F5", "F6", "F7", "F8", 0.7, "F9", "F10", "F11", "F12" ], [ 0.1 ], { "main" : { "alpha" : [ [ "`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "Back" ], [ "Tab", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]", "\\" ], [ "Caps", "a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "'", "Enter" ], [ "LShift", "z", "x", "c", "v", "b", "n", "m", ",", ".", "/", "RShift" ], [ "LCtrl", "Super", "LAlt", "Space", "RAlt", "Menu", "RCtrl" ] ], "movement" : [ [ "Ins", "Home", "PgUp" ], [ "Del", "End", "PgDn" ], [ 1 ], [ "Up" ], [ "Left", "Down", "Right" ] ] } } ], "keyWidths" : { "Back" : 2, "Tab" : 1.5, "\\" : 1.5, "Caps" : 1.85, "Enter" : 2.25, "LShift" : 2.1, "RShift" : 3.1, "LCtrl" : 1.6, "Super" : 1.6, "LAlt" : 1.6, "Space" : 6.1, "RAlt" : 1.6, "Menu" : 1.6, "RCtrl" : 1.6, "Ins" : 1.6, "Home" : 1.6, "PgUp" : 1.6, "Del" : 1.6, "End" : 1.6, "PgDn" : 1.6 } } guacamole-client-0.9.9/guacamole/src/main/webapp/layouts/de-de-qwertz.json0100664 0001751 0001751 00000037435 12612226251 030373 0ustar00mjumpermjumper0000000 0000000 { "language" : "de_DE", "type" : "qwertz", "width" : 23, "keys" : { "Esc" : 65307, "F1" : 65470, "F2" : 65471, "F3" : 65472, "F4" : 65473, "F5" : 65474, "F6" : 65475, "F7" : 65476, "F8" : 65477, "F9" : 65478, "F10" : 65479, "F11" : 65480, "F12" : 65481, "Space" : " ", "Back" : [{ "title" : "⟵", "keysym" : 65288 }], "Tab" : [{ "title" : "Tab ↹", "keysym" : 65289 }], "Enter" : [{ "title" : "↵", "keysym" : 65293 }], "Home" : [{ "title" : "Pos 1", "keysym" : 65360 }], "PgUp" : [{ "title" : "Bild ↑", "keysym" : 65365 }], "PgDn" : [{ "title" : "Bild ↓", "keysym" : 65366 }], "End" : [{ "title" : "Ende", "keysym" : 65367 }], "Ins" : [{ "title" : "Einfg", "keysym" : 65379 }], "Del" : [{ "title" : "Entf", "keysym" : 65535 }], "Left" : [{ "title" : "â†", "keysym" : 65361 }], "Up" : [{ "title" : "↑", "keysym" : 65362 }], "Right" : [{ "title" : "→", "keysym" : 65363 }], "Down" : [{ "title" : "↓", "keysym" : 65364 }], "Menu" : [{ "title" : "Menu", "modifier" : "super", "keysym" : 65383 }], "LShift" : [{ "title" : "Shift", "modifier" : "shift", "keysym" : 65505 }], "RShift" : [{ "title" : "Shift", "modifier" : "shift", "keysym" : 65506 }], "LCtrl" : [{ "title" : "Strg", "modifier" : "control", "keysym" : 65507 }], "RCtrl" : [{ "title" : "Strg", "modifier" : "control", "keysym" : 65508 }], "Caps" : [{ "title" : "Caps", "modifier" : "caps", "keysym" : 65509 }], "LAlt" : [{ "title" : "Alt", "modifier" : "alt", "keysym" : 65513 }], "AltGr" : [{ "title" : "AltGr", "modifier" : "alt-gr", "keysym" : 65027 }], "Super" : [{ "title" : "Super", "modifier" : "super", "keysym" : 65515 }], "^" : [ { "title" : "^", "requires" : [ ] }, { "title" : "°", "requires" : [ "shift" ] } ], "1" : [ { "title" : "1", "requires" : [ ] }, { "title" : "!", "requires" : [ "shift" ] } ], "2" : [ { "title" : "2", "requires" : [ ] }, { "title" : "\"", "requires" : [ "shift" ] }, { "title" : "²", "requires" : [ "alt-gr" ] } ], "3" : [ { "title" : "3", "requires" : [ ] }, { "title" : "§", "requires" : [ "shift" ] }, { "title" : "³", "requires" : [ "alt-gr" ] } ], "4" : [ { "title" : "4", "requires" : [ ] }, { "title" : "$", "requires" : [ "shift" ] } ], "5" : [ { "title" : "5", "requires" : [ ] }, { "title" : "%", "requires" : [ "shift" ] } ], "6" : [ { "title" : "6", "requires" : [ ] }, { "title" : "&", "requires" : [ "shift" ] } ], "7" : [ { "title" : "7", "requires" : [ ] }, { "title" : "/", "requires" : [ "shift" ] }, { "title" : "{", "requires" : [ "alt-gr" ] } ], "8" : [ { "title" : "8", "requires" : [ ] }, { "title" : "(", "requires" : [ "shift" ] }, { "title" : "[", "requires" : [ "alt-gr" ] } ], "9" : [ { "title" : "9", "requires" : [ ] }, { "title" : ")", "requires" : [ "shift" ] }, { "title" : "]", "requires" : [ "alt-gr" ] } ], "0" : [ { "title" : "0", "requires" : [ ] }, { "title" : "=", "requires" : [ "shift" ] }, { "title" : "}", "requires" : [ "alt-gr" ] } ], "ß" : [ { "title" : "ß", "requires" : [ ] }, { "title" : "?", "requires" : [ "shift" ] }, { "title" : "\\", "requires" : [ "alt-gr" ] } ], "´" : [ { "title" : "´", "requires" : [ ] }, { "title" : "`", "requires" : [ "shift" ] } ], "+" : [ { "title" : "+", "requires" : [ ] }, { "title" : "*", "requires" : [ "shift" ] }, { "title" : "~", "requires" : [ "alt-gr" ] } ], "#" : [ { "title" : "#", "requires" : [ ] }, { "title" : "'", "requires" : [ "shift" ] } ], "<" : [ { "title" : "<", "requires" : [ ] }, { "title" : ">", "requires" : [ "shift" ] }, { "title" : "|", "requires" : [ "alt-gr" ] } ], "," : [ { "title" : ",", "requires" : [ ] }, { "title" : ";", "requires" : [ "shift" ] } ], "." : [ { "title" : ".", "requires" : [ ] }, { "title" : ":", "requires" : [ "shift" ] } ], "-" : [ { "title" : "-", "requires" : [ ] }, { "title" : "_", "requires" : [ "shift" ] } ], "q" : [ { "title" : "q", "requires" : [ ] }, { "title" : "Q", "requires" : [ "caps" ] }, { "title" : "Q", "requires" : [ "shift" ] }, { "title" : "q", "requires" : [ "caps", "shift" ] }, { "title" : "@", "requires" : [ "alt-gr" ] } ], "w" : [ { "title" : "w", "requires" : [ ] }, { "title" : "W", "requires" : [ "caps" ] }, { "title" : "W", "requires" : [ "shift" ] }, { "title" : "w", "requires" : [ "caps", "shift" ] } ], "e" : [ { "title" : "e", "requires" : [ ] }, { "title" : "E", "requires" : [ "caps" ] }, { "title" : "E", "requires" : [ "shift" ] }, { "title" : "e", "requires" : [ "caps", "shift" ] }, { "title" : "€", "requires" : [ "alt-gr" ] } ], "r" : [ { "title" : "r", "requires" : [ ] }, { "title" : "R", "requires" : [ "caps" ] }, { "title" : "R", "requires" : [ "shift" ] }, { "title" : "r", "requires" : [ "caps", "shift" ] } ], "t" : [ { "title" : "t", "requires" : [ ] }, { "title" : "T", "requires" : [ "caps" ] }, { "title" : "T", "requires" : [ "shift" ] }, { "title" : "t", "requires" : [ "caps", "shift" ] } ], "z" : [ { "title" : "z", "requires" : [ ] }, { "title" : "Z", "requires" : [ "caps" ] }, { "title" : "Z", "requires" : [ "shift" ] }, { "title" : "z", "requires" : [ "caps", "shift" ] } ], "u" : [ { "title" : "u", "requires" : [ ] }, { "title" : "U", "requires" : [ "caps" ] }, { "title" : "U", "requires" : [ "shift" ] }, { "title" : "u", "requires" : [ "caps", "shift" ] } ], "i" : [ { "title" : "i", "requires" : [ ] }, { "title" : "I", "requires" : [ "caps" ] }, { "title" : "I", "requires" : [ "shift" ] }, { "title" : "i", "requires" : [ "caps", "shift" ] } ], "o" : [ { "title" : "o", "requires" : [ ] }, { "title" : "O", "requires" : [ "caps" ] }, { "title" : "O", "requires" : [ "shift" ] }, { "title" : "o", "requires" : [ "caps", "shift" ] } ], "p" : [ { "title" : "p", "requires" : [ ] }, { "title" : "P", "requires" : [ "caps" ] }, { "title" : "P", "requires" : [ "shift" ] }, { "title" : "p", "requires" : [ "caps", "shift" ] } ], "ü" : [ { "title" : "ü", "requires" : [ ] }, { "title" : "Ü", "requires" : [ "caps" ] }, { "title" : "Ü", "requires" : [ "shift" ] }, { "title" : "ü", "requires" : [ "caps", "shift" ] } ], "a" : [ { "title" : "a", "requires" : [ ] }, { "title" : "A", "requires" : [ "caps" ] }, { "title" : "A", "requires" : [ "shift" ] }, { "title" : "a", "requires" : [ "caps", "shift" ] } ], "s" : [ { "title" : "s", "requires" : [ ] }, { "title" : "S", "requires" : [ "caps" ] }, { "title" : "S", "requires" : [ "shift" ] }, { "title" : "s", "requires" : [ "caps", "shift" ] } ], "d" : [ { "title" : "d", "requires" : [ ] }, { "title" : "D", "requires" : [ "caps" ] }, { "title" : "D", "requires" : [ "shift" ] }, { "title" : "d", "requires" : [ "caps", "shift" ] } ], "f" : [ { "title" : "f", "requires" : [ ] }, { "title" : "F", "requires" : [ "caps" ] }, { "title" : "F", "requires" : [ "shift" ] }, { "title" : "f", "requires" : [ "caps", "shift" ] } ], "g" : [ { "title" : "g", "requires" : [ ] }, { "title" : "G", "requires" : [ "caps" ] }, { "title" : "G", "requires" : [ "shift" ] }, { "title" : "g", "requires" : [ "caps", "shift" ] } ], "h" : [ { "title" : "h", "requires" : [ ] }, { "title" : "H", "requires" : [ "caps" ] }, { "title" : "H", "requires" : [ "shift" ] }, { "title" : "h", "requires" : [ "caps", "shift" ] } ], "j" : [ { "title" : "j", "requires" : [ ] }, { "title" : "J", "requires" : [ "caps" ] }, { "title" : "J", "requires" : [ "shift" ] }, { "title" : "j", "requires" : [ "caps", "shift" ] } ], "k" : [ { "title" : "k", "requires" : [ ] }, { "title" : "K", "requires" : [ "caps" ] }, { "title" : "K", "requires" : [ "shift" ] }, { "title" : "k", "requires" : [ "caps", "shift" ] } ], "l" : [ { "title" : "l", "requires" : [ ] }, { "title" : "L", "requires" : [ "caps" ] }, { "title" : "L", "requires" : [ "shift" ] }, { "title" : "l", "requires" : [ "caps", "shift" ] } ], "ö" : [ { "title" : "ö", "requires" : [ ] }, { "title" : "Ö", "requires" : [ "caps" ] }, { "title" : "Ö", "requires" : [ "shift" ] }, { "title" : "ö", "requires" : [ "caps", "shift" ] } ], "ä" : [ { "title" : "ä", "requires" : [ ] }, { "title" : "Ä", "requires" : [ "caps" ] }, { "title" : "Ä", "requires" : [ "shift" ] }, { "title" : "ä", "requires" : [ "caps", "shift" ] } ], "y" : [ { "title" : "y", "requires" : [ ] }, { "title" : "Y", "requires" : [ "caps" ] }, { "title" : "Y", "requires" : [ "shift" ] }, { "title" : "y", "requires" : [ "caps", "shift" ] } ], "x" : [ { "title" : "x", "requires" : [ ] }, { "title" : "X", "requires" : [ "caps" ] }, { "title" : "X", "requires" : [ "shift" ] }, { "title" : "x", "requires" : [ "caps", "shift" ] } ], "c" : [ { "title" : "c", "requires" : [ ] }, { "title" : "C", "requires" : [ "caps" ] }, { "title" : "C", "requires" : [ "shift" ] }, { "title" : "c", "requires" : [ "caps", "shift" ] } ], "v" : [ { "title" : "v", "requires" : [ ] }, { "title" : "V", "requires" : [ "caps" ] }, { "title" : "V", "requires" : [ "shift" ] }, { "title" : "v", "requires" : [ "caps", "shift" ] } ], "b" : [ { "title" : "b", "requires" : [ ] }, { "title" : "B", "requires" : [ "caps" ] }, { "title" : "B", "requires" : [ "shift" ] }, { "title" : "b", "requires" : [ "caps", "shift" ] } ], "n" : [ { "title" : "n", "requires" : [ ] }, { "title" : "N", "requires" : [ "caps" ] }, { "title" : "N", "requires" : [ "shift" ] }, { "title" : "n", "requires" : [ "caps", "shift" ] } ], "m" : [ { "title" : "m", "requires" : [ ] }, { "title" : "M", "requires" : [ "caps" ] }, { "title" : "M", "requires" : [ "shift" ] }, { "title" : "m", "requires" : [ "caps", "shift" ] }, { "title" : "µ", "requires" : [ "alt-gr" ] } ] }, "layout" : [ [ "Esc", 0.7, "F1", "F2", "F3", "F4", 0.7, "F5", "F6", "F7", "F8", 0.7, "F9", "F10", "F11", "F12" ], [ 0.1 ], { "main" : { "alpha" : [ [ "^", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "ß", "´", "Back" ], [ "Tab", "q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "ü", "+", 1, 0.6 ], [ "Caps", "a", "s", "d", "f", "g", "h", "j", "k", "l", "ö", "ä", "#", "Enter" ], [ "LShift", "<", "y", "x", "c", "v", "b", "n", "m", ",", ".", "-", "RShift" ], [ "LCtrl", "Super", "LAlt", "Space", "AltGr", "Menu", "RCtrl" ] ], "movement" : [ [ "Ins", "Home", "PgUp" ], [ "Del", "End", "PgDn" ], [ 1 ], [ "Up" ], [ "Left", "Down", "Right" ] ] } } ], "keyWidths" : { "Back" : 2, "Tab" : 1.5, "\\" : 1.5, "Caps" : 1.75, "Enter" : 1.25, "LShift" : 2, "RShift" : 2.1, "LCtrl" : 1.6, "Super" : 1.6, "LAlt" : 1.6, "Space" : 6.1, "AltGr" : 1.6, "Menu" : 1.6, "RCtrl" : 1.6, "Ins" : 1.6, "Home" : 1.6, "PgUp" : 1.6, "Del" : 1.6, "End" : 1.6, "PgDn" : 1.6 } } guacamole-client-0.9.9/guacamole/src/main/webapp/fonts/carlito/Carlito-Italic.woff0100664 0001751 0001751 00001051554 12612226251 031740 0ustar00mjumpermjumper0000000 0000000 wOFFSl ƒ8GDEF`[‚™ž™9GPOS¼3 òÇx™GSUBDȤ d«)LOS/2ü^`jæÁcmapûê’Ù‰cvt .¬,8%ôÖfpgm) uÛ¶.ŒgaspXglyfJÄÀNÁþØœhead€66}/pÝhhea¸#$>»hmtx\¹+v—ãloca.Øì+|,cxmaxpÜ ž Ename ) øÝ=¶post@ ÿ•Âprep.(šx:8^óþGü_<õJSÛ&Î.0Sü˜ýìäxÚc`d``cøÇÀx„kÁŸ?ì8žp00üf@\w¥§ Þå‹P]nå uxÚ%‰± @Pï½ÿQ‰D°†Â0 ¨µ–1€N­7ƒï‹ârÉÎ2c`‹µ åow0) ¹©ªwê/4ôÜ!Æ,Â0öOHx=]µ©í{î´:ª‚Ë "xÚÍštUÅÖÇÿSι iúh¡‰$t¤AzM衤Lè-@”FŠ¢(HEAÄõxAÁb)Aº(j̼ÿœ{/$y ú-ý¾ï®õ[{Μ9sÏ™²gï™­CP0;Än@/1Û,nIó†î…^º:º{œ%÷«ïÐËc5º9ù«3ÑÃi‚~Rc)(Ÿ2—u8º«2ÅTй0Ø GG#Åi„^N3Tv/ ‡.Ad™…AêEäóÈÀ‹S©=#}e0J‡™÷½{iðéPÜcqó¢Iô代³1 rÈ÷Ê!yYo3ï:ÂË?ŽD~K¢5'Õ6Dðý¢Hy]÷Šýè%ö›«Vê’H§M™¤–¡i#~EkY ÑnWtà3­É`–kÀº ð[ªóº ·í"ßCkŠüg9ºª‰‹¨&ß@/¹ õå&>“¡:ÅSÃ|í0ÌýÂëéiQ³ÐÚÙˆöjŒùLÍFGU J%‹Bª/N¨±8("K¦ã¢lŠÝL_”KðØ‚=L–‡pE$à1ñr.‰XlÕíE-ù%.Ëýø^À7䢸{eM¤‰cØÇ{F>Žcê‚('Å™Š#²6È®â9[@vwPæã{ìV³EKÊó”}…·«¢´Z!¢TTýEC†sjè©òãUHT–³ð|IDÊÝ¢º\/bÔ1LCl;ùnŸŠ²2åöÃt–÷ˆÕ}õ“Î1©ç˜3êÛ7©×q—[£\`1Ço1ý8ÇpÚé†Ïöj¬k"N-G’ü]8îRôX<®›¢“¬…gÔÛÈ£;¢¨š‹D%Ù¾ ½õν¨Å2}TôSÅÙ¿Ý0Õ¹ñN=Ö5Ðìu^ÃdçY –ÓÄXuIÄñ{…0|¶R8תÑä„™Ãîó£ú58wÆq^ ×Ç฿˜C>…»íüÓ?㎅éú<éÌæ+…‡ôe–MÆF÷εÒìû3˜ác¹&xX'à^Ý£}y9NFšŸ©žÔ?ðYgu3Lä·ÆªtŒä{á8ˆ×uÍ~·-Îý2ÞøÙ€þúsÞæ¼3#u} uÃ1B—aÙÊÌñÜ–sr¸³ u+$ñ™j3ËÔÃp·/ÿ³-¢œ®Ìû¿sªª§àlÃõ1Û±Úë0ôWÃDm2€ÄÈ…xG­Çeõ ¾eû$È ˜(êŸÇQ…×?’oUO᪞hÆô=VÊ ØH^ÓÅñùD•0+ÔŽ›L1AAŒ…x•‰ç|±œ²9yu>!‡â%RF>„ޤ¬œÂ>Ÿ‚Š|.œef‘†¤^‹$ÊED‹m8®ê 8)¸Ï&ò¹«±šdé缌3Î*Î Ʋ£ô[¨ CP‹éúd¹Meš,¾ãæ?åæÇãyªaó+éÙœ×c‚:‹’ä>ŽŸÚêJèz|n£œLVð¹(’Fš’6¤$¹Ÿ "óI2õc}+XW8B½ÍúY×'¬ç(ËÄ’c¤ ¨£3ÙD>WåÐG\ÅÝr- ËÕ¨Éy]Y¾‚Úò0õØ,®ߢÓ©ò4zS–{X~&ŠMæ¬\„â2ªÊβ î”mÍo²%ÓPšº †ìb²çÓ›Y[œheaÜGf“zt²‹ü3ŸL@:۟ÜSè¨Tr÷qÎ6ãºpÎ<ÂüÖn"^Ñ]Íy}'¦°Ïßö#®Å¾¡Éd’JÚÜÖQ6rˆP1ËÅ!ü “Ät²F'áu’~ýúšœG> È¥¹®­|Æ™‚1Îv¼ 'sŽü ãYOiò°Þ‚5NL 0L§rûI±k‡üßÊÈP)ÈñxEÝ…Ÿäox‘ëÁ¹›€¬'u’LäW[pŠœ¡n$¢$e&uJ%RD®Äý¤ª\)ŠSV$]H ìÏ Ä>ìÆz–É•æ8ù…c^ ÊK,—E]üµZ }”¡Z˜¯´¥Áëì >åÌϵc%&“yê"â¨ù,F«71—eÂ|}p[€Û-î|D¹„ˆÃxGœD®b½ô!MæÅv±ké­*/ÒE6ˆÎX/¶’4êå0l'§iëÄ«‹$¾Ã!û¿z;çY¶auÔ#Üh^—ÇdÚ9ô.ÊÐM­6Mi-2@Fa%çB÷uŒsÂMC«SÝœ3©G£YþE¶‹æ‘A…G¨‡0=vK9ÖFb8Bm}¾pŒÕŸÓ–*Ìõ!„6Sʲ|3ÞKa]÷ñ¹ž ÍûqHPG©FÓ†YͺÖó?C9/C©ãë )íŸX¦c©_#M÷.êÚhAÛ!‘kN}žïÊyB{‰6À0 Û:‰ëgYµyU_sU @)êâhÚ'ëÔ&ó‰~…¶Sç=ËÈ8Ú ¯¡¿œ†nbݹV…R/Ùçg{¶Bu}•ŒùVÿHƒ†z.êÖªqhŽòê}êãó¨Ãõm(×¶8}ßu»ê†öUОJÁÚ$C¸ÎLò¾Ñ~3á7WP ÑÏË«Ã9½-½÷·s|9ú°_ËëŠ\ÃCÑR÷ÄÃj¿w·.'ÖÆ&O¢ m®áœCã¹MÌ6hïÙXíÙ¯\«œ thÊ÷8¾¯¦¤·œ‰å$–k’ZÅþ E)Ún³ÄiÄÉéf"Ûº¹moÖW–ý÷°nƒçÔ,sÅÙh®¨Ùæ¼êDÅvÔ`¿n2Ÿ’_Ù¦ÓÈ "¹–6ç8{ˆö^û¬sûµ0Ûq¬mw·z»Ÿ"ÞWß|Žýº›ëé8¾ï%¶ÍxNÄDÚ.Y6ŸEíÇíÎíæÚ§q´;Z²û±ý’Y÷ƒê~¶kj±MêÊ0®%³Øgߣÿ{°µ±9ÆèM›~<íÎ>3]x/‰DsMäø®I^e=Ñ&š¥9žw û ÈËüT73U$×´ÔÓm8·öÀ¡Ýd߉öÀp§× ;¨c.c:çç\öÏWd£j‡ÔmGÉNŽ¥ ª7íÌWpD|Ìõý¶ÉÞØ§‡ŠòÔ}—dÈuÔùÌA¤sÍØªŠr½jKríÔUø’vT¨Üˆór ~ ÇE?¬æ:•%;R¯d%3Éëcº»ˆmñ<ûú3®C«åd¾C"qNÌ'ØïL»‘SÏ¢„šŒÒj›9ª4õË6úCCQ—Ô‘µÍ÷ª¶¹Àò?³þý|·oøŸßÐV~Zž!âYì+‘ÁwØÎôÇâ³ñ¥xïÒ†9L»ù^R‡ï”dßMiá0}UÕÃ.ÚÎëic‡Ó~þRž¢½¼‘m•! â´¥ÏÉÙ8˵wƒLÆ*æ ÚåY´¹—IÅ´I¶ª:ø„º?”z8/uã%9ŸëÀ|QT·w’8ÙIô”œÿ¬Ç¯·_#ÏX*ûâ U~\A4#åTAs™|Ê{y=– cüuÚTê¯dŽçy:£hƒ¦è¥xžº7Õ³ŸS8Sè·Ñ^ÖЙc,•c´—.lV{¶ðXúMô{Ô|®Ù›hç,¥^lcu×ëhËoIõlß&Ô#h“4Å÷eóí™~Ô£ýÉê‡éNQ qÞD_=ú¥Åi¿úýÎéz„Yä¼M9 oÑ&~‡ŒvÞbYÂg8!&Óú$ÑÚÌ>Ǹã7ñlqû¿¬‡¶iA®%QnStwr݈ÀR÷8ŠøJâ)êø‡ù³ìúâ„@{¶òh˹PQ-Æ@φ¾„ºÔ©±¤nÎöˆå>ˆÞômÐVŽ£9‘:m¸Ç:s˜yÕ ÎÓÍüžÍ,û*šY¨ÔåÌvk{ëÆô/6ó;h‹[]iõÕoû½"ëɤ¯—éVG¢³sÕ{xÚ-Aû½5ýž´G&±éÛè*lÓó^¿%“‘ìÓü\+òQ/tðu ŸZ)ôu{;—ù\8ºØõ0Ï s‰}\@?Fiû}õ÷jê>ï–ÃQg,Î;‹h;db’Ox6ô ÝYŒÒM²†‰¡¯6—>k%ý’ɺ˱Ÿè×Äë=NmâH±ºœkàp]ÌœUQ´{ó™ ƒUE ñ]}C¨Ãr æ3î«ìçQÐw?׫/O=´dŽ´eø~)Nf:àw×6§œ5lÛ‰´Iòq<&˜«¾(Ä1ÙÓ}€ö7×aš9UèƒþL®ZhsÌ$©Ìïg±¾ÿm/‰i5ÔÙÑ샇Hÿïýö>ô‡^¥ >›6ø:Úã©Î?P݉CU§?ÇÅzÄäéˆÞöýøÌ ÙýœÜ>‘õw´‹Óê³ÞÃ|~ÞÍ| k›[ÿÆ©J¿Öï{Œf_< vÐyÕ>êûöxQßÎ1°öö އå´mÒp·õA8FÇRÖ¤,ɼÊ$‘ãµ6õk)®j!õË|Î'hs¬¥ÎO›kû9ýûû3ý­Í\óÓh_¦qn¥á%æ¿Å¼{(Ÿãõó”q¶<õ¸ÜCZFÂ1—… !£QNÆ¢ óòÓžn ãè‡ÄÚrÆÒxÏ‘ˆ‘æ0õúyšëÆaQˆþ -iæa—…¹Žì¦ÞE_缜EÝý-ޱ\U>3—ü“Ì 1dJbÉr2@l¢]¾ ‹ä$sE„›µd«L¡N„RÄGîà½#”Å(¿'G™®DùŸƒ #’9|ŸTÒJ¶â|¿±Ÿ•ð³V\ó³üï—D’I 2–t#-É“$JtÁ*Ò„þFúÎèà|…ºõžÈ×fà7š;¿…ÓNšÃq{ŸlŽ®”‘"‹6›µåì^µáê°_£MÓžvUiú¸÷"^î@$i)?`ß|€jLw ³H]2€ãy±€ch×µ¢*ZTäøl}ñ…ÈG Éib8©G¥¡ª$ž%TO>“h#”ñ¨€Cö9&|ô—î ôz[? J¹†PŠn¼Ž¢¤Ç%ææÑ÷xŽ,‘]EGÒZAgßQLõÍ£íg¿g¥È¯{á!ÿ{gýh÷c¹îÇ‹€oXþ¡Ó9îÒ1ÝÂôÊe@f_ùqöO>ʇm;zm´@´f»¸ê“¢^¾ÿºŽìŽØf·‘*lçh'JVd;%²L[Þ[Îïú†ß7ˆÕUgùâ~¬Ž ¦ÅÇ„· U­™Áç"ì³zèÀúz‘ÖAŸ‘¾fWÊ‹l·b”IäAÝÄübQÅÙOÕH2ëûï-ñwù cgƒØ=Ãëœ âù€½[ÚŸYW¼=X»FŽ÷ö8Æ¥¤2ßo_´½êOë¿LF¸Ýëµû¼Îm\Û‹@q-éQé.¸ÃyÎRÔq>A g<Š1]Ö©Œ2\k:ÈZh«bPC½o^æzR…㥻슑l»šœ“ ¼}øPúO PE/暷س ®x¶CÊx>`¾±c^ešiúeÑ\eŠJô9öqM«f÷ò{å{ûõ8Æ–©-bµµù¬í§zš,ù9^r—Ðox‘ó.ëä%³QîÆ»÷Kûô°ø Çig¦Û}Úk61maú­‚õ°¯ŸóÃqÜïÛq!·ÓÖ߯”¡¤"nñcß§°|•­ìÞ…Åî7ðÛÛ=ŒÀþD–õõu¦Ùc÷½ù=ý‚çÿlü;ë·ç2A¼óóÓõt¶3›˜¥9Îrr‘gÈS‚믡ݚýœç÷Á,wL›äï×kÍd-Óg(ß%˜þšìÉ~öt3ì™TdKç¤;;Í—Îf§O¿!Óœv¶™OÉîÀõ×d÷µ²ÛÌ×ä†eg_·¢ä òâ2< ;ù¥9'w™“ä9.ó$Ó{É>ÿµù‰d䮾g¤= bÏán„w6wÜRèá´Â<{f—ï ïà[ªÔ³Oxg}·&ÿ,÷'0G\m>&Û™¾Dù.ùéSäwâ?{¼Nmú_ׯ—ÙóÈ[qí¼òVÎ3sb:æÎ žwþÕØóÓàê°çªÙ¹Éû½q¾wï §°I$ýôf ¯cÈàeÒàËŽÕ7Ù¯ƒç¹?œ×ÚýÍëçÇAì9rv‚gÊ91 ƒiïl9;PwRúÄÝÁsæ¿sìf÷ìY¶ª†6³ëú9ΰo±¯šcÕÚ-¡æäͰ>kv¼=Uⴆɕ•®O´ÛSäµgÝôÁFøÏ»Ñš4 ¤ƒT$÷Ò-rÉ`:øLžkõøÏ˃LÊvvž“ HõÎÓé+“š²)ý«ýÞuy±Ÿ¾Õ~3‚ymäIÚ,›1Э‡¶öÌýã™wý þ÷i‰¾® Þ9ýÿ7¡@ fà¿ðì(?²¥û1¿c nÉ8D\‹9øoø¥iŒAø³cþ,Á؆¿”q˜jc%‚ؘ‰›áÅRÙbÎxqÿSlíñ)&Ý‹™°±8 ?&Ë-bNº…ÌIçm`^Q^äõ÷¼ÎÏë<,så”õâ.þ°û·Â‹óø›±ñ#ä€l¿‡-«Hx¶¼¶¤.‰´q(^܉¥¦þU8õŒWù;Ñl÷ÛmÌKö¸—ìØÏæ³1/Ù±ñ.6Æ%ëQ ~Åî¹cWT‚¹ŒañhàŸõ Æ´cYl¬ŠŸ2Umì ýÏÁ6ÎD?%—bMfˆñý߾Ƣ‹ÅCbfПm ˜—-^< qËQâ¼8€ÜlÇ ¹ñârcãrô¡ƒqt±kY¬/Íwx3ˆ]«i[ØsÑž\+ž Þ·ß>•:w&õç{¼7‰åèã›|„õ˜96~@‡Šrj¯(ÄçVè’¢,ŸÛoótЍí~!ֱܘ XƒJüÿkØØÊ¢~²N“ÖÇgùm×ÉúŒd‘3ö1¿²ç#Ó¦cºa÷ xïélØý§™d/Yl÷ Xîµë  ±{\¡¼îäç·¯(KÛ˜J»ÕÆ.Pþj±1 ”Ô ÍXwË C9€Ú~L9æ§Ù¸¦÷úA~ßdγØÊe~PÈÆ;P¦[l܃ͳq|öC‹ÿ³Ëã*Öó~„ÅÆ@0o“ÅÆEP¾o±ñ”öÝ¿ÍUf+¥½N³qÁo·{*¬ÏÄÆRP°Ø˜ –ùÜbc+(YlŒ¥ýÞÚX‹oA9ÕÆ\Pþè'« "LoB¬w–Oì¹9Û5,pþí?_‰ö<ØžÏou|£sßà9'¿á.‹w¾ÙÝL·g’ô_z‘hú0ÏÓI&ý¼³J3ÁžÚs>ïŒÏž›Ù8ƒ2ˆçw»¯Ëo ±Øý]Ömû¾‚Ýç¥, ˆIöÉ;Cj&8_þes>ŸxÚÕXe”U7×N²“½³3÷áÞfðÅÝÝÝÝ­¸»»w)--VÜŠ»û îîÐÂ{:å­¼ëýÖú¾Ÿ_îÊÉNr’›'9y²Ÿ!@DG.¤ø=$ór2:ÊF—|þRó_&ÿ› dVYOvÔßëî²¾¬++ÈŠ²š\-sÈR2—,&ÇÉñr‚œ(¿•“t9Yw•S0«Ì)7Éõr³Ì&xcBA‚E $‚…OÄqE<_$‰ER‘\D¨á"¥H-Òˆ´"H/+É’²²¬-ËŠ%â+‘I|-rˆ<"Ÿ¨'ˆÆ¢¹h©;‹¶âÑ^t]DWÑMôÃÄ1RŒ£ÅYE—Ue!dÙV”¹euYC¬Ð¼’B"äßàd™Wæ“ùe9YZ–kd¯¬°èø{ B÷­õbÙ\¶“%dM±N6ce¹Ý«$6Èfr‹î©{‰eÜ_Ö’åÅÝ[Œ“EuÝW÷Óý1fǘsa^a„Å¡8 ‡ãHoîOâ)<ƒƒ)¦÷? )‘¼MIäZ‘ŒSjJÍåo½h-¤ïh1}O?ŠŒ"3¥stž. aSØp›Rä²=»¶­#ïÚzrè`·Ùív‡Ý)ïÉûö–·ö:î%¯§wb€·=<+HôËÔ&u@½T¯Õg0âCD@:ÈY ;€"PÚCWè£` ,€cº«f⡚!‡’ÏJ>7ù{|ˆ¿˜¿œ¿†¿–¿Ž¿ž¿¯­›¯ÿ´ÿ²ÿ©ÿ•ÿSÀ ¤ d d ää 4t L¬ l \ 5¡qC†BÃBS†fÍZ>´AèС?†©0 ó…Å ‹–$Ì–&,]XɰFaÍR¨±R„†w!"TD̈Xñ"E,ˆXq$âxÄ”"eÞ”­RõOß6}÷ 'YúîóGý)ŧϟ?G/u÷-+ÔuØCþÖCq!‘‡<‡<³‡<—‡¼8Ô‚.Ð †xÈÇÁwpBw×#<ä"„Cj‡|òÏ÷ "À_Ò_ñ òþþõþþ}þsþ«þþ7ˆã!ÏÈÈÈë!¯èè˜Øøy‚¿!/Z%tpè„?‘ÇŽFžü ò†aM£‘†?ÿüäK"ý y·ô Ówñ'\ð‹Ohäcüµ•ÁûÚðí6ù<û‰·½£¼4òoïLúË~þoënú¿S½Þ/ë§‹ëñþ´ÚüUz³QTë¨åBD*t÷uÔÄëyþ¬Ù¹ón¬ÈõBܹÄkõ1rzd‡èöá× ½z‘i"‘!‘I"FF5rmäÜÈD×._KtõÍïùóÑ#™0š×F}á¼Wr¥Üg®ï ,êÅÊXë{©×;vúûà\<„GðŒg_#Mµí"»[>Ç·Ü ½>g• ê4 èPЙ?Ft%8Qð€àqÁÓƒWo Ž ¾üÊG_&_f_V_vÏÊéËíËëëãûá¾…Í„o¦o¶o­oýŸùÑÏí_r›}[ÿH£ícŸYß^ß¾ä·ûÎyÏK>o}Q¾{ÿÿÙúbÛÿ׿üO®Åp­p“üε”øŸ\KC…ÿO®]ôŸ\kþpmn[ÑÖúkmÝr­½!:Û[˜›~À<´Xh‘»Ð>ìJû±;Äntûˆœ8XäÂQt GÓeCWDL‘GÐ\nWX?[ƒ+-âZÑPÄð´À³6ž³©EÑÏ‹¦øÂ†àK› ߨ¢þjóão¶€H(Ú˜g&žH Z“³å)ÈV¢¶ŸID;JjëS2Û€BlCïÜëHÅìP*a‡Sq;ŒÊÛ±TÁŽ)DwªhÇS];“jо"Bô£fæ9µ°»¨¥ÝM­ìjm÷ŠTb8õ±h˜½IÃi†}F³ì šiŸ‹Þª½*È?«ªê¨ «"ªQEU1ÕYuQÅU UR•’mUiUFuSeM˜*§zØ‚ª§*¯*pUQÞ–wU%ÕKUV½UUUõáDj²ê«ª©~ªº¼#頻¦ª¥ú«Új€ª£êªjš¢ê©ÁjªêÎù’šÆ§iŽ}i_¶%(Å·5±mÀH›žRÛ6ô=@íìAjoQ{˜:Ú#j–§X6a[Ú©ê«¡¸Ê¾³9Í s‡Òضnž9hà@DgpµµªjH>[óÑO؆v¸+¶Vt+¹¸8Ó!Îrñ0Ê^wñ)­³”Î% ’Ž©”KHœ£Î.U²¨–áæ«a¸ÑÆSÔpÕXPMÔH¬L›q6Ý£ôöúʶ£ ¶=e´hœ} F©ÑjŒh¡ÆªqX{ÒaüDõÔx5Áý &bՔ⺯mÕL}«šÛª…š¤–ªej¹Z¡~V+Õ*µZ­QkÕ:µžQÔFP›¬OmöNÇ­j›Ú®v˜PþÆf²ÅÕNµKíV{Ô^õ‹Ú§ö{žÃAuHæ)êˆ:ªŽ©ãê„:©N©ÓêŒ:«Î¹E꼺 .ªK겺¢®ªk*RE©ëꆺ©n©Ûꎺ«î©ûêïRÕ#>®«'ê©zæ»ÕsõÂóL^y'ôõV½Sï1®ú >ª_ /Žéy%ýaÇæxn Ÿåó0†z§õ0ηùŒ€‘b…X £0±y`îš{æ>Œ†1¨`¬w¢‡ b“Ø%¶ˆ½bØM^ø–‰ƒ8ŽÛ“`2L©0 ¦Ã Lb~â f‰Yj–™åf…yŒÂülVšUfµYcÖšuüÙ)ìb¹¤.àÂÅj—Ò¥riLc·Í¥w_¹ .£Ëä¾vÙ\—›#9Êmwy\^—Ïp…1­ËÉ×vé\1ôcÀ•p%URWÚ•s]!WÙUqU] WËÕvu]}×Ð5qÍ\ “ܵvm]{×Éuu=\o×Ï tCÜp7ÚsÅñÔ”wß™_Ío|F<±ßÀq¸/ÒHNÂm1?¶ûÅK³q8ˆ<à ‡)¹8BùÄQªM#Ä1 Ž‹x‰FqF.Å•¹ 'Å)qFœçÄyqA\—m>qE\×D¤m$®‹â–¸)n‹;⮸/ˆ‡æ&¶yOó|©`¶ÛévÁRøÖÀ\X aÌe0Vx~æJIçÐ_ëœ:³Î¥³èÜ:«Î£³é¼:»Î‹á':b³ò&XßSQX½`>ü KÌóÉ|Æ 8QÆä•’©2^Æ+øJ¼doäu¼YJ,(ža{ñ\¼Äñâ•x-ÞðZñV¼#Ä ¿R~ìAuÄoâ3 ”B* RK#Q’´ÒÉ´DúdlGÆ£«2¾L ò™H&–Ie2ÛDúm:$ö¶ ³We !SÊT2µL#ÓÊtò+™®Q¤ÌÈ»ez;Ñ&·imf™D†Ød|”Ú;2\Ð¥e°Ì¤ ê2º.« ërºˆ.¯‹ê º˜®È‰yªø(“‹O2Ôé⺒.¡+ë’ºŠ»ä.ëRºª©ïN¸S`˜†°nÀz¸IEìñBÆ… p 6ÂmØwÌó6Ã]Ø÷`+܇mð¶ÃCØ`'<†]ðÄ|4/a7<¥éð öÀsØ /àx ûàì‡×pÞÀAx ‡à>ãU¼Ã{8à(|„cð+‡ßà|ÂçØŠ¶akÚŽßÐ.\JOá$|v‡Ý1^Í{y ï‡SZðz>Ìøåµ}á´–îˆ;Î;ùo±y«}ÂÛìSÞÎ'àŒV¼ƒOÂY pNk8¯ \ ÓpQ#\Ò—µ…+šáªvpMÇ€H¢t\×Áê7õÉÓ<$(Ѐ@`ù1 ¦§ ‚Á±(ű 4”²PÊCŒ‰AŒ>Œ…±¡"TÂ8PÙ<1OÑ B$´ÈPÆ€jPjؤ&·³ƒ &Ô¢_è=·±l^nËÚv°c§¹ïíYeßÑV·ÀD˜”&ïsGm(j¨mOA¨k…zP°€†¬ 4†&КAsh-¡´†6оvžfì¡töTWèÝ¡LJžžšê }Üd7Måu31Ã0†c¦t#Ýl7×evY°#íÆN´;Ó^ìMG±/Ã~tûÓ óÐ<ÂtçÒœGq>=ÂôÒ\FÏpeÜdããf›·Ø„˜Ù&­61Þ°¼i3à{› ?ØÜøÑæÁ϶ [ØÛÔEHÙ¢D¶Y[𨖡X¶ ŶU)Ž­FqmuÊd;Ò×¶e¶)‹íBì*hRi;’ÊØQTÖŽ¦r˜…ªØo©ªDÕìdªn§P ;•êÛYÔÀΦ†v5²sUK{”ºØcÔÕ§nöu·'©‡=M=íêeÏQo{žúÚ‹ÔÏ^¢þö2 ²×h°¤ñö!͵¯iž}Cóí[›ÄγWì#WÝNÇB´ Ó2,B˱(­Àbô3§•X‚VaIZ¥h –¦µX†ÖaYZßRN¢ë8™nຉSéN£Û8îà º‹Wm:¼fýj›R˜mF)ls ·-(¶¤”¶¥²­ù.?ç{ü‚ïóK~À¯ø!¿¦6vµµûù¿áÇü–Ÿð;~Êïù !ö:¶wi¬½Ocì=ÎÄY93gç¯9gáXkbuO—UÃZXëpi.Çe¹—áòøþ€ßã¸sU®ÉÕ¹6WãZ\ƒëà6Ü…;pnÇݸ÷r{î̹+wà.܉»á-¼‡wðÞÆûxÚ®¶‡ín{Ùn¶'e¥”rQ6ÊÉCy$çÑ<ŒGñC“x:Ïæ™<—gðžÅó889‡q‡²ŸSp.ÎÉ•¸"×ãºÜ›{Ù>¶7Oä ¼€çs~.̹(à"\ˆ‹a#l†M°6ÆæØ[rCnʹ97âfÜ„[àOyÂcxâa<΋øGþ—ð÷ü/楜ŠÓqþŠSszNË8/çᜋsn.‰ °>ÖúØÐ”1ùL!Sš[qnÉõ¹5.ÁŸpþ‚ûMSØå~Ü—{pwîÃ=ñ)>ÁÇøÈ”0ÅLqžÌ“xå<¿åñ4¦ÒšL…M)SÒäåü/ã…¼‚òPnŽàpNi ˜ü¦¢©d*› ¦Š©jª™ê¦†©ij™&›Énššf&çï7j|Ù¼2o¢oÙ ˜\Ñ7mMô]œ,gšën«y+ ÉÂæ½ÌnÞ™¦¥¾ÈZ_Ò—õ}U_Ó‘úŒ>«Ïéûú˜#§Êirºœ!gÊYr¶œ#çÊyr¾\ Êïä"ýÄ~°ŸÙq\˜©}:–Ž­ã踶œŽ§ãë:¡N$ïË–:±N¢“êd:D'×~Сæ°9";ÈF:LÍ6äJsQ®2WÌ5sÉDšË檉2×ÍyÕÖ-”ä!yP–'äIyT‘Çäq¹O®‘»t ¹AGÈ­r›N­Óèú¦›énz˜ž¦—émú˜¾¦ŸéoÚè´f $÷Ëò¡¹e‹™ÛTHµ¢-v¾;¤¦«Öj†j£fªYªš£æšõfƒÙh6™Íf‹((ŠˆZ¢¬¨m†šaf¸!Ä¿¾£NxÚ­UiwÓF•¼$Ž“´´Y(¨Ë˜‰µF&lÁ€IS)¶ éâ@h%è"gëÊw~ƒ~ÍShÏéG~Zï;f3mOOsrüî<]Í{ïΛ'qŒ¨ô(ר%ÏúRZ}(¥Î£¸¦k^+é÷ãšl&ž’Q+I”ÌvrËÙŽ’5‚52žõcu¤²l ¤ÚSxŸU‰Ö‰ÖS/M’ÄÇO-N?>L’@ FaŸR}€ÊQ?–²eJ‡^­–ˆ›R4ù¨ƒ¼¼*>9žu `¤2•a»|­\Ïvâ´ï î%±Nðló~Œ³… ¤dd:ò‚¥a e,u¨•8:HaïHÜ}”R#)£˜U¡³ÿgÉÙSÜA6Ó„”tËf5=d8¾Î+¥zª:™PE[´ãQQŸėb]¶†/WL^.wÄl2càRJf¢»$è0‘*W÷°ªbHÕ(9e3THh±d6JU–âìý_­Çô9TÚsã…î%£oSŒ“ú7XMÕ1.ù”¼4¼™Ç/áBS .âæüŸbF¹‹ ‡F.ÂDT­]UŸ¢¶ {Q"ÀŽ9vœ6@À%è™c×z>°ž;äܸKÁ69Ÿ“Cð97¾$‡à+rúäìs à9÷É!Ø%‡à9_“Cð 919 97’Cðˆ‚oÉ!øÎÈå±Ìßs!ë@?Xt(µý„E ‹‘+cö–½oÙ‘zhäê˜zÄ…¥þh©?YDêÏF®©¿pa©¿ZDêo‘úØøR9”âJÿ ¿Á_ãø3K¸ÈRX±ŽY¹c °#D°#p°E °(`f ŠUX°%a°Ec#b°#D² *² *²*Y²( ERD² *±D±$ˆQX°@ˆX±D±&ˆQX¸ˆX±DYYYY¸ÿ…°±DxÚc`@:A˜U‰µùÿmìóþÞb5üÿ Èðÿ„”âxÚ•×{´Vc×ðùœCT„…"D!ôB:IÇ-©B*$¥vJ¥“Ô®vé|N¡’ !¯P„¨t¤(¤’dãû¾ÿ¿ñí1æXkÝ÷¼¯y]לÏÚÏñÏ_¢®Èÿ7’éˆTu±6"=4"Ó:"{0"×QÌ(V2â¨J¢GÄÑÅäˆcª‰6Å«Š5Ç:sìèˆã`g¿ü+#JîŽ(Õ+âøÕ¥‹‹'–"NRûäN§”jbíÔ¼ˆÓÚ ùe»‹¢ˆÓ=ŸŽÛÓ#Ê׌8 ‡³Ex "εvçðpÄE{#ªàRUý‹—E\â¾}Õp¹‚Ž+é©çÕ]"®U¿fe±5âºAׯ¨·Öªˆí×v­Cg݈ú¸ß„O#×&Î7Å1oqDsÜZО­ÊD´¦ÿ{wª}WÛˆ»Ý·¥í>\ïǯ]àóÀ¦ˆöôvÀ¡m=?èú-Á|˜';Û©´P÷:;ÿ¨uÙñ-ÝxÙÍ}wØÝå<®Î“x?MÓ³øô‘Û—¿}a÷Ó“þ°óñnvÄ@xƒpL÷ p†éïp¹#ôkÌ‘öG©1Jþ(u_äÑh\_âÏüÇ‹(p_À×ñ<Ï»Bó2±Po’ó/·ÎOÆeŠœ)z2U?¦á8Ýó >Î4K3ñž¥§³ÕŸÍÓ95nsÍêÜZ¬Ì5góÔç~žùœo}>ìj,ÀáZò! iY„ß"¾êú*¯áúšú¯óæuÜ^‡±×%4.Áw©9Z {©:oT8-ÃcMË侩¯Ëq_¡g+a¼ûmõßá×*=z—Ž÷`½OÓû‹"Vãð½àü—¦éûÐ<­áãGÖ>Ö³Ohø÷µî×á¾Î¹Oa~æy=ŽŸ÷‰ø‚®/ôãK{hØ€Ó8_ñæ+¾~íÜFõ7ÙÛ$w3î›ÍÍ7jnÁ}+¯·ºß¦þ6ëÛùºÏ´î k'¿vÂû–®oÍÅwfd¼]æé{ýøž?˜ïx´Ÿ½âGñ“s?ñõgþþ wŸ³¿ˆýúº÷xü'èûUî!¼™‘ß`æÑa³þ;MG<áÓ¾üÁÃ?`éE‘sâþþ.‰¨.¶F"Ñ'Évâp$Rù‘HW"‘™‰ìÚHäj9Þ]‰ÜÁH«%Fˆ•bo$Ž*.`åìQÎUEG1ZlŠÄ1eEcç˜ÅNñ6Âý±öŽí%p9N ÜJì‹DIX¥zDâx8'F¢tM±('ª".'•„÷]¢ÌúHœ ã45ËÒqzëH”“SÎzùe‘8Óõ,<+4ˆDEÚÎ)-p?·K$*y>¿m$.˜‰Êò.t¡áSeP$ªÊ­ŠÇÅt\B×¥¸]*¯š¼jk"q—Ñ}™µËa]®þå°®{…zWìŒDu\þã+­] ÷*ëWÑ]Cý«á]MãÕ«#qM÷œ¯)ŠÄµô^ ·&Ojâ^\_‰êFB µøwc^$jíkmšj;W§…àO]ëuaÔµWOzÖëá\¿šà[}ý¾ öMöèQCZÒÒPýFÅŒFðÓÕd¨Ð“¦´xO'š¹6S/ç<oqßÇ[Ý·è.ô÷6}k‰gKúZÁkefZ»¶†u{e¡Îpî ëNþ¶1gwñçn÷Èi«§mñ½ïûyö~íÕkcº;àÓƃúý0=pxD¯:ãí½žx‡.fªëªH<ÆoïõDw\žTã)ýèÉ¿§éî%ï<ŸÅ¥~÷Ñ׬öµÞ~ÿ2Âüå㞯Nþd!g€õæy€ZÏñd ½­Âm¾=OÏófáy<óy°ÚƒÝ¡k>CååÙ t½@Ã0|†Áw8Ý#Ôó¿#1§‘ú0ÊùQEÿÆ‹|xQíÑ®£yãÿHb´Ùmï%gÇÐ<Ö,åmó´èó8ó9NÍñôMÀ©Ðg¦Ð¹‰fb¢¹ŸdÿeüüILƲµ)0¦ê×T÷Óôbº¹šNß |gà0ƒß3½+fá1÷9ÎÏåï\¸óäÎÛ‰ùj.Pÿ×…æ~!‹ôëUç^ƒñš¯óz1KpZê¼ÿ‰7x´ŒŸošùåz´¯ίÄã-sö¶x‡çï:óž½ÏïÕféú>ÄãC¸kä¬Ñ£yù‰ÏÃ'¼Yk¾ÖšuüûÔ|&w½šŸÃþ‚o_â´Á¼|e¾¦o£›Äfg·Ð´ÅÙmp·ÛÛAÃNþ}‹ßw®»x°KŸÀc:?zþIÝŸáî£o?=`ýÊÿC<:ÌÛßõèwÜÿpÿ‡þÉË¿`ý­Öß³#u#™H‹‘Lv+#™òì;f2S>’ÙZ‘̵d±M‘<Ú^ñ’‘,Q#’ÞŸÉҞ˴‰ä)C#yêÎH–­ÉÓ[Dò kå`—oÉ3;Fò쪑¬Ð'’íU\ÉsaV‚qÞáHž¯NåE‘¼ÞEêT‘_ew$« ïÄä¥®Õ "yÙ²H^îzE^$«ïä•5#y¾WÙ¯áüÕø]³*’×NŽdMµ®ÛÉëa×êÉËëµi­§÷V²^—HÖoÉöÐÑæ†#ÙhC$}gL6iɦô7¥­)žyÅ„Z·Ìä­ôܺ5’-`ÞFk˲ÂÙVê·^É;p»ÃþÖÛÐswåHÞëëûýê¶s¦¬Õyn'|¡»³šqî‚KWÞtµß­0’Ý»GòqÏ«ûþOLä“|x~žyß$Ÿ’ÓÓµçúH>="’½øÛÛÞ3ø<˃>úÒV_|úò±Nýô¡5¡OùöÀÀÃçòéð3ù<Ÿ—3þàÆ‘BǹCpjÿ1Lá<sí#åøNš‰Ï(Ï£ôëÅJbM$GÓ0Ú™—pãÞû$é}’«‡r èoo®ÍÏ$ý~–÷ErJiQÉ©p§YŸfmº>Í o¦š3y2‹Çsx9×ìÌÓÛù¼[€÷+<[ûU½xM_çÏb^-1KÕ}÷e°ß„½BV¸ËÞ[túî˜|Gþ;zµÊùUzô®~¿GÛ{´¼¯Öjœ?àÑÕþŸi]£×ñòø~&$×Ñù©¼ÏìÆßõz¹Îçð¾PÿK× j}…ïW<û—£…¾l2‹›ômî›álÆëZ·ÀÝ¢[ôÈײäVØ[ùµ†mz¹ ¿íz¸ƒ_;ÌòíÔ«>;ÕónI~kÿ;¾ÃyÏw©·Ûþn~}ï³ôƒ³{ìïQw/ìíÿ¤ÎÏÎîÓ§_ðÝß~z¨qÐÞAWï¡ä!žø.™<\!’¿ÓxDÿ€õ‡ÞéÏŸÎÿÅÇ¿F*JŠÕ‘J´{#•l©”ûLM±2RÙ¢(R¹^‘ò[7uT1Ñ%R¾ ¦ŽY©â"uì H×Q8_Â}ɺbv¤JÉ-%çø‘:¡M¤JwÔ‰•„½“vFêä}‘*³*R§À:µG¤Nk ¬•Í‹Ôé‹"U®¸X©òå#ufÕHUKà|ö¦HU©ŠÖ*Ê;§l¤Î­©J¸WZ©óÔ8Ïúùjž¿5R¸^·²ý q¿hz¤ªÐVUÍ‹Û \.ÁÿR—â)üjÕ^—ÑtEuAguyÞ©«Êë5`ø¾—º¿kúDª&½×©{žµvGêÆÖ‘ªÍË:öê¯ïÜM…‘j`¿>7çGª}yÔ„ÇÍÒB[\›ÓÞ‚¾ÎÞÏÛäµÄ«%ŒV„Þ´*œi »µœÖ¼½Cþ°ÛÐq—ÞÞ­ö=x܃ó½8ÝGÏýîÛÁ{ S¤Ú«ÝÁ¹Žö¤ç¡õ‘z˜g~[§Á¥³k¾vuíÊÿn8w÷üøäH=ÇoêT|ž¢ß;1ÕKÿ{áÔ›}hð;Õv?ûDª¿çþúžW¾Úä äý@36çAüñL=OÏ󞛋Áø Ñ×!ú3Ô<½ Æ øÓËáÖG8ï˜ev^¤ïE_ú'ð³6RcᘓqpÇï·ð˜@g!Î…r'Ñ7I_¦Ð1ÎTõýOMUgìi4NS×ïóÔ4=˜v8RÓÍÃt^O‡ï;Tj¦Ú3õe¦YŸ s&¿fÉ™¥O³ð˜m}Ž9œco®zóhÇïù°çÃ] î¸¯ðk¡s‹`½êŒïT©Åö–È[‚ÓRxoØ_¦¦ßß©7Årº–ów¹9]á~Ž+ÕX w%Ýo™×·å¿mýß1ïొÎU0ߥñ]úÞÓó÷yó¾ÏØûægµÏýj½Z=_¨óµÿú\}èyùüˆ÷9ÿ1NŸ¸_«?kñ_k–ÖѺŽ7ŸÂøOõëS³ñ™çÏôÒOÞÔz<ÖÃúÜõs³ð…ÿB/=‰ßZ6ðä+ø_ãõ5¼ÎmäÅFº6â´É l2Ë›ø´‡Íjovÿ ~[hÝk Œ­ð·™³mî·ËÛN×vs¼§°w8·Ã½ßý©ÎË›oÕûÎówfpm»hØ s7wÃù^~Pÿ}Üoµ½âGóö£ýØçßøI}4ì·¶Ÿ·û½#À;`~èÍAùõçW=û•‡ð9ijßðüÝçÅ÷ÉT‘ëŸxüÅWß'S/ŠtTt¢L¤“Õ#*ét:Ò™Òbg¤³ù‘ιÏå‰5ÿF±åEÑGLŽôQÎ5[ìŽôÑÄüHcÿ˜Ã‘öÛ>]¼(ÒÇFÚoút çK¬ŽtI˜%‰e‘.¥~©üÿ_¯Æ µÄt±5ҥݟ´7ÒeÛFúôÆ‚¶r]ľH—o÷¿Qôoœ¹8ÒgUŽôÙtœ}0Ò]Ïy®Üó†Fúüõ‘¾¯ ëFú"«VŠôÅ5#}ÉÚHW“sy÷HW§÷Ê‚H_ŧkxXS\Oßõò¯‡o ýøµh­…ÏÕÄè£v ±2ÒuøUGí:"]W½zÅìú|¯OK}õn‚Ù †€wsYáùf^7´Ö—†°YoÔI¨×Nc~û^œnb­ >Må6£¹™Ü<¹yüÊë(ô —y¼»…†[¬ÝÂÓ[èj®‡Íõ÷Vœnµî7}º-ðo¡þm0[:ÛÊýízèGÚ÷èô«"ÝFý6¼¾ÛÚ½jÝË«{i¹ÏÙû=·ãI;çמNß±Óät´×Ñúƒâ!õ:©÷ˆõÎüyÔ^y]èéêÌcôtƒ÷8=Oôˆô“øöàñSt÷„ß§ÍB/Ͻ]{ó­·³½i}FÏŸÑÏg{ Ï}ÍG?sÙß,çó~€úÕ ¹$>ùß’ö{>=Øluv(ŽþŸ¤}¯Nãß0ó3 W¿ÝÓÃõxÌ‘êŒæûüÇðd,žü×Zð×ïðôx³3ÞÚxgý_IÚ›¸)Ò“h›ä~ÜI«þ7Ô~ß—Õ|Þd÷“ þï˜BÃýž‚ÓT:§âçÿSzϦYŸ¦w~Ó§§ã7ÃÜÌàÁL³?“ΙxÍâ÷¬Âúl|góÆÿ£ôs<¹üë~ìùê-€åÿQz!ѶÆ«<|ÍÙ×Õx/‹y²Ø ,Ñ×%t,Ám)½K}ÞPw™Ùó?*½ÜÚ õW¸_ çm5Þñ9]ÅóUê®rÿ.ïòä=^¯æÛ>ÿÿåר©ÿ±Y o­ûµx®sv ëøï{~úSœ?ƒõ™9]¯Þçð>Çã |¡O_êé—>ohØÀßùÓ_›…¯ù¯Ö7ÁßÄ×ÍømöÙØlý}Çÿ’ôs»Ç­ò¶òx+~Ûø´ mtmWs»^l׋pwâò­yø–W»Õþ^î÷t}/÷Ï{x·×þð~4Û?áøm?«½OÝ_|öË?à3ý«Ïâ!|ÑyH_~£ñ7ë‡yuX_·ÿ;žGÌÉóvDOÿ€ñ]Eð‹xT„g‘~þÉ»?õçO3ú—¾ý™h™DõÈ$ÓbodRk#“vÍÔ£#“-™\™Èø¿‘9ºRdŽé™â+#s\åÈ”™RòJ Î/ïøNb¾€qB 1YØ+í¾t1]ìŽÌIΟÔQ,ŠÌÉjŸlïä¢È”É®§¬ŠÌ©³#sZÛÈ”-.ÖGæt|ÎPóŒM‘)W^´rÊmˆLyå{EæÌÒÂõ¬ÆBÞÙøŸÝE¬‰L÷àV,)Ô©¸/2çTîÏ‘ë7Eæ\ü*ÑT‰ÞJrÏsæ<<Ï£ù|Øçóë|XÔê\€We9•ñ¬|826°.âÓEòªØ«Â“*òªâ\Õùªp/vq‹È\‚÷%¼¹D½j™ËÔ¸œÏÕiú}ÿ‘{%_¯âÏUÖj8_cud®®)ð¾zgd®)ŒÌµð®]™š0¯“sœZzp£µÚ[#Sǽß$™º®õô»îõàÔ§¹>œ›øp“¼zÙ×›á߬ ÍHCyèiDWcëG¦‰óMø×”î¦x5s®.y|ÏSóþ4Ç£¹^ßêÌ­ü»•?~ãdn« èj©-ili¯­øÔJ­Öö[Ãk ïv\o×÷;àÞÏüjË6jÞ¥OwÑs·:÷ÀºGŸÛfî^y÷º®û† šîçëýxßï¾íÔk‡k»ƒ‘y€8ÿ.íaµo#ÌV{{œëÀã´tÄ¿#nÒþì‡pyØZ'½ëdÎÝvgXùô¨Ù}”þGixVœ»àÐï®<êjý13òÝxÑÝÙÇå?ïÉ^3ù”ÏIO4ù¼}ÌëµöÖáýëa}áú¥õ x~…ïFÏ›älÆm =[}N¶éëv³¸C­‘ùÖçm—üÝzô=={x¸GÎ|öÒ½¿Í”÷{æg¹û<ïÃÿÚ~áÕ/8î7xpçƒü8ëW<~ÕûCô’çÝŸù?‡áþn†~ÿçÊç#úæÏÐUÄ¿"çÿÄñ/ýú‹f¿2óýïU‘‘MÌ#›¬)ܧЉÆbCdÓDw±6²™>‘ÍÖ‹#›k!öF¶X¯È]6²Ç lñò™c7Eö¸‘-ál‰é‘-YYì‹l)ç_ÙE¶t¡ö‰%œ“Ô8YÞÉ8•q-Ó)²§„PȞVCàXvvdÏØÙr0üÈž)ç¬Ö‘=o¿²ð®èÌ9®ç¶l¥º‘=¯ª€qþèÈ^€Oå¼È^hÿ¢J‚–*¸T™Ùª…‘½ØÚÅø{ÿf/¥á²Õ‘½ÜóÕ„Õqó»![½(²ÿiÙ+Õ¿ÿ«ÚD¶-5VFöê.bMd¯©.à^ëLMÞÖ¤íznàU-{7«/vG¶uùWÏ^}97áÝ -àݼ,² í7‚ÙDNSÑŒçytߢVsy·Âi!÷¶A‘mIw+~µÖ£Ûùz'}mh¹ ÆÝúÓÖÞ½Îݧ=ï¸l{~´ßÙjwTûÁÒB¯òì}–}˜ÞNzÝ™wÞaYï¬l}è ï1uS§;ŒîÖžÐ÷'õÏ{*ûŽOñµ'ÿ{êÏÓ<{šŽ^üè…k/ç{ãÙ›îÞ|~Æþ³¸=Ë«>8ô¡«s}õ¥//û™Í~øæã”OÏþ0GÏñà95ŸÃe oª7¯ƒhLǼ‡à1ÔÞ ø¿àì0> ³?j7’O£x2ZÞKfv þcÔ«ÿcõ},}Ö ð§¯ãàŒ§wW-ŠÜÕy‘»ß÷rÞ/¹ë迎7×˹~känpö†ù‘«¢®€ë{`îFþ܈kmçj;S›?u`ÖQ¿.þu»ˆÕ‘«‡[=êãÑ uän¶wóÎÈ5´ß§Fj4*`6áU>4ágS˜Íô°™zÍ<7£;®<\ó`æYÏÃ3ï`än©$Úˆ!·9ÜæzØœþ[ñ¾Í™Ûàz·åZâÛ’îÖxÞ®g·ëåjÝ û.{wÑq7nwïŽÜ=øÞKç}ÿ„üûûD®Úíé VGëáv´×ÑڃńZòîAžùN—{ˆ'y~§‡õña|;áÚÉ™N´?‚ç#|êL«ïx¹ÎfîQ÷š¡Géë“.zÜ•o]yØÕœvåßcÎùŽ—{Œ·ÝÔìf&º-ð»;ßVw:»{~Üóãž7CO˜¡'ä>!÷ {Oâ÷$ Oòªþ=hêAÛS¼xŠ/=ÍlO3Òwßs=õéi{O[ë•棗^ôv¶·>ôæioz{ÿ³Æ“gà?Cß³4?«‡}ì÷Á»ŸûêM_ºúÚë˾üèGo?uû™Å~töW£?Œþ8öÇ?ßg$Ÿö|¸`vžãís<(w ¨Ïåò<ˆöAz6H?žW{0ŽƒaÑï!<ƒ¡ö‡âó‚ºÃ` “;Ìúp^ çÕpü½ßs#ðÁ§öFòq$#iÅ‹Q¼y‘//òéEºG»óhZ_òzÉ,½¤w/ño NcàŽu®€ÞqxŽSkœüq|G÷x<Ç{¯‡ø4÷ <-T·PB9ÝO4=Oô®™hö'ñf’ûɸNvÖwêÜT\¦éÓ4\¦ÃŸÁ³8ÎäÁL^ø~óý:7ËýlþÍÆm6=sœ›ƒã:çðg.NsíùÞ›ëÌ<õ癫yrçóg¾¾Ï7³ `,pæy y·ÈgéUø¯òí5ï´×xøºs¯«·Æb¾,Ås©ç7ä-û'ð{Ó\¿io¹½å°—Ó·BÏWðb…ÏâJßâí[ðßúëY#ÏÇxÚ¬}`\Å™ðÌ«»owßîÛ®UÝ.iUVZI«®U¯¶$K–-ɽHroض1Í0@`RZpêŸKBŽôÿÈÝårR.¹žË…\þKÀZÿßÌ{[Ôl“œÁ–öí̼™¯ß|ó âP!öïøYD ¨µ£»_C˜A¸c𥢑õ‰Ä2˜añƒ¹„Ã"fqˆ×qü4Òë…Q$r'ÒéÄQ$Š&±+7Q½¸›€ôHÐO.éžé4™0µ'Å^ò—æ$wÄÖÊÆª ‡]æÌصrµ5!¿Ofü¾°­¦•Ѿ„ ö*Þ8üý­»¾c¨¬eG_8?RSéå”ëMœ'R[^Ø]® { 9ÆÍ–ü°ËÎS”¼°Óη$_`?}iM5Û}é‹Ü*G0ßâk¬«j( œ;y«JBåñp´Aq(Žùgq¾¢äCG:·ýÝÇj¹yX¾üäG.ÿsò d@Ö„™<C+¸+àc”[S«v:ì‚ßj0ˆ‚NE]òUA/èŒÊå¯#Äíá}È„œ(ðÚ0Çâ€FžB,kìd0‚o»Åo+âí/Œf½¬Œ#ǰ—­Àa‹Üž»Ý~õüw?¬ó9Ï0z5¹ÿÍiÞÄ˃ӎے_Urxߥ7»-H¸é»“Ÿç¾ ï£(ztð%' Þ\ŽE>pÈ2ð^¶#7aM=a°€XŒ®cRm[„DÀ·ÈN!ž7vr˜aL‚ŽÉÀ 7ÖaÄ æ–i ô“j9™pbTRðå¸]«"E…qXoŽðÕ…XưæÚšºøÂ¥»dÖawƪ[™ >!³Ö‚‰Š‘ᶆ…ðÀëüöHÀ,´çY —ÞHçÛÆ6g^g}ó®pLïþo(*‰æE½È¼’ ³O#$T̬ȅqÆÀ4€-F‡™I¤C<«ã ½Sú6v¹1sœ‰#TÈ‹Þ!”+jÕõxá‘ ógð›J² ÿÑ6$Y÷)à:ÃÚØýïâ“9…ÜÃÉÞ{’-gñ®4>?ÞV ¬Œ¦€Ž<0/;*D¥¨%ÑhÄ,G(IÄ,ñÓw4ó‚)faJ›p—ÃPi±¯ÈQè(p;¡³Í«³Db6¯Ædb¦¼fÀ^3aÄpLý%‚§žÁ÷ïúàÖJ÷\÷Ç;oùÌ¡¯ÔcaàÀ`ð†ÿço†ŽÞô6ï+Yµ¯«qÏd·ÃsÇÜÔ÷µ0ë“Õ¡Þ­ëgއ{¶5mވנ‰ËïpÓ|ª@݉æxâˆð˜çˆDAàˆ@qØ fdíÁ¾R³Â·­ù»³_¾©}öÕwîéÜ;Ù_Ь³Êg~¨n jû 'ú£ën8SR]" mš{äû7 ~ï°»¼½¸_oT<ùeíS¿¸oÇSšèZ’/ѵ  oªœâÎdÔÇ^035$öb `¯å¿Â|ŠÉ¢ˆåŽxNàgÇÑUÛ;♣ëétc U0]|Q=F"Àp™¾À©ž“ 7FÝMu5U•á`QËàÀaÃg >¯`Wg, u3ÖHX6ÞŠñ£‚É걆J»›ÎÏÔ^̽)Œ¬þÞ펞ö¢†áŠ@»ÇÅI¿f åyóô×É›úÊ¡Q@Ñ49Ã×*Íåþº°]‘kt: 4<òðˆå ‚D.eX?åRÊ f½ÅÞg‹`;+óRFk*¿—%À½1{ñÒÃÉ–gðë7~õîþת6Þ1žüÏXÙû™“½Ìo>™ü÷7öò¾±‡¿rÛ“û›Þ{w쉟]¾ŒÚà½?ƒ÷ú¤Ÿ´M §áUe‰ g³ '1©™ˆ"E‘‰éò*JP)ö*:_h ø›ý#Œa@ʼn ‘7,‘7ñ£T¢93Š41CdŒàÂ|!m^öŸç_«Ã?V’¡83¤Ìãg˜y%}÷$›Ï düäçéøAô$%¼‹T` P‰Áz"øõLŠ&K‘ȼÙQª¤È¼GuT¬ë¥* J‚Éêç®Ø ,ŠŒ2ð`ä+UªÓfU,fÙDB ‹¬LL­ÒŸVlaF Ì¿Kµf/~Ú^âs¨j #ûyßY†¢âÊ\*÷U¹´àþ=ò% ³àN¡Mánb)È- ›5ˆûÞßžÿnþ…-YÔýÊ3̯ÖÑsóß! †16¬ûȇ"§ý˜á% 0è …ñ1ÕÜŒ®Ê8*î,ȸ²ÒÂ|›b4èTƒk4·(ÚàÅ2« ºVNeL!\Á¦¥ÜÓ±uC=¾àW;#á3m5ûn¼{õ-ßy`uÕôm¾47rÛîµþxAÄëŸé;öúý«æÎŸ0Øò­8h“w$õþZ¿uâñ¿>yè›Ïݵ©ÚSÑ(ñ˜>÷Ú'ß¾gîcGZaÞë$Cé³(‘Ÿ²_x¼À€ÑèÒKàã·ÅX¯-ÆÕ¾20Gÿ+ϼÜÇÜÔÿ2³nþYÞ7ÿfˆðëv ï]0¦ tmm¢Z†„g°„À2’ÀœØI=&Z–ÚL—F/Ú‹ÔlLô³êëüñ˜úJ &#÷÷g \§¾úèYã¹¢—‹˜ãE/_º¹)oþλûR?™ ûÙÝ'/c<ÿ™ŸÃŠŽòþæÃR[²«º2 Øz°ª¾ráe•Ÿ,ÉÏÓ¶ùèðR~â ?ñi~rSfP"^XÀ=…@ Ï ~.« H†W¬yÜiÉÇùâb£ –á \’áÈ…—ŸÎ_žÞ}{#àË¿¿ü[ænj_Œ¾äYÿL ÃrÈO“•ÀÄ¢Y¬`yê@°7 ¸F]ȶÔW“¯9¼A¶t®DM€µ£Dcòs¿õñÒUa™QlJuiqO¼XÚû"~íÇÿf”ge»¹°÷È{H³¨ íX‡~°{ˆEŒÃ v š4ú jHFPeqÜŠ›ÀœÇE_+À #t‰·Ôå‡ü ½4ï‘dY èòÉxô1o®”|Ûä t¼xá%æŠÿÎe™ÿf©E_3ÿ€]a .ÛüÇat>¬`Å#w ¦t‚× jra²ÖHHåÎÁ—­ó#ϼôðÃT4@_02Ùø  o¢À²“#à]<ˆKq 8(‚@¥^Œ×µaðÜdfs’oÇéÀmÏ›Y÷ùÑ—˜{þh®üt²åa<¿þ—¼Š9Oø‹¼+‡ïƒyÚкÁ—ʈcá9ºöÎÅïÍMð›»SÍ7wN¬Á&Ú³[#qºÐ ñ‚Àœ$–®íïµEãX$ÑQì5ä¹ê'BfIï©ètq„ÀbØÚçˆÞ$xuĪBrŽÃÄ–eU3Öë¬SЉü\jˆD¶â8"Øú b«.ÌöŠÞâbe·ûÔ…'na$aþ rKò4PɺpÒí”ñ ü­{8ÀƆwßf¿î¨õ\úr8̶{ªì—êyŸÑvé;_€öÞ…9:‰Ea¦.4P›:7•è\^Õ äí0%„ڴ9áI—ÓÀŒ€{uì‰ “Ž<Ãü‹Ðïú=3Íû$û¥\GXa/¾û¶ÅÊþJñÉ—Str4ù9ŽèëúøàKÀœ¡ÙÁšd@Ȥ?©RÆ òEu^Hó½@Ö€_BÕ‹Âv媣|pÙæR—´K„SM—Ç/hš²Ï¡åädB‚ŃŠË¢s-X¿(³Ù ߆ÓÐ8D ÑŒ9æÈ㜾røÐ`iG Wç0|E÷»…ùoù}ü™)@ý’ÏUfg?ñž>4xÓú¨Eé¶›ä¯ÅØ¿u–(—Æ( 4uùÞºÖJ²m>{ÚæAT\\¢6ŸÌ-p¨á—eÙò®Êõ7?öêö¿vWÿ®ÏÍŸ¿ñkw÷_¬Þt×Ú=¯žêƒŸã{á'ó›ƒß|îî-±‰Çߺõ8织×=þÃSÛ/j]wáïÛþôá–‰§ÿ4Héå'ø~#ò£JÔˆÎ%¬¹°èšXØkílϳƒ/#ÄèÀ{¨Ë³À~O X»éfò Í e\Úl¹““Ÿ-) CD@RP„”aãX‰Ó'¼"Û›˜@'>¦b›ÏQá3ùHË6WÜÓûzÏ Ó–§ÛÖØ ¦_iëxïBæžòúÄØ±ÂüùãëOeC­¢ôxžë¿Wýõ) ººØŒÇñÓ¡ï$¿¿þ\œ-” ‹WŸ®½ôãE‚ßu—'J€ß<EÏ'$h H„éP’7e²é@^c"{§¯œW½r€_–¤m;NC¤YÆ‹_Ø!Qu¥¶iKœ˜´üØÁ^0•G‚¶b°Hî,ã<¾ÀE{ñ"ºþ8ÿ“:üY%Ùgü¶ùv÷ðn›ûÄu­?öÓÓ§¿T›ÇüÅ¥Ÿ¯>Ø·å©#‰’Õ‡û¶|ÓšÈ‰Óø?’vÞW½áÔðì ·ö0/Ìÿ$:~´{÷qŠËÀ<ša”Kü#@¸þˆß@ÌI†ÃX3 ¨*WrOQŠ‹D{§‚‚,‚ÉšÆß¼SˆußñÕ›¿^z»²óëÃ7Â<îà*ª§OϽpKïämó?£Ó¸ÑylKþ_¨ÑT$QLlNJ¡‰eSœ£ÁƉü†ˆˆ'*ŽØ•‹ÃK$IaåÄMCØt®¨Jy­j¶î*åÀ7ž½;šü Íõ?fË劊K#ÀÐ ’%ž¨‘µ(8šˆc§E¬™Ži«<'¡Ž/Ï]\eànZàTtë(̯guC'`wÛžÏTUM-´cï‡+— ^eËóÃ7•ž:É„æÌûÊÆ®ë>³o•Íé›ÿ!ü7.s$i,ޛرKÕc†Ëï°æ+ÁGnM4Ðè'¨Üß±±`4r°„˜2奌t0]-M¥Å~ožÇr ÊBVìÀ©3¯jiÙébwI6Ùˆ6·lj÷U lÙ¾e ¢ãøKûnü‹ÖVËhÌ-ŠÔ÷GÛwõ‡Ë7oß4XÞ|è#3sŸˆskeSQ°(Ð:öÔ•E¢‰ñ†¿¹ãð˜ÁhÏ÷çÛýۛ«;+ýò®m½£êZÕIÖ™|‰®s}^5úfS«Œ±ë±ˆØŽ(p{î¢çX{®y åü[ša†E"°hDæ1bÇA)$ [¾=C, œÕà›êEc±ãõµ±Š²Ü»UàÐ4žÖ/ á•@+.EÄâ`5 *Û÷\Ãá]³ žêç>°É¹ldè€nªÿ xXŠ»µß8bhX)"tyøì<$–ð ;ÊQoVµs‰¼G"H!ØEuùã\½5y˹Ï&oQxßù÷qçÎ/ËLb7à‘p˜jâÝg”†F êXKéH‰q€ÃJÞÅVCò×c/®Iþƈ­•ðö³Ü¹ä­³óSÌÇgñmçÉ»î¼ü[nÞ•ƒü‰"Vó~²#¥v›l”ƒsøT!¦ÈoBÃܱ̎£MÚþòvyó£ßºáúÏ\•·¾´Õ?pý=õ;îYÃ]úÙ¡¯9ûµCðûÏ×ܳ³ž®óÎäç黫ѭK}^uó í󿫱47w âÇ7 ¦v7\°m`Ǩ²¼¨Ðã¶Y©ÿ[«u‹üß V[W,½X1í3ÿr‡;ë=¿¯<µÐ‘L£'Ÿ›±ó¬ |äøÖ7vÿ—÷§–ŸËì+÷LÉÆÂ`™[õœ1•û5õç< —Žú§ÌpÐc‘"pä¨kbË8/Øè~W÷÷¢‰wâ/ëüγó‡žâ}nåÒ†‚¾ÐP‚ýˆÅóîÛHIˆ7Âø%„Nƒ.K¼_FµÈ ÁY¹”²f²s”~ qŒÈk}Ä~²‹"€;r¯+ŸyœX½Ý€Ÿ}Έ3ý/°:›ŸB®{çÿê©¿ßB6&Ûžà}.ë¥9 …E 9ìãùäŠçç×¹Ø'È4Ùÿ”ýö’¼KVGøÀc7Ì׃ò1€hþRØWìc\Om!êÃÕ5a=õÐY˜8tø ÉxÊfåÿÓ ¸ÑšóÙd'îÒfçßtÉ3.ÿKÌ—™F9éÒÉ:üíÜùgæ›F&n0ÎÿÌ›æÇ1WÊM¸ ,C Θ9ŸÂªZP„3 "f²ýåöä7Ÿpåè^³éy ®‘ 1òü;Œ‰u8móÿ]uUz“Ù£ò¾’ã=Êû0F&-FàËÑ´,1#³CqPYž&V!y¥7ξ§g ºy/³Ý~é :ø•ùéü«`AÕ›†÷~p6n²¸ª³ ÷«@¾Îþø¾M% 9àMñ qUÇΕb W'a(4†UýŸ >Qú²|úuʲ³5×ðzJrKJÃEàÎ{i´1;Ööå+ÕpØÙ‘ ~7c ­Ûº¯11Û®\{ÃCÏmè8Ù<ÿFÍŽÉUùÅ®¨kó'oí÷ç|?17P\1yzíÄG¼i}•ÍséͦÝLà~ž=ó™íÿNm±ªäEº®Ô…~¤ŠW ù¦"Fó0,°l²™²äFýF“9 ÍEjE¸:uà 1"M4\µÈ .I…–ï²:€NÛ”[Ж‘t ÚÕ‘Ý<K]m[smW]WieÀ¶èID4-bÑP­ÀW¶!³©É¥D™«8îuX®™MNU¬6ÕVØ…"ûU1Ã"ßåßò?c:ÐÚˆõ c¢‘щEn pBrÄ%­E:QuØp˜£f΄ !ài³‘E4¦—AN¹: Àùª{å þXtn»ÖÎ(Ý· ™aW™®> -´f£u£×lèêlmŽ×VGËËB‚<§Ýl’ô¨wXÌœ²›U|rŸ4ÚAÿ%XmÃuZB8 ªÏ6Qšâ¾[Y0³¸'ö”7Ÿÿã—Ï\_»&QUìµØäòÚ™½G;}ºÔ»iÏ¡š{ŸèþÐ_Í[Û7vW;•!gú‹ŽÜîjû|ò–ÓÅÖÔÏ®©×ãÿêØ=Rg¼ÿ ûË&ØŸëªëæÖÔ°y?fÃÉ¦ÔÆú£®Ã‡Z'â9GvßÜÄ79Z.²Êµ«Wm^;¾iþל¿aU´§3л§ÿ42r¨GôUµøë:(ŸŽ‚üù%äO):©E¤ŠX†ç˜áiDJû¤E¤Hƒ|è)wŠ)/ÓE¢̘V1UrÞÅß D¡©­Tsf2aËAðyJsKáÝîâ q`H¤Ÿ¤‘ c*Žá'`%†¨æÄŒ~êSkCë·ìª;õ¯uý¾“=£·GÌ|NþyÕöéáüÂðžúá“Ñ þìü`óÖîàç^ùð7OÕt¶´yrØñÆÙ³ã¬):÷ÒÄ‘@V4Âú×°?Cå¨!Qg%¾ æx²§Ãò°î9Äs,nŒ&Z]ÄžI^üÅþ`q@pD‚iV_ÆNï­²¡Ø†Û'nŸª¼ë‹·ß‹ VIÎ-m¯îº~ª&¯n´¶c²%v^ÙKõýkÓî‡'ýø“;º;:t’»ÀÝ{ËsÛÈ‚B!¥–ê¨Æä tî}è MÂõQƒÕ=éF²'Ý­mW/ûÍÕw«]Ænµë »Õí õÀ†¿· îV÷á¾ev«¯O™]ìžði÷$Úµ«7p5(G'ÖMÕ,ðGþ+åÜ`윽sxEè×ô”9˜úEÎFáËïÿÎÿ mDÏ$”Žö„î{!6ê6YMŸ×"½`ôÆ9d4Fà ° X—ºi”0±‘9L Í`@£"ÏhÚ¾áZû‘P‰öNŶéɉñ¡Þn¿ÃïØÂÅ“{!ѺBµZŠVLdµÐ˜Qª¼#¿¥`cEú¶- ïD]qíÖûÖµLvV»[‡VeGb[‹ÓÐÿb¿Ü8\¾yýp^닟tÕå7Mµ¶ÕÛׯ ÊĆÁÛ¶Äó¼¹Þù{«ÚtÏúù™GöWNœ¿Î`*éÝë;W#{–ãŒ*4í@oªQúûz΀˰È!l4‘Ü)íŸy¦I¾$8ƒÄÍ!“hM€ é1£'r¢‰Utðñ¯b‘(,²ªÆr»¯­?Ï›ÕA¨È!‰I)…QǶ-§ÇFW%Z•`qÐGp*_ §™3§wÎXmçìZëY€ÜÖê`lËÝk›&ծȎ{Ž·› \JÞcÎW,n.zÄ‘gø½1'×Ûð³Îf_ýº¦ÂæZSε¡Ù¼v Ð\hv]ú¡Á‚-ÖäIKŽd(´&óÍVü Ý”,çw+JiϦXßÙ©`y øN\þ­`牶¢×ÖµK@"vaV2ƒ@ä€)ó­ ]¥9$±X"ábxÌç F“@Òcb‚ƒÕ:J ÿN=5~ )«¥õ*C è Ò1k¬J7N¯èëH´4ù#aÀïW,2‰7‹^VXhbª¹,åNÖ麂„̦5Á@äÁ´6'ç{t•‘œ`ž|­pôæ¶}³æ¾½f…Ñ=’(^ ýàd³[)êZYv. †\/Ú}Çðøá.—Ⱦx|þ«-­f›Y-­8y#3`µuÚ~|ûÙHߖݪ}Ë Ôlä³õ7™[]Q»î`3à¯9ù‚ ¿Þ…SU•÷&ŒäªJ£!ŠE†Ýga8fïÆÈmA+v䪹°Wih‚†é6ºÛLjrÕ ø5#A6 Ó:¼y É2ð+ù}•PgÂÆ«aØZ1tiS_aP#g⌦¹÷182™Òc'F¯mX EmÐ%d½xL0¤sNŸºáØìÎm[¦'úº;ýeåþ ¡QË"u. R¦•×¥(ó òe‰2-k -U¦‰Üe,š“ì#tʳ‹z=W½¥}߬<°W¶1º' .»…L¥ñ¾CÅ õºü©¹tõÐpÄ•m è3io«oXSJ©<êRÌ5ÛÚTîÙ/Ÿ¥µÙ¬˜u:•Ì'íÖN{~{ggAåH£·jdW¼íî˜à[†Ú+:Êrø¦ló!ewZw[K¸@àCá¾»fÛj×j-QsùQ;½ Hœ˜gŠ@€ñ©8A~Jsh¶±¾ÑRöƒwÑ÷uˆÓ6:8s=˜Øäe¹en—Ý*éH´QgŽxS±ƒPÊ›;„ ÛÔ­DPËüžF¦¶ï­'†ùä¾=ƒ7G:?õb²>º}Ãp~Aho¼vÃPS‰sAøÜKÃO>p|ª¦¡†É}÷ð­sgÇÿP)íݺïXÑ›ÐåËhãåwø-ü9’·!Š— Ì¢WØÝ@©÷& .€—Ï‚A‰,Ê̜$0 W«W¡¡KA#²bK…‹~\ *+òrÜX_ÑTÙôç–ç•ç¸6ƒ{Á!~T=àç«€Ÿª%ss‚µá>=7k)½àe2Ùd¤o4ƒ æã–Òs[±¥DçFÚgÍ-$¡Ô`m¨Öïóäç娸 à€q9ÜÅi((‰$É* „CÙ6ã(Ývà†¶Ÿ tl>vfÕø½Ñ‚ÏݼøŽçSž‡¤GîJšêή/Ê+¿±mêìöÚþœŸtì[]úÅW‡»çèÚhcÝï ‹Êßzkï{/ßÚ´ëÁµ?•ŽÝñ‰µÇn¦þfëåß±àÇÀßjGu‰X¬„aXÜQ^f7ËXvD>‘VÓ$õ'eïTV4Ä+Ú+ÛÃÁü\\õP™q˜YÀ”‰»œ‚*<˜Ôî]JX„E–8晄:ü¶Ž7ÜV¶³1ßdtmz~à3c>:1Ûwÿ_ݵî¡=M}§^Û?ûlŒ™½ ÷†MífCIÇÆ–5'ÖU„:§9ÖäÑMº‚f]ÐSgn­¾!nÌ7 ýæ™'/ngý®lœýì=£SÓ·ÿEç—>{æPéê–@Ãö;‡:f{C°ö&ð5gŠŠQÙã«À˜³aš,9Äa’¼£%lÐPTÊY.-)i(m( ùKšìä!سÃN 2ƒ4 Á×ÖT0ìã%÷>uzx÷`MŽÍ\V7»ûhçÆ‡g㾎Í7=¼vͱUä—»¯›l³üØ‘¾bÙncÞkÛµ¶Í®Ú÷W+µ±JWÃîl<ç¡ñÊHÇh{õºcÆ|o ¸Ôe Ù)^›’éÚ:Ñxj§ÌÞy1ît¼t ÝSàˆ+ºàqjƒÏJR9‘çæHŠ` ™Ý†…çªRÓç–íµ`ëÁÓÝ…Q×x÷x¢µª²¬4Ïc!¸SZ´á²áêXðXÌœe`3pgîOEôz$»a!¼£dèìˆ- ¬ZˆŒï©ñ½†í!¼¡E8ê´1™  W¹Cä“…ù[þfäG͉ˆ#çÐöÅePºµJWê9½Ëæ`ÄGRÇŽÓC-q cvXËW5‹F,YØöK?}#â*eÆiïÝ8~z²Ò6-éì®[׺þ¡§à§MñP3Z…7%Œ! kdê1üÕt#IÔc¢À)†¡—ú{z%F#;n’ô,ÝQ³«ºÆI:‰ÜI@ï¯;å5KºcƒAë®Ð—¤«Ô_[_“ÄЈ™Ñ¨ùïЭ×!!¦Õ!®µã$üIä¶¶ ÔÛݲªu@´©®&¤øƒNEqøÍîˆWsÍXݺÌÂmãŠ9HHà*A9æOuuï,%l4'ß}nf‡ÃÉè– ׿x½¯­­3TèÉ_sM‘»¡lÕž¶‘ã—þÛnbíöî‘uïmæ”ÞüÖ«Fõ4úÑ= ô3Žvâ7T¼Y,`O +ƬSˆ£¿à‘^¡~>!®v$IܘÆ'w¦rÏäN›EfFÓriÐ(ÙˆÃ)ŠnÜj6é9ΡK‘Z7a+‚ ‡þœÁÈÚ¯0¶X´¡d|ÅBW™Õ Á¬^oRDê`&I 'M“M“Ì#2îûƒRgpb-B›7®Ý9±ð5>¶ftdxõª¡ÞîŽD1!TGMÐiwGbÙ”©9½”j¯Hª ¬’·—]D¼÷ÏlWIUjÙtË@Q½ÇÌ•KûâA„/ú}Ðo{(/¯pYúÝødQ{a÷®Á2h/™“—fŸ›MÎÌ †…T­Qòèøm“’~­‹uÕL´ eÛ9¿m aó…:)Í ðw1¡c´$I)'ªÃ«Õ¼CÙ‚ÉÆ˜Nd:'ã2y ¿s©oXâ"Û—i›jÆkÍ&‰÷Kõò˜P‘1r'Q±hTPqjÎD)‹¼'§üG¹S탮ÔÅ~­c«©~Ú‹lz]¾¸˜l2Ó™>˶¤ôg‰Q‡À×XC©Íx5jË~ QÕ½3ÛÀ Ò=¤’Q!£²`i/%£'Mððæ‚F·™Úê­ èLX”“IB+»fÙã2´±>VèÇÚ‰¶ ¨8" ÛÑ—.†,ä„§¦˲u¢„Ó:üšÒFÃB]¬‘®Ú^³@YªˆÒ;¼5ÖÆ¢•”Þ1]›ÞYŠf¾ðdAKþBMCÙ÷¼dè&újop­ÌèŸóÙgmû•K 6¡YÑd±5SØÝ¤—ÖƒÍÜó@•F·ìeþ>T„ºíð¸È‰g".Ø9zœ‡Ä:hìæ‹ÉüµÏåzRø$½À¡"\¤'[äÎÂT–,`P;Ÿ•^k©k6ÜjË1|äIE`zŽ=7“Üì9gw1úGECß­Ÿåïóçÿ«Q~¿ÿ¾êKT¥©¨ŠO=²§™Ä|`ÎÜËüióÙIb>ƒ$¡}_¤1ÙŒõ8ˆEàŽfÚ+¢ƒ@ãhˆ;›QH’ü(‹ }K£%W ¬gæ2½®Ô¤ªáò²HIQÎÇùŠb ú]†"rÎ dæÌ5‚¾’êɀ/+êÈÌœTÓ”!-w7“áo$å@©oÒ3ju/¢2•öp­Ýåé 9uÄMgõ\Úø[XFPÖâ(a¿Ó@dq 4¯A$Ç«bÕñ˜[ˆ`!…püÿ4|Zì;å X$Ì\Æ6Ó,ï­J cea;Sn²0=GŸ'Ø×Ð{êérüF “ÆËë ò¼yÑý·©“~ pþE€cø&§fVSh&¡,ˆ”ªüµˆœL³ú™1¢›ì4 d”§ 4èA0 DùÃwÐ1ìt¦×2-·55ĪàÍeÚPÐï4º²Ä› úC‹M×ÖÄåT ©Ç4æôÆBàè•2WiÜî+sÇeÑbèZ•™Aò¼¾¸KãÙpšPì ¥î’"k¼8?‡Ù¸K÷ÏîRòÌ]VdkP}€ÛWnyÀá=‰N’†µì&C©TÕ më5ÚFˆÄ ¡{%k§äX²t"Ù3k©ýÐ"ô3ùîìUáÓ –ç//@»Y—^o; ñø7—ðøçñߪÎ%$3Öa«aÝÊEz#‘^œ–°N·ƒÒ ^)Ñþ:à‡T¿+ö˜Lx 0®¬(-ø b…± Ÿ›(Ÿ£É‹ì; Gëѳ*_[U»™Ó—‚Bˆ`‘'q­ì‡‚úP i•d½Ÿhkíý’Ž%ÇMˆ^¦*™f~G‘À‘|й+öA™.ªùZêãuŽ€?¬Ø–7_…P ‘Ë¥¬‘Ó¦^ö¡´¨}òX¸Hé'Ÿ·‚,?ê^&_m,QœÅãT„¤u6|öî‹sÔZw‡U­ªê¥w(ÝÖ'jej ©6ƒ%1M«:Õ]…N¯B£+gÊÖ¢€ëft&!‡xCªaYÎ 1ášv.LF‰Õ¬°’¥~È m'.˜@3jnjl\ª®ò2æV:Þ–]Õ¬J›ö²çfvt€iµåÖA0­dFvÚ"ýõ!Q?úø¦g÷ø=  ݼ¶Z/2Í2&ÔuäŸÍ‹­(£ÃÔŽÊ Úß·§¯:å›f|“ðG›p[BÊÁA¢ö’êFÆ$ò¢Äm0czÕ{ÅÔ¯$í5ê’ÿ@ÎAÒ Õ±¬ÊêªS=鉮ÔÑþ§¿3ô§½Ôwª€DIÙ“î»lÕml¨©*”„‰}M÷(…HÿÊÖ6&Æý}½Ý͸û×T—GóZ°*~ÐBÎ'Îÿ¡Âù…³‡ÀùUö O ¾$Í”‹e! ®P˜UÞ3S8)7j­+v°/ßÁ¹B‡„• 2û ŽÄŽHôH¢Þ0ÁÊo,Šò˜˲½SÁf³iÌ~¨±Ó¶":Èä[éVYϦÆÈ|º¶Q¬ö<ìÿ+ópþÙó NB—új¤…_Rôõ¾Ç"I(ÐZó&óôUGµ]‘iZ ‘$ÊÃWpåAH%ƒ&$È¢0 CAg³,NgF»ú‰f"v-ÀuÐÿjy?Ý'Sá¦FŒG‡úz:Û[šǛƗaLaL/³/‘ÿ’]ÜÂ_r÷òßÅ=ï“{Ôæ Y”’¥´HèF³¨ˆ¾"Jâ‹Äºöîå4[u‰»–Πtµ~Ð:ð²)kee1Xˆñú‰‘Õ}=‰Öeàm¥‚SxÿàÝ‚M»Tz@TÕb£d `­Pµ üÐ/ø& ×82—+èji&jì4ËêMËk²¿¶ªWflÅ@À™éµ^d÷VÝ‚Zš›êëjbÕUe¥)ˆ($ùŠ]ÑNÈ2U+Ù.’J‚j*‹BS–±÷Ü•­E‹ýÂI—0‡ÿÜ;“)·} êf9m+,µÒö¢jh‹ìlíã(Ã_‡S8´ôt{XYPÃk*-™Çl†+ ÈÄ€¦dY3Jªü¥˜H}P,*0Í+à/3@Gs¥Î‰i apÉ+ôÒâ©Ãhxõª¾ö6R¦-Z™…CëûÃa,u"R‹ÍÄӾϟ‚Pö(ÇçT„‚µ>wƒìÜÑÓ:^ã©•—ó~1l/ЇÎp¼¨vSO©·qUy.õ­4|ƒMýª \ "Ñ™í\ua½®ôä¨Ââ/$õ‹4ç.ã~™MÙ.•b‘$–Ý)a½Þ0–θ © \§×I¤¨Ù5ƒ²F“`‰cwõî-L ‰µÚ.gog{ksÚÙ³¾ogoeoþÏðË–uöm‚g¸B _WýíÓ¨>uJÐL%v¶0§åø)P¾¼n4]QF–Ó‡+uï¸_ þL+)/ê¾Ó0L,[BËï»3¼ì}ÿÂùO`Ú yôi6 <EÇR[âÐ"핽PVˆFR!y4$Eù@!18?âÇsëVnOW¤înEmuAøß©w.Oû4¬“*<Ä8ÉIåföÏã¢aðæöV{,’"ÇsŸ¶ ÌðI12´¿¿(ê±²:³Û6Ø/Jn]§Òë©zïwF$ý4ùÜ»š=«Å¨’O ­‹>ôw kfWÜ ÖcžÓbA5ËIÐYb@61é£S*,«— «öLT¦;鈡uWî«æmûP_oOwWgG[sI˜Á| [Ü© !š% ôüàÌ,¡ËJRë–éðÂ@C¨òÓ¢Ôü¹½~E,}ò“V‘i—™bdpO_aÌmc3Öé`ò!òõ+lÚ-ˆ*¿tx ²²âNþjG¯'$@—!€E.•+VBŽ×œ¸V«$™ˆXVXI¼…Ó±êæA*ò£u ‡Ûåt‘ËÌÑè‰v¤¸ÑóX=Ë“Õw¥šÈhG퉶֖ú:z‡l/Žø¥öS¯á ™N¼kfÉD¸_2´_÷ìu¾––D¨Ì(ï¼Ê1§}½ÞŽ}Qºlþãì’íqéÒ7V<‰“+S;§©ýò­€‹)ìPáoFÌØHŠÜ€%‘ÆËá!¿ð¡¶{^‡D$éEi˜ÉËì¢ß™Ì²4©H18½Þ¤Ï¨x­·¶áMÆXÄ¢ÀNæÔ0ZjRfpÏ9dd8ã´VzzÑ€Wè 8%‡_§ÐÔäúuTɯÖëjk¡¨%Y”¶EØ]¢áߺSß»g6”Ÿ“¤æ7õ7Ö˜“#ßZÒ]V|€Âs@­‰`5/ìÌ¿ !øý„tÀG–w s[¡bËè yp—¾º"yx$Ó¢uŒ.›9ÌŸB~t ­þÅ‹˜M®Ó||•ˆ¶À£‚šRI÷>Hrª &§æÒ-ÖLþþUš«*Çü6¿â : rðr»ñ1 Âá'|h»º¿•@øAQjßóÈæÞS­GŸ{þñò‘#ȘÙöKo“½ø¨‘îÅ>¸·Íˆß½ûoNºï¶š[gÖGï©&Œ¤*Y¦‘ÀQû Y8~‘fFñ•WLöt™mƒ”À"gö+ÀšI·D×ÒKµ†D1›9x|….¤ðqJû¤»-ßvR“mq¯«%.«­àM´Ž|u­“üT–È=3*ìI–ÈñÂ8M­*é­#©U©é9ÕzäSŸ|"ºÍä¬Eļ|ٔܿ‚Z©Yóà>CIF8ªfAÎÇÎË¿åþ8j@/©ÕèäÖ .ŒY#)°¸ÊzÀ!6UEµŠ„cÁÆ!yÆ,ûÒh`X–•T“‘fö“ã ô¬|íµõÑ(9s~žäT5 ²STC7 l?É©ÂKÏ ^aÁF+nzEößûuѲœPžCg‹£·¶˜3÷'7iJÛƒF©eóÍ 3_ Ù9¢¿´šsY&J±ŒMí$”E;•N®Ú’zíDÛ®’­É§`,¶ŒGp¡ <›ßÌú7è;°dèÄF‰(‰ÅOe‰KA»ô’Þ ÍÑííì$B-)J'¨‚DWFš`k2¦H¿cAï¼ÁìQ00eŒœßtÀH‰Dz cdvÕ!4‡pi©V½Ý‰Ö¦†ºZÅGÂØR‡ðJyˆ¶:ŠÌåŽâ-ÍP<>Ð\#“\«À¦?f¤ Š…4ù5PÚ¬eÄ/­ÒùÃzKò}ºJ |»¨Óó%Ñ[ûæÌ”@’ûp˜-Å`KP_˜_|b.ÍtìåN^?ãg¨ ¢i°áäFÌ AÌð…´˜šöŽè1Ì3D5‹ êI~‡™ø©#ä(Œ§ T=’ ıË‘ƒ!‹».Û dâŠt`× ué~Ô„s·4¯ŸéîllYWU.È·[%3­Åé°p©KOjkZÙjz‚±.ž}PÉåI>•kEúÔƒX¸&Ñßèá’Y—¿$à¶Š·¼r`òü\cÝÆ›ïÿ@Ÿd´ºŠ7·4ïZU^;ûÁíÆpžÈج·Û“ד3×Çë×5lÄ%ݽß:Ýzòh÷á‘2æõèPGCC³?·²¢²¦%Øuó¦zbª®uswÄ!Y‹]ùí¡œÜ`ßÞ¾ž;æÚª•ÖU•üBy߆ʵ“¥=ÓÑu’Ñœ›Cªè+íî÷Öªõ’a?ÆþU¢VôQ5wÃLŽÐ×x$bˆè$Oø¬'“jÃÒtµi¢[tÄæ#Çö4X›Òõ§)Ž*Ru˜JGM­Ô¤eUœóºhkU+Ì®¢¸ÄJä4‹CË8%‡áVó'sDîc‡ Ç.lO4¹ªôE)ªè®mÙÖME‰®Á²î=¡ö£Ù6¾;䔂ɛz×ÕM6vî,™bpiçú‡÷4[½ežv£)Ï›[Ò¿½q .`­ÝþÀô‰¿j(ÉékC=ø¶ÚÖ£Ïî½ã‘—#À$ä|¡‹TØdIžY­²’©L Ÿâ‚Ç.äò;üŠ`S+ª,ÉÃyþÅ‘eócÏÎïZ6–A>ò~áI:KkldLyq7Xƈ߽hBºô„BÁ€ÏãNMK¿Ò´èN6H2Z¼6¶Ò$ñ‰Û nq·d<¥÷OþîãËOšÿ¡(LëõÓ¢ðmªoš`þßøùPM¢Ê²…”C:Ì‚þÞŠPP*2«%4ø.ųõ«÷¾ØÔó’dfËæµúû÷¼˜óG‰+ð|µløõÙöí-.Cß‹}rÓhåÖ鑼¦—îü÷åï?áÊIž?r}Õæ3ëç?ÎL>¼eUÒÍ­¦5a¢_û%PNøI"é5oÓ§²Lä<=Ý FƒÂ[ˆ/‹3†Æ·{áØ:|e.W©×n÷F\®2Ÿ}óRû&®Îßí¾²œœ ò³œÜó‡äEúÞ|T…î¹jÕ{Š&Q\›SõïÌ+Ñ¥ëß/×zA%|'Fe„@²«áë¯X ß¶xÉJ&LY«W2Ì—â{™H$€$ ä°4êýøöM€·;á`à‵¨çÌU#úÍÉg¸ …ò†Ñ@¢·,E;åRÏC#ÇäN‰ç§Crãê¡{í@žBøfh #Q_WQ£„ƒuA‡ß`O[x©òé L›VI5n[LübhÝ9$¯Ýj®­Î~î©iæÁÍE7«·æ9ŽÞf66ŸZ?~´Û% ²®p•ÇUœ¯8CUwq2ÿª+\K.â"O\ÅVöó8nÌ-ó2¿ºT5|jcL/ÑðÌîc;›k·Ü=ÆÀ‰ÜhÀa-;s«‚ð³Ä¹è3Õ—g’ð@‡ ¢‰×ôô¢U‹çÒû˜Ò§JÌ$ë:£Ü€'z•ÅÂFÄL–ò¹ÐN© ’j³)˜ÕUÿz)ïÜyJ¬ÚÓå¯qÛ¸|ÿ@¯t۹݄4¼e.w…ÏÁ¾ùÞ?7Í EôÒ&[Ïj.W¹Ë½„`rrÊ áTz‰§’OãO°?@6Ôš^ û)ìÒùËÚüÙÔ¬Éc6d« rY3N…Þð'N‰eC‹ªIxÓ’c]=$ÝÆþà½_.qyª/ª™ý<ȽM”Ë îXЄþ´‹à›T@Q^zÖóŠ!üñÙ«D”>ÍŸ½ô¥£géüúa2/Ó½¶h~*oüIs;P•¯x,¸æöxòQs"; Øý˜Ûrr _¸‘}3yƒ¤Ð¹°äèYs£LŒQ¥æi&õ&Ͳ(p¥Ô®zƒQ"¶rƒífòjqÀudð…Ž{ÏÉÉO gþO®±‘98ÿ“ÎÆöy/ó`ÍXcÿ‹ZÛ~õqGhÎÔ+è¾4 ‚AìÎñdAUð>Åþý9*!ûYäЦ$`wQÁŸ›þÀQmZDëvJþêÜEZè‰Ã üôªKÑ”¦kù†å4pÆ x/sËvH# íÁtSrf€Y·\5’2–Sôá/ͽ%Èw¥¶ðNP~‚Èî¹~§Õ‰™Þ ÛWìrICRíú›GË{Ãy:—Ñò¯Þh–GwôiYÇþ@±îî¨ÝzflþÜð© 1ÅÚï4Ê;g™ÛHÌGÕúìß\ÍÈOkÌk•9]Z•d^§(Œrs,~ů‘›µTrÆ­reÉzâ˜á·Vß:YuÓ!òïÛ×=´«qôÜë‡àgÓšs¯ºõãSgî¼õcSgî;S¿û±mÇtaSý®Ç·Þô£g6«zÞtùÛ\ÌÁÜR>–°Ù0ÇÄë¡\@Bpê…ŠÄì¢j¡64FgÐúxœ:3ìM] ±Bk­œËâK!µÄ9++À´ª¯¬/-ãÊ †•ûH™e_Öš—ÓÊÒû!(LÈ`šKçô­{·—8ó;Ï”WŽ=øÍ£Ý7T‡$‹Õ¥¬¿-R9öÐ7’´)øç­S£wn­Åxô®-u6%YØ$'bùîOÕÞ?yÓ.lôEM6#ÿÉš{×Ýô·ÏlÂOW{™¡²¾[žKÞÜwësÛ¢k¼ó#SR£Ô5¬BÝä³Û,®ã™Söꬑ\¬ŠÔì£Ø‹O]{ŸZÒÕžÞ¹¿á·|mSûOõ(«Á¯WïÜ4š_P¼¿aôÔTÕdò"ŸŸ_ðVb×@ñg_~òÜÍS5µÉµÜ“sçÖþ¾¶têÜ‹שgþ‹aÍÿªÕ2íH´e•1]¶`é‚Å.SV±ð49»´ðAñ_¿0šÜº+~ûƒ¾ÖÉý'{†n”³¡|¹jûÆ‘ü‚ðÞú‘“SÑõÉ‹Âæû›·u‡.¾¬ð†EœÏ Â2fȉîHzYµ“IMèºD,«&ôÂbÐj NÍŒôÀüÓõžÕ°ßK‡ör¸D`í¦¹úŵ†·O®Ê/uUÑZÃÉ‹åyøÿ5mé .*,<÷ÀZ<Äq©’Ïš½ô} 9 ©in4¬f.™i©fµb¶Y¼¬%­tY—]ÑwŒ¨Zsñ†™ýõ…§ßh«% ö'ãÇ{,®gcŸþ§d;ßxù·¬Æo$÷ï5‚OZ@5é WN¸²¯œ¨¯ }EW¹r¢.ææR€y[g1ÉÞªîêÊUñÂ`ëê5«[ƒõ³Oo4Zªä[¼Õƒ±5 ‰­‡¶&j¶Þ;±éþ[c08sœîÒúB_¬ÔïWµ•vßXßQÛ‘›çÍÍ‹¶ª[Ë‹k‡[[ެ¯mŒ‘µ%_ k›@/«n6LSàúÌF‡Ø1zÑDîâgÙ¹0W¼_Âõ'Þ/áZr¿Äèpm,ZQv;mŠÀ¡ <ŽºkÑýËÂñê×K|*Ssmèúâ•`YUN¯Y¾ëuRsùò0o[Š¢¶ë64 õKï”`/ÿ–ÿ(ÿKt„¹UMG·5`‘? Y‹9v¢€a8®C;§’ý•”ùʾr¯ÐJ½²ŸêSO'Ó5j$r7 "œ€L¼`šÎP¾‘¢›ü3ÓZf̘AÛpRgÙ—K*rðÿÌûÁþg¼Ýýg¿]-؉t¬Žò¼¤c¥E·ü@X;¹t5WàÊãˆÐ‰º¬Ž’è[2I¼Ò´œí¯Ê'm(-ñ¯\“ZÍÔxVS–-¼¨+°úÕuñ«i]\¾ædé²ì_Uæ®Ü|nËøuÝ.1#Ëå>]y‰;¼/‹%ÑÛöî4÷Ï™­ŒîqƒËnæ92ïÝ;,Ìw•å´xź›W»–Õâ‡u­•ËI}gßÐý»uS‡›ÿõø¢$R5/ÑÑ^möUïk®ÞVnÌS–Qû-‡¦[ô Ô>Få—-œ?ŽFQ¢§3B.¸—ÄùˆÁ4G¯qö†—ƒx*úù.ŒV õt57|…ù9.4ŠGui§3kWxÙÍÕÖ¤iÙ™I­,ó­õ·Œ+ºF»*J‰Ò–¹{V­¿;U -ÞòDeiwu^y×HWy´mÔ^ÞWÓ²g¬^š«ÔEBݧ÷Ø]¬îiׯSøã5£Û*Ú7vÕTÕ|%þPóTÏØíª:jÚsÂ5ÝÅõa_l¨­ymG¼uU]^}y~ûók.}V-ÙÇ:l=럹©_•±'æ §"rG©+(¨õQÒuSæ˜QW]Ðéçl‘àŠõOÈ5|&2á®Ó{É„?¤f%Ås,ŒìÈWJ»jüº¢Ì46ß6UaЯ#5OrêÖ·‘ýšË¿e^æÍ(BnžH„C…C…ÜDR™QZNpH“mBWÀ_¬(áèŒÄkRws3©ì’}¼8Éþfy¾ù äýÄo·l,!¥¿~ßăÓ-Í .EonÛzð+‚#PÂÿd·õ8p™Ùš|ùdè.ÏÄF+e«¬Œ—VÌ©ç;ãàß«1á89GvóOhLWÃ׃‰¾°ºšTqmUê>ÒÖVQÞ/o¬hT×–¼´¦AÀéÀ©u':BX/’‘QЉs<‘p‚ Ó´æ‰=Ó{ßéF;Mƒwè»jªK‹É€0¤×b ¤ Fÿ[þzœ6*›êÏ:–ÀÏsMÉ_å,EoÏŒbg˜'>šÉøö"PÎRp¿e·«ô¹l÷@Ÿé\®2Ìbx,¦¹\™œ•Ë•ºIˆçiÝmSâ@ £$ÑÀ¬ÞD3µ,Í庆>FL³ºµø¥ƒíJ×Ç¢É\A%LëjÓúX+VÕÖ ¼\)2–ŸI6öéªÊrÂyvM×9‘Ø¿ËÜ¿ÈâÇVsN’µõ†šÒ¥\ •öÒ|Z¼.Í•]×Ú†£$ukQRM÷…5ïK~–»?‰ÚÐ.ÖNqäbŽÏÃZÙWfkê ƒ dCÙ>Œ×„x–ܲ2Gjã“|]Ó2™H¼†eõ£@n”œ™QI üNƒœ½Ó¬b«#5„ ðËÍpc¥.Y {Ö=YƒኃHê6vŠË² ïÃhd5ÿ±êheEYÐ_T@Š2k»Ûm¸Í¼dwûj¨¯°û;ibíE“•ƒcU³ÉpIæÓ°_L’µvÉûå ö™Óæ³£,èûìy6ãk®P•Çʳ8CQ3œ§|ÏØêÊíPÏêÌ¿ÒÖ´<1Ó=ÞâªÜ¢Ù<ÿin4èPÔ]§µ Xã=.2&ˆ®OH¤6’°“ºá7˜®Z$FZ^%É»f5s‡Ãé38+¶Óªm©åê9mÓ*ˆ‚*YIv¯È^´\ÜLŠ%\jì—i’èÊ¡òf?È›JXs+šLL4…Q×fÈt€#ˆE â•ìgµxõ(OënÃéMê5!5Äkª++"%Äzðä¸]N»b1›À0lÅ­2‰ß/"ŽŒ¦Z.õoÁ3|úàsÿ!Z!Ÿ¬km)þÊ*5“Ó*'ݯ‹•ç8t:_Zu²ãÐó@²§/ý@SuÒ¼pݸʬ,“Ü— (”‚ø Åè»* Xú°Þä—ÌÌŒ*%¶›øäjÖI¨íK«Y7ˆæÏdUŠzݪV5K4Kq¾’•{õ¾©pÁiRþm’A¤ƒ¾ ¤ô Èá {;Ðù„1leX®ØFT‡¦qÊt$Yqìœ l0*‚ý£f™1ÍT»ã1=I:1"ˆ^¹9É¡}ø1 zY<=¨ß:ÚÛš›ëIqyMH¨õµ£:*"…pŸ]Š£v"ÐÓ8Re´†¹åp”Vcïú¾“—ßaßdßD9hð5‹–ÓDÂ06zÖD=Ù0®]››p¥žRöBˆOÙ»$ÿ"å¼?¹Ñ +¡ÍïËÊbÛ0 7WnÛ0œ×üâ±u¼iäÛ{ÔL öÍù‡ïNÞºÿñREÛ«'NUoºg=³ŸÌ1y‘Î1†^×jC”ÂLÁîäó1ǰ&̓ۺSOÙ¬§šH*Љ©Õðã½M¬‰æç)\*ôæ Ïð`–,m²óE`„¬v´¸ãªÝ˜i;©Ú/1 þ‡ŸžNÊÀƵÂQïAæZö7»" «ØeNkg7ƒóy€¯uá½ÈæÔ½Èe îEÎ\QÜwú3:=cÈUp@—§œIzγo:åùsÁ^¿¿'À1;.UÐw ,Âþ•“}ÄPöÝÈæÔÝÈ%Þk½Ùfɦ>½xè6»‡ÝÉ ŒÁ#ãÁ£ôÛÌqÁÀê2Ö ¹Ê‰äš;ÜÂ]ËÅädÏ öM»2ÿ ¯½¨(ácŽÊv›</ùÔæc“ùz™ó…E–"Ëü~/…Ïå|nøä¡‚D®ÃL2ÙRÒ8£ØWB.JÆlê¢dÊÞØ¦Ý”Ì’à^óó۬‡xƒîû’U~2ùü»¶9…OëL—$·õ1FÆ_7˜’¯J6 ûeS²yþ»Œo·+ɧe›Š§ÕÀ_‡y8HKß•lÖ¶‹Ë|^rW2ÅÆ.ÁVÌ^|ê­÷t8¸Gy‘ÑÃ[Êuyæû’oãÓìcËü“öR‡³ÔÎì°XÉþ¬¹ Þe&k¦åo‘V 7uU²âPìdÍ /Iö²~V1Ìÿ ÿ@™ß¬c#ãMÖ[™GÙ»m~y¾ûL¿lÀ¯ž!b+¬y—¿…A†ý´3èf²%Î’­qRüTq¼l²Ù)ÐtPµ¤5K1Fåeű’˜¯(7‡˜Ó’…q˜Ó mW¼ã•d8…ir-á>欢0¢»¬²¦ uØ?vÏ Ïj½¾<³l欵G:¢½å6¡üe]~cÛº¸;ÌF$%ê(t:;B} A6oW슬/Ž–ZFÊ’®íb”¼âo>µ©~Ųþ²!/*I„xÂa ^G3܈O‹qØI’“ÝëðôȆmBæöÚ”j·Ñ°±ñeüõSmÞŠ±ëz¦Û|•ãG¥‰ñ––ÊŽ[ ,б©qøÄTül‚ŸÑ¤|úPßh 1]×7Bæó#¶Œù7~|1x͆9šuG2×ì$CíAüËá)’¨ÎÒk©ÁrÚúcn©Áºa÷L^T¿ð­Z´ÎO#8ÆÆÂôsæß6Ë ã|#ÊÛä-ؽÿ“÷X–%¶ÌdصK’™¦äS/H.£ËIa•d÷0áPšx(!å«4AO‘Fh%]í$ aze¦¨^™©•:§WšŠ`±;‰h%ÝE=xX ÏN§z.l?™ÈËÐU(XTàq;lfY'Ê2¤(+ôb²(«N Ùû³I«]QX!Ü[ïÈë:<Á`±Èï ä»Í ¯L.m ËàšÞ,ªöªÚ‹•|öˆdñyý E­±"Æbª®”-²I_¼:/Ö1%ßÌõŒ3²«Ð–ã‚ùÿ–Ý˼.ô}hÉ8æ©f !M*xå‘„öÝj¶(M˜!ò‚BÊŒiº(ù4æxÄMg9™]΂<§ÏåS‚!Ÿœž]3W»†!fó jê !NüTayûjÛD,³êXÅ8“bN_Y]”nÉÃÀ<ž^Ò.k0í*½TËL£ÉÉÏü¿Opdöi…E;“XÆ Io¶ç;Ûz+#ŸÛj0 yå;«ÂÑ\ç–ÖºÁ\î’¨3Ȇ٭-]²"Ê£Þ‚øh¬mïHEÀ‡4œþàÔ²bBV0ÏäjÿŸ¶÷«ºÚFÏ>mz¯šÞ«Fm$FuÔeU«ÙV³%¹Hr·ÜäŠÁ›âLš Å“„„>’BhI>’JØ:ú÷Þ猊m ÷¹÷ÂcM9{ÎÙuíw­½Ö»àzLÓ&;°+uËL|4ÎGw"_(<åmB’€pé›{¹'e4B Èn5¸nV¥€ê‰èÑQ†gÖÔ2ÂṌ#wÑt.Τ«5·²Å[Ñäè\‹^²g›l*4º!j£1Ï›¹j°f¼3;ØÉXÍöŽY?º¶´¯í`Ûn†ãë%2‰{RR9a€£K £ëƒ§cŸÇ铘‹¯G„}–`úÒ¿ºp„§ÍQ)<{Sf¿¡ /ÓŸiÍÐi”r‰ˆð/ÏŽ'¼ûb¹¹H“‡Å ³HðAû0#Qй÷¹–à ‡ÝnŠ˜£ó þšÅ¹±råÏ(Vça֮赺4Œ¢¦J¡QÈbp%‹íɮ²•­±…É"S؉öÞ_SI/[AXˆ1ž9yú‘qР’÷•pM§Â…)XødOÂ{6Þž.‚¾A«»½aÚqXWmOJÁ;Lcò]‘>j%ßEÔÊó)Ô$[ÍÈVvhl*iÔ{³Dô&Ùú?U 1 ñ´¤³"QÐΗ«åõF¡Cì PoÄî™Óq¼Ïùz&<öêhs•4¸5À:½Öü5 žÛø¼7aw¿ÇÛJô=!Çá<ŠÀ*ÏÀ'#0ÍL|<ï§È+k4{ef4Wb_˜ÙZ' m Êœq-›ÏÖÇ`K?3‡áÜHndiZKr²ÑKV®YÏå×íçÔò5fa&Úϰ É$²¢eLÛ(ùáG#z‡¯€ V®™0 7Ø=Üy^Ä ¬üçe Iù;ែ NoÔUlR[À§2e–7Ó¯ð5µ›¹Ñšù'µ°ïu„“hæE‹iFYªÆ“G"k Óõ›ý}OJ+¨TNƒó[U*,^¿ÌnÊ·…ꆊÑk°~ˆ-­ËŒ•ÕǘµÎäüÜšµ-øšW _¹¾e+“•ËW&S¼Ìy‹ü5ùÜëf[—R¢Ý:âpà–ïâi_Ònù˜‚GÙØS]øëY"ŒÃÛà,¢éž.‰BœtF!à6ø(=\5‚[¾ý“¿ÕÞhq·Æ«Z|•rµ÷{r!b ™ó°Þ7˜ò|1³NV8¬ï˜u£ëJй÷¿àõç„v­ŸEÁ4õaŠÌí$fò¼&ü×V mC_ ëž) .h˜.àEžúpM&æ ÑDaœšƒFŸkŸ°:b<æë¥bÓ³Ø6eÈTZfF&ÇþìôóMs[í&n¹Ýö³~ÍHi®…W¡L‹À¶àZ±$9½¾3(0=ÿ«™™Q£…µ/C0†ÝކšÖ‚0¸½3k>3‰Xؼ^-«×Êi« »´zZ#²È½YºZ¸JMI¬@Õ§f¢î%X·g <Îÿ8±4%CU'ûÄ‹$šªÐ¢ýõg¦HÏ“1¯/æu#ņÖk.ta*œã6’)%£]ïõÛ3ã™v[Ak^N§Ù£ÐË4F‡9ÄkÉ—“Ólbh•T.u[tF­Ö–é VåÙ¼–,­B%Yô&ÖžðVdÛV"fí¶jjUc%0(乊ƒ†[rŸæªeÅFê¯)Ø_Ï3#„‰h;£Tœ?A»@E¥S.ãi¡ÇÑk$h™ý5?{M„ÉçÖÍØî yÓ}öŒúdqø,qslnG$éjÈ$Ü2&ÀŒpÏ#VsrcØDò”£:¾רaµDö¼Y™¯¢Šr^U¨g¥˜‚j%œC=_hlæOJ¼ï,…JH.„ßbl£“ ~¶†QH¹›c&SÌ ¤R'Çs ¸áÞý|n”H=é3ˆ¨Ä¢¤ÁÜÝÚ€ÞÂ¯Úø­ÅšП »£%ئ—ðìzäW«´%šb£ð¹ÃvÊWªH)etò À*Ä‹ƒlñ«*“&i)³Q¬`Flܤ1¤×‡Œ€Q(¬Ü9}P§ è­PÁ~‹]iWrï`?Þ ûñ4l…(OIt – gúR1K\¡!Ö¡Î$‰6¼Y#º5B­'Bž ¶Oñ&- ,FX,ÏÍoÛ¨”ÑóÄ’…ríšÇwmT*èFF2Èjå«èMŸJÄßQ©öéþÎ}©œR(ˆQ|ûŸáþð4¬—ž(:£`HÚòŒŽrÔ¬å±WÆ^‚}<âñ c5ŽYä­ÕF¬¶Uo“ê4½.[©T’%,%Slcô²ò_ZÕOÁË*17eËpX&¥Jt§3°o–À:(‰*8ÇÀt¿Ò6k¼-!ÄG`¸ C¨â…žYßö<¡ÑOÛ²©¸È€³ß›p“=¤\òÅ[2îF˜¿ø ,aò÷«¸Ú £¹–K/%w0› ‘=ŠÝ$ùy ·pš 5;ÉxÕa“¬Ô›d3›õ†¿É•_ z0¯“÷°çà=L‚Âf04÷&þ0<î´+ïÛ®7)>cÏI Ü0"OUXûq=ÌÈ÷ «p÷ãø, !ö"”ˈÂm‰¼ÿ_J›~ùJ·“Ùü…Jö7ƒµEƼN1¸Æ”@Ø~hæ<\pêÇ)†{ZmÑoÿ›Öí`Ïq)Y 1H‘ %Ï2+\ó"$k„„Or¼ÈW!{š¦R$V¢iÖ|­y!çI0ÍCž]/Ó1ƒ"V} k”myÿ6zTÌnITÛ ‡×y¬ïåì›°·a›Yü°™ÛjBÓ7èg @»|‡Ì)ï*6j]²­ŸÝȼ.ÑÔhê%(DŠø=JYqÝsÎHÒ˜Àñm+œXN²ð+Ëû-2-3¨`ÄÌneƆ÷oeVH˜:FJïPr¼Œ‚u¿w¦î‰’pW¡îøžü’ ¼`h§Ì!é–(&”.ÙÎÏoeßdÔÇ5šk4,÷rÅ¥»Á¿™#„âÂy(—   ¦Y¼å¼©‘‰LQ¯Ìn*íeލ%[QoƒAŠqýô~ª–YeÎ8¿…š±NÐ{•Å‹DØßéïç$±µPsäµÔhÔˆ¢Ã-rŸÅ‰éÝ:~Mõ0ST?»q-¥°@C,ø¼’µøGè ¬ÿ–své2ôŒ¯ Üü\óBXë*‚¤ÑB\HñòÓš2Áw$…HuiK;2’Ø O00éA¤4›Ìü®¶7W)Úz!?½4R·¸`6·>®Î×ù,:s ¥ü_óP@(äR1|¢ ¨fžˆ[\ HOj¨ïìë_ד)…Û편Ã~Ø"¿È…åÀsu»èÜa"žÊù/när¹Â®º‹ÙѾ©_˜4Îx•×Q4ÂWŸ#?hl zÉ;³|ʽ«1ë·?¯ó8ØX¨*Çj¸‚U9C ªe9Þ¬2pì—rÑsz:Ç_K/¥ÞÂØCGxS.^¤!g;,Ô¤R‚ê¤Z•Ãv6qÏ@’ç_V8tõ×+ÝpõJ«¾ý‡j'B'j)D'_(åÓг~̼NobŸM…ðStâ—–OÿÝ3)ÿJ‹¾ýVUÈž™T;ô}ÏhÃNö÷*•Ô?R*à_lÿ¤‚W˜ƒð‰ZlÁãœDMȇDÊùBø½7–‘‘‰ÈÉxÚ1F–¸¤Ü± KL !ÀÅü’f_ƒ÷D²ê<ûîEoLÒF_v†ÁkU}1³ÁoU³c–,Ac÷,Ù^ø€¿-ç×RÂF\Ê›™eJÜâ2PêT”×:ý éH–1‹ hÀþ\*Þ§åáÔœ+] /é4LM_èÁâЕUˆZmÀÆÌ v‹‹æÄ¢­žŽEK¬r^4>ìPÛyAÛ€èå΀ë 9a%óõ“ ”v*%I ¤X™ˆP#8)Hj†J­¾:9 ÌFCX•žã¨ åøŒ“ؘö¹Êˆ•ûÌøb >îòD®^>ãqëÜÁÞ©» .Ëê< ðqe¤3x¿Ë°ÆÌk"L¬ódȦ’ÖµXæÉoʇXÍ‚÷$=@Z˜5pßËr™§Ýt0¦PãéÕBÌo<Æo†^]ü꺭+é F Ò§îe<ƒ}~±—Â~íDšè!ñ—agê^-§8zú$ã¹â,„b<ßÛAW3nXº lBª‡tð<{âÛ0€bÈÍëÏtõÙkÈCwLþмë™Dn’1©Í‘2gÛéÛ¦>aäL.|—AÄÐþˆšÈ G8†£ô3 d LA·à†ïÇTh!iótþ õ Öÿ€7´\&È^´óÆÇ–n{þòycß›¼vÛóçÉ[|y÷ªÇ.m€¯]«á+ùç#·­ÿ驃ƒñ7½¶û^ñ‹ñ…7½~éÒ;7”/¼ó«–Þ1^¶àŽwx¿±a_ŒÃŽüêxýö'àã xS‡[à8Ãþl¼ë =~Âdeþ$V0 ›’+{ûÈën7i©ï8ǹ~ußß‚Ï(€(B{<‘ÊGdÍôLs¤Ë±G+Ž!S54M2©DL‹hDèDŒ¤£;ÿÁî¢ tç®Ó‘О;®…3ï7:.ÌxŽ?;~ü89ÿ4O6Ág~ Ÿ)Bí"›š¼ššGnÏéT—tç飓{á4¡þHŸHÏ‘%A}?x‰¬TÔn„K J„à9Ž4lç“cÏø›¹-š¢´¿ü‡%9ö:“tr ?ÀŽtd !¯$Nî›_ç*4‘rݹŸKÈ %¸—û…XLJ©B' •;ݹšÛôrò]ØË]ãöËêP•> †ÒÍ(³*›¯`Y}–“{ÜëGý¼qê+b®„³ÏGd½üÁ´ ïÄK ,„ ¤ˆ`ûÅ@„ä  øÞ ‹«¾¾™Q³Æ 1k4fµÔ “©€7‘EA|p Saªœbà”5é”õ»ý¯nZñ}îÜ­†{ ^>Ü8ò}@Þb¸¬IŒ\¿dÝø6}±päº%ë6üð`ýgÐuøé•>zt<òz¿j|O>0YÑ~tu œÛ:¡ñ{êr²ýŠ3hŒn'Ö Ç(@”¤ŠŸ°QX{1l `ûR‚œ§Q¡BõL7ÊëñsêªÕ'–wíY˜Y²ñÔÚ†u n½+£ïN}ëšCmûÞ¼maýî‡GG»zØxGŸÙIbü><õ D[!"DqÅ“¹p­³ÈLvr Á²|ŠS5f[&º°ïŸ*Éñ %0ÿ˜ƒ÷ðD%ˆ‹@>™Q@$ ¢™A?âÞ’Iˆˆ ^Wĸ<›döYq—#OY‚à—31gQéN¥þ¬KøYw_¬qgo¼ãªï­(ì›W¤÷HË7) ĉۋÄTÅä«yKÚSº„4XÜ’[½{¨ÄSÒG©&;·.f(^ybÉ¡?ÞÜeŽ{Vz'ÿ]:òæºUµFß^ëÖö³žŠ{bÛ筪ÃþUMÜ­4 ±ª…("v§”:ÀRa€bS‰iŸ9Ÿ‚%–¡Øi/!>{5ö ÁÁÿxçöa+Ë7–EÞVf›õY,êóX‹lER1ƒ¼ô,¸Ï à÷ê µéé$@$j†ÝND¥Y©²HˆYÉ`ç®HÑ©Î÷~¶`W{ÐW9Üyyý%·TŽÎ ÷÷Ug)Ú܃-í;»3#Ë‹KÔÊ¢$WL¿ö%çv®JV¬^X®uÞ»eàÞÝMTƒ£b¨¦gitÉ «'÷••&vÖTô$-ûù}eœgjf a‡»m Zÿq@cæ'PŽ(’†ZNGÒ­N‡Ìƒšì¬¬’ìÜ„ HÁ-£fÎ]…]°Ó1òPžèPkñª Ðj[ÅòyCUž…×>¿rðð@lùƒÛ×8V圴(4ÉÁê†K'2»vµ¿PuÉš–l«³6Зç,joñƒØüµU»zeSÝÞïÿtþm[¸¦{òæínJ]ÓSÕŸE;Éêò½¿<2èmëè %k0f>Cý†ú-Ô¸‚)Ÿa)oÁæÝÓ!À…ß8 gÔëg´‘È,A‹Ès­ 6$ÖTö<‰»¨³Ø#¢Bã 2ÞÓZe•º4G‚c;5äC{ËÝÀöë-ÿðÓÜO6Þóð™^wÆŽþüê·„?S^2õ5ɬ'òˆ:b %˃»l‘<¦yÞä}KàxQz5:qM_çYGûf]…@³º2;Ð0_C@ƒÇi–´3QØ-‰JC&êŒÙ(/mYέŽè²š‡– 5g5zz|Ù£¯þ:X›ï/:4èîìYè»+XÙ«Þ5Xì(_œªÛÖ“G¿gR)4 µÍop% ‹k:G/m_ÿƒýå«®éz ÊéŒxIm$¿·&ˆ|êΫëªqkÇŽ ­hŽå @ò}쟇¨·‰2ÄåP€(P¤ç5CîFlN‡ “(®ûààmÓìHú’dÀpÃÅ&6Îí #²ãâCQò|Ç¿ÂéŽ1š¨ËäZ¹Â›ì*¯J¹wn8%?uíµÎ’…uCÕQ^ò÷­XUÐ~ÕŠ’Ý+ÃÅ>uÑš;Wn¾=NÅÔ“Õ”MºJZ6,ï)Žš~‡ºöaŸ~Ü‹ËRÅȯœd¨1v`ÏN(è˜P(ºœÈ!ÅïsF\K†Þapðn)’´[ ™v&†òm 7vKArÑ] \$²äúÑDÇ o\¶ä†±Dç oX;öU('wQ{²cïp…м €Ë˜-MG_½ügÜÿ¼9Þxôµƒ?ÅoŽŸ%ëüxg¼ðµ_ÓÛk.ûáV}ÄøÚ«XδOý‹>Æl" ‰J¢:•"hîú ’(Ú%IP‹Ä€2e¥)ea³RåÅImHï3xƒ>ÌÌè±B[PäEZH"×x^âí¬õ÷ë&„ Xúé´/ «÷½¸Ñ޳Ì$vÄ ÅTª(RÓIŒ@, Tç6îê- ×?±§6Úº¾öØ£Ú;&'¾{cÏ®ŠÔ6RW<ÏrÏýuOùº[ž¬JÖýŃõOV¬lŽ.|ž®ª¥µ%Ë#¯¼zb~-U½¦9Ò†æÞÑ©ÏÁ¨WàäÁgg$»xaé÷ªñùÅ"‰ ägýøE’œªf–´† lEí9õÊd²±¥@kÕI‚uÃ¥xN‹§þMÝû6“è8ã#„ó¤\ìo•QBj§i’S+ì³®á$ABÒhw&ËôûÑÉ4•E¡¾å³‹A¥c,œùj“ÓÚäË…¬Æv¤Ä@a ;ýuÅQ鲜¼|1“öUG彨ÄÜ{"«ñ(µ¨ì!³¬BaÒJÕTÎ9m2¯ÖkR…ë“àgzFãÖLÞ§4ðòHWÈ'°ßìÈN©DgXztœRÅNàóX“*hh´ƒÚ {ùˆôsý 0Gk hDkùK€þvªùüëªzîߺ»{ì?WêÍÔÕ" •XÜ^c»µÔ+Üß9í¯©W ÊÉ®2GóégþgØnæû¢Êæq(›ë‰®”ªn¼…È˱Êã&IA>ë°+$¹ûc·#J{$Å2dÀ„Ø nO$‰€h›Æ’’úºê w ÎÉgíK‚ZrQùÍû'XOZŒ¯¤²-Ý ;zâû/ëVìéÜ>?语NÌËÔ•îVXMJ¹³ T³sqÒ^9ÖÜ0/Pµ(OéPï Øiy=ãtz2›–µ÷Õ'³ë‹rÌúhNÂÑp ¡@Šf™´.‡E¦OvlhÉ_·¼ËâZY—ß×\f^gŠï›>Ø7—Á¾)'šR²B,Ügõ‹Vè;1áéÿ)Lž~a¯œ)/…]Bÿ?ëlȃÚÈv*+c¬m{gôà…ŠË\Ò 6®ªM6géÊaOØŒ*…3©Ý3\ìªKfϓ㧷Óë¯HôÁö%sê’¨ý¹…ΆõšP¦Ð~CQ熖 #Ýæªû®I vSŸÑ»àÇÉ˦´ÕÖqF™ÁÊÍíò–w Ï?´Ñ]¼ê¦Á½¯]7¿M㙸ÒT¾ñî‘ÇRÛÂë`)Z°/ó_ʇÎ÷ª¬ˆß°»†0ÅI¨ÉÉòz¨óùÅg6† -µRéˆUÇ[&:3#mëkójÂÚâüÝò ƒBfÍö5X^\·÷©ÍKÆ)¿Lj¶›KÇŽv6׿™uÑœBǼËÑJÆ3Ùn‘eT ^µbü'ÇT—¢Øà©/©ÔO¡¾ e)-8ðð¡Á|R&ªÆà÷zRT˜ÓhÌ~=V‰Ñ–=³1â…Ù.´?XÌ딕 8íj¥T"FG*u Nʧ¨H ”hñ ÓôAèLøh™‘XÄ­‹ƒg"Õ¹pH]§«ÚõØ8·ò$e,K¾wͱuÛé\gVû†ê¥wm¬`<“\øúKû–£é0Ÿ÷ >;æuèôùOÏ{Ÿ±ô¡É³GNŸ$OŸ¢Óo‚ú€m$DD9oô1P_£Ÿ+¼3N‘rÞ÷=OgŽüà¢{tò ôŒßAþ)ÅâMý‰{–þ7|¤ "èòT bÙdÚѪ–W³ÈÇ?#åßó›Yf8ÍN³ÓaG>ò/üg™Œcqa7š~å´6aBvÔ+zì™<Ê›1:·Ì^‹6[“¥•Užëmí]²`ã9;­2ÙAŠû þý™¢lüáIÐùÖïp|u_­B{ϧpeýÖ7‰j[`&ÄàßÀöÓ¤¿ID$NmˆÜìH(ð{6½VNàX" ’2Á}Y<:/Ç–nvŽ-Œ7ð6TŽ2É)Pj‰Ê¦ÍqÕÝS¿Ú]h mžõJî*[Q ˜4|²9§|I“ÜÕx÷¼á" ÷¢¡4¸\⮋¸£ÚOÝÌ%ÂdjòYKüÌdŸü$«TeO¾è±7üÞÉ»É>o€Ë´yÈâì"¥"/@b¿îì©O©Ÿ0Y7¤j‹pfJŠF±ÐDY ͰÈò-d¥¡|AQ’ ƒmUúé<¡ñû¼!wÈïˆMu±sPàì§g®ÌÖMPäo¶¢~²~YÛâšKÎŒïy1U&6)ä6ofik^ýúÖˆ³tAâ;š[Šë‚¶D{~ÕPm–:Cæn~z|áÁŹ4µá»±ÐÁE½'·7 Ìï)ôv¯]j«Š5'œGБs•3à´vTÕÆÚÖ¢vo'F ÇÜJØS3ö©BÛ¨ròaò/ð²Ñ6ù®+*—åN¾h³‚w|öÉ»±ÃÜ€¡.ýøÞ85j€EË« >¨C`—´Î¾‚ù$âbC:ØÅH=(0X)@#^µS ÐѸóôQn_dAÏÀðhb˜Q”ºjêæ5·†ÏWoÑ'&9(¸ï]âõ®³/¸ÊÛ@Äû¾¼9õû&¬£›ˆ#|-]¡/Mˆï.ÓŽIdùìlÓäk‹`í½QÿU'â {PýC.@:[„Ÿß· ¤eúª¯†¯î‹¦¶=¸†[tѶ‘¯/L~yÅåÉcí뫗ݵ©â¢MEBÿ†Kû–Á½ÇåÓKxLÜÄ!> „%‰˜$EÙÏò#þ 0MÞç8I´ .É\´dÊKDú#î‡{ *Õ{a©žž”Úd2¹M.b-†="5Íí4ý(¸#ÈÒÉ}¼ôâÉÁ+{#[XÍu5NîŸ÷èÉÓ äöy§&ÏÖ,«r ¯ÃPµì;›*È…“÷Ãmîq²µ:8o´²w„%½SŸ0&&Š/ÔrSYiÛ?!ŒšŽÚŒ2³³"!›Õè5yCöì›ëxs¿ç¢J[GÜŒéB¤Éá;vvjÎ}ä-¾œ,EÞI½·lïÖQ5äǺ%MfTnhD+AnIä ä¤T¾ñž´žvÀ5þþíÃS÷È‘ÉÏK³}x®€kòûðƒ ›b>*v3$!1çógúÔŒ¢âBˆa.à¦ÂTMÈËEí4sî6eˆ~/Ó2 §ÔŠ=¦k&÷:*jš"99¤¢ì@VÕê“vò>W¥Û]é&¨-gÖE4ZÍ:\¯«àüšÇ!Òt¡|XïwÅIaa¡€5òF]ȉYˆ<â«‚Ã6ñè½êj“ù‡J&·*ÁªàîE¹Rž¬2rà6ÆcÖž(o 6¸(wo·Z·:7÷ìK(?,|î%Ìb(»PÇéáW„óë|v5ä"„´å‹’*²bÈEÈ2ÇEˆœö¢/æ tbÃã»kî~g¿î^݆'v×DÛÖºc‘î>àuooê¿e ·‹êÑ{JCÖO܆ûzÄTÀ9 Dè“ÄŒ'Ó‘fÙƒ²¸kÚü¯$”ˆ9QÑgœ_˜iµéä#Gɦ£§žÉ£=–Ñ HôiÄÏ·íðYïâgyRN¤`pKtàI+h˜èîøì/ €Æ?‡®æVc,äÖ G=‘Wоÿ÷àšXï/x1#ûb®€·!ñâ0tÂâPäÀ‡:*Àç¿8]·ôÉ[wuxùÈrvþüHó#d±ÍE†z¯rýëbº‰H6@Ùù|ö’dm‹ßVg\$ÑÄí: þc›¿òÃʃ¿}ôøë±GÒ$Ô-÷,ûwìoOå 0}8øÖ¢dQð0K¡†Eá%´x84>hú9éAY‹öúÑÓÇÀïŽ=rô82°ÿf:3:$q%ì»Gð³­È†ú± Éö¥Y5ÐÂc˜´ÉP§ÓYu¤ˆhÔbŠåð^\¹•¤´Áü¿Ý0”]½ûÉÜÒ£Ükà†äG7ßÍxŽp‹²:Æ«—ž¯ ¯ü¾îÒ¾<ï>”o š—P‚o=ȉ Ê& ·”Eí¹ˆ¢¡Ÿ‘xdÓ1KpWQ«°Y-j³Ú¤ÓP…<[”·ûmƒíxšˆìT&!P=ô3`ú6¼R¢‚^eRµj4ëCn$÷gµÑ:ÝDðRݬö\”nÜäÏÒmã5-¡m(ïùÔ't“eL{ªÕõ½,:VE04b6Åž¦$Êf’uÜôÕiò×ËŒðÄyˆ£ª ¹ W¢ŠêòL ‡¯¸î4<Íœ'œScü|·ZnIT¶ÅÛoëÖNžQ“µˆu²q¬!K­T3Ùí¿Ü³ñ{ë†úçákž/k)BáŒ9”å­Û/[zûºR_Ô'··ôtßøêîû¸÷~1>²¸ "!ÔÆ-°×Ã6ƉŽT›bšB ¢ ‘·ÑƒÉŠXB„"ÜY‚a{Òj®žGüé4ÊB5"ŸËaÍÐka#ã .2§/ôy©ÌP–ƒ´ÛG¢ú+gʺþ||ó÷.­É^¸ýú‡/}ª¢F¤UªÃÍ nض@Gv«'¿›ßIs~÷¼j(¶i`´ë†WwÿüCƒñž–v±6¶tü®‘ý®åwn(U:bNæÙŸú”= Ûi…HâZž†Ô…À6KP½¸!|ÄÊ/.Ø“§]ù¦ËáÔB«Ï/› }M1|´Ì—°1DSv[Ðo˶gk4†O-AÔi3µp¦_†•ì8˜¥,aز¨ã÷ýï®MOî©~ø“#›¾·§šû¨þÑ;O“O­8µ­:Öµ£uäÔöšØ‚äÞº}Q÷‰WwÞÇýõ—ã]'^Ùåœ|Ê•e Oþõê¡ÛÖ—õÀ×á[וò¸úK¸æVÂ5gx+/•­F~¨UPëãM4š)RpT÷¸œvDn ºEÚ¨[Çg!™Ñkùà  “C¤óR¦º›Œ¡`ÈÀ½f( ìc®êÐî­•;N¯ןœ|kåÄ{Åj,vnÔæoGŠ4ö[Ö/¾y]T¥öõ‚ß#ÒØƒ~ÖÕŽjŠÁrþ7ÜôVÄ Wam ±CƒJ($Äé4;"i±õ‰ÿ0m¥µÙÂæµy=nD)æFfZ’”(1×PëMð¦Z¸ áм1¦Rß™O9£áÉ„—´f¬ŠuÀ^ZZVVæÜi--O¹À‹¨WÎÚ±‡[–aíÜŸµNоXk&~dgöŸø?o ’¯6ª)v1õ*” gëHS\"ˆÐ¦_Â`‹Yn¼tMAÜïÖøBð©tÆo0Φm³ÔlÛ,¦äqù»úóïÉï‰Ô[€D5ïç^°—sJMomŽe)ï®,¼»`,«Íþ©1/{3¨vÍËóçêÿ´ß‘«*Á}Ü" $ôî*_–\‘áVÛí ¯‰kßó¸_9làx$_!ÏòMÄEâœú„Ê£Þ&*ˆ¶T3Ê,«4cA–Ø*BÄ2¬ˆEžƒ4‰¹‡h’¡û0¯â\OPSVRT˜• {Ý›Q﫱ûÀy‹vÎN¦¸ÀJ-¿g]ç1o~qç@ݦÚs5[J½ÆWØ^^¼¸Ê»nÓ:ùúâ*¯9§)žì* Êåj1µ&^ªì޹ê*jËºÖÆ-1­?(W;üt¤–š¿ÁœYâ³dû &«I¢æõŠ4_>Ô+zrxåñzBóÿ†9Î` °øÜëÀy¼F¦¦¨?c¿º@Ê+$ƒó‘€!ûiuœˆ‚È!„¹i¿fÊðŠ¢U9p^|`Mø¢ú§à¼Ptw žš8•ß(1Q' üðË àð;ì ?õ*×ãqñgàÿ˜úûùaÛk`ŽíUõµ¶WÕi{%•”`±¤´ ꓉ûG¸/ÜM {z¢m”"ÛQQU]]å„K0›jþ÷Ïg¶T›,«LU[Ÿꧠ̀ucÞ„uCR{_;7²ÎTùcÛóŒ®Ñ¯/s««Áã 2Ëó{áƒÜÈöêæf_ç´̈}ZŸóÜ‚-ó\ñ¡«{¸kG¸¿º[»zû2Û(y޳º¶®¶ÚEê"?.h+°hC•ÙÔ+ÜžH] ‰¹°ÉÃÝã(œq„1Ü·˜\êuÂlp …¹éÓ9ËæÎSBFŽ£³,oüïIì}É›ÄÈ»Ä*FfV‚Í*¯öàÄMÝ”JÌõŠm$²Ü‡$ ºNLÒ¸eŸ ÙfS¶$Mªs™äa[mòn¥V«$»l•–Éqêuµ‰ãó¶È•íB†æušcRª´á–“2`#âýÚx.MOœ*â Šà¬Í?ÏA¼ÄÂÞÚðºùzØgñ<õËyû»%¾ìÑ$yUç›™¥ØÈ<;ìÀR½)Õ“2bLê5yÒ˜¡R¼¿\hl¶Îz]ö¯ûwµ¸ –_³˜»´ˆ Úú@ømÎêȫɅI;JéˆÇ»~ ¯qGoÞL‹ð8Àq. á£ðA(w¾„rW‡wÇ‹ú››æø›ÛmÈßÜå´yí^³Io5XÏó7ŸCƒ˜@Ç:¸Ã¸‘£ùªã#…͇žÝ´ø:ôúœøŠû3&¸ë̓§,dõnêí²õ· íý͉þõ†ÎsÇî¿ÉxõäÇ÷W\}'Áû³°¾¯"dƒÅöeÌg!‚݈}·øQ¦¾Üzj„+=5W{»JÑÇõmêÏD”èMÉg‰œEé„Hã,BN•@È©xg‰ô¢F&iÓÌEž‰3JDý¯Îçwóæ¯4iŸÑ0‡ƒ–wE*LgðlܲÀá$MO®[ÚôÜÊ}ëŒ6ªb-±E Ö²牀);˜××^g+AïßuÉ¡K^öìéÉcäºñµ‘މÖÉOÉuÉ®"kvÏžùœdæÌ-Ný°Aä#5À¶ˆážŠšC˜¦Í×*Á|mM;ª n¾ïyBçͶáR/L¤ÍD¼e{Ý•z u«XF]›ãÉ2Ù©ae¶P‰ÛsïÜì«ñV6‚©‚*¿‚Kè%¾È¹áãfÖaŸ0?‘MŒ?á"6­$üÿàyÜÿ­k˜Y¾¦²|ß—½å©Kk|ÿhèöàÖ§öT/¼ëý£Ùà©Ìž} {Wß½>y®í‘‡ÈþÕ÷lH2#Ü6¦fÓC;_:ÖÉp?5Lõ¦Û‡wüüX.?÷xÃæ®\jÒ.e*ÖÜ(:ö€XL¾ÇT¬»ÅA¬jƒóSEÄRDu[IN [¦ƒ8ßö­ñjys®``F¥oA(|jìÔ„ú¾8å0FÎ} '?„Äç²!à4Û1y ìwêcø,l÷æƒVTR4Ó¾õ¯fθë~ÞÿðþÜÿx(‡q,ÂýOúþÜ»øö¼\¦ŽáøKÊD‚9iE½Û¤ÆqêÈÜ f›»-Äw½q¢Iº×+‰.»e$q ÜîÒÄ/Yª¸ßÖ·;•fðan³–ûP/ÁvéÙýÄÌØºgY ÝïÊ95®»ËWjkŽëÌ=ÌæÂ{jQÿ ÷lÝĦn oåV‹MÑ ÜîwO‚ý#ðϾ±ûGÎCÝžóÀ‡…‘@{Ì|¸N¸¯sým jç—䌿V«uhíMȃümtn8ßÈ-ìKÀëj×K=Û›ÝËŽösGǸ—Áw¢¿LtYu‘Ú<ꕽ“‘z ò¨Vîàì­­K(õx~h 'QœJ pƒ(ËûÅ´ˆÂ Øv3™¦uNèWiUZ` –j.ÜÎfU\–ý‰FwþÒÃ½Ü t|멉sÿy±°½Ðª WçðÛ˜PG$¿_™ Ux_%ê*\Oë¶sÕ¶s«ÊòM¶ótµŽä¼¹ø²ùÞÄè‰AnÏÖe‘?”ö–9 9Í \¡†ø¼=yBe "Žê4@dÂýtÜO³‰šT%ÒcÔXÞ~ÓsQÆë×è½þM«åeƹ„¼ˆ=x¬êäò¡ãKóZœ^6úhA\¤‘i\¹Íee«ºJ\‰<ïJ-.+h*ô+•J–.­®¿µÿào¯k›×’Iì{´c[ËP»®-j²eR~ÿ†í¹¶' µ' U€fȯ‹…ì¹h,dȇ[„Ú.n¿0çµN¢Vª\¹ …Õë»K”àe9—ã©ìO–w—„4i áÅ•W,M4\þüŽÑãTT&3ZM‘¶õu[4 ã­Øu²e^ÉÚÛ–í|ãæEÕ¥hž¬žú„¹¶ÙÄoæ·÷›¸™ðf3Û(Îçð ò>mî>¿p*üuå°Y\2×,n°ÛÏ!2ŒcÆrCÈ'8Í5å•”îÓøáü·µ]»ïì½z(Þ´ÿûk{¯Ì}7ÿ òÕù[ZÖÂ΢ù[ZÖD'œ¦½ËNŽ——­»upßoOt–®»yÐĵÃåtwÕÄ=Ë«V5…«'î]_1·¼6á¬'†ˆH*¨Å!&®îKŸ¶MC.·73ìFRÖíå â”’Š0ÌêN›Ã•$å<o²ø¬&ù¿ápyM›AufIeoÒ’9xݘ7ù›‘¾ÃÑ“eq#¸U¬1«¸?**Ø\ —ÄÊ…«&¸_Ô¶…þd-ÊýÅ=CM2WCÜZÊÃÞkUzÉV`ÛE£Q¥ †¥™¾tb”Ùºlï´ÌNÄE¼!Z ¹æ£ Ò¡ÞÞGm§l`‘Š»×ßðå7Ïéßß½·c¢-°| õheëý—>Bßÿî»{¹7ªÛŸ<¹þOüãÚÄæ7~òTß–ÍM•“ûèìËØ—ý.Ö6ð^ˆAÄ I°‹§-ÕüY&ÃoÀ3öjk*€J²+¿­àEôˆ´m›:϶íMÄgK92ßrTÜËÎ?ôlor,=ÖÇíýRõ•§*úV¢+aÓEkóèw¹öž›Á-{þÜ'³v D,3LÕaÎàj~U©# rÑ4י´Ÿ4O"‹/w¡ ˜ÝÉ ÃébLÀĤ§iÇ ò Kk$³}q¯Y¦V…šVW_’¡_&Ï¡¤ UfY5Ym%î¥]¾H+ö¡¥ÈIFÂ×'t~}äÿßÔ'bvtRî*nÏë^ª×.Ûéeֈˤô$#¦úR‹¯"×g€ù1µ}"Ξ\Z >\|áãÿËše€Œ¯«™ƒ${|ù^»uÕªšñ=°«XÆ’ÅZ4ÁŠ,‹&»¹ÈÑßé gU5¼~ûäÔ'¤çírµ3¶y>§…rv˜(¶Í£*¢gvô(²Í#ïPx …çµPRˆÙ2¡£Îc#55ŒR­—m„ TÄÔ|_çδdÄÜ:¢1Ët똑Éßf¶wõäåõtµg’á³tFÌ£ÓyðU­Ö ëû9Ü“Ö1›¡¶Ägî8t±o’%ŠX‘vÕÄ¡®˜KÛÀÔ„ވ͇á´|Å|8åŒÀ‹áõϧüÜ5¯{°°fMs8VÛÙ\"õo’ñy] Ù}-…mI¿Â לßúCk^À®N¶w–%ËÃ×Ý‘U_QZ·°¤©nLƳ?¹°MoO}BK™mpŸ­"–§T&T¦3B8,r¤?¦i¶ç´Î:+pg("pŸôåž”¼¢¬ ž ¤6 -XØÈð›ÀÎSŒ „@Åùœä½5P1Òr'êšæ•…YÛ˜Ê&[tg )"­U5©\c+OZbšä¶\0¶Í2™»r ´r¨1‘Œäºï®¿©™ìNNjªÁ²x,–ëPäÇb¥òìžó»aî+ÐJ¼GXo¼ €Š4g„v&©Ž&fÕWäiÖ`;†5ލÅs¨2²k2rg( ’Å+&Œ!‡ÖèÛ£‹ûúí2µÌ¯ ×ðöØ=ðY/R‡_#±‡@ž òôxnêsŽE~ýSÿ„—>@vÝ~þ÷—LóŽ Ž|!?i‹Â²Ñ©2 _6Šòy<7ÕËÂWPuvý–ʇŸ¡.…•¨ç’<ƒNýçg]Pk >¥^ÿbž'tûC€PêVå a2¢?%©7¡®®s±žòl™Djˆf˜DjõZÕK¶ä9Å ±Rf®íXœ§²‰0þ{›ºü‰¹jÖýÈ ÄùDËÉ@0P˜O;¨™û£H¥c®ª>°góù7œš,õÕë‡x0á{د%:”S ôÂ牟° Òâp÷]5u y>|OxAéÔ0Î@PJÜ?»4,›ËýdËÅ%¦æŸíƒufÀ׫¹ÿ‡øëPýÄw:Û‡ït –›õ\쯵“ÔS{ .Æ>¦!a›u `r@~ÞN`„ëå{Pæë)Õ}Z%+cVIå¦ð™l±Èü&Î,c42ÎYy }?±‹df߈ø¬Xp`5ua ’û,ßÀhî+E ñ*‰ÂTLJÜ!‹,h:'Ó° #gòË,¸_þ@ì¤vÁû¡~ïaVúP›+ÁîŸAK¥ûð—Ä.j­P¾‡e«pÙªóË`꾩û¨ÝÌ!áù(ùàQ¸}{=¤&jŽˆJ@§ºÈVOÅ”qWƒÉåf®±»G.wü…ù©À©+0†hN]×xAÏ¡CÌO¿øÜ;.áÎb\V„x€ œÇ ô¤™Õ5TÚOýNƒ~{É¡Cèד€ûhà>˜º|aº®3QD^âðEö&hpeJÀK ¶¸˜™pÛ¿Zëv ç‘à+2F^ñ œ¾ˆx<¥åjÉ+Žâß\ ÎR»É-ßò›“䔓^rËü›»ÁYöè·þææÏò¯ÜéßÔpÿ ”SK¾=¿#r'(¥r²Fyç† é¹öœ÷ÃõšÉû‰ÊÒ‚ÓÔ«qÚE€úžÇ0)èù»ÞýÉá½Mû—&“K÷75îN’åMû‡“ÉaøizÝ}9ø.x€üµÀ5zÞ/zÛËMÁ‡³ d2… œŽ‚ ‰,›ý ^Em¸ƒ{\ÏüªDìc: Z/Èù-;¤RXÈ\Œ(–µ@a Zå:õüpÒ¯qvÙP¯éê×6Áýõ7Ý|Ìï»Þ·ù{º…˜E„(øŒa“B\Á08UB4SÏÍŠÆ#øT^¡T"ž7*T¬Ž™p‹¹x§h¾Ø¯ ⊫)ŠÊ\xIW¶˜tJÕ },+f(kf‘+¿Ìæµ¥J4² R’[½´Êåð;ÀãñŠòòó ‹¯]QЬS¯]Ñwwkfc2 Q÷”%×®ÛPZÝ¡Ñ5j¥6­]k€í*‡í*C¨ÑŽìkUÓ|4PQXQœË˜[VZRœ‹ãa„˜ôÓ.T8gGa¢0â ®_ø ;œ S…ú‹<à.ܰ=9R¢Œ†>d;£¬Ñ¤« ÜîµW–¨aäÙUK«ÜŸ£ .?Šõyqܰ&fíŠØ‚ÎùÁÌÆ¢€DÓSž\»v6M­kÔ.IVlZ»: »-¿UE-c$rˆê”.ùÝ.›^­’KÄ´’Ï`aù– KdôÉ!rÌ&ÄêŸÀ1@*L¢m ­:jœGUäÛˆª¡¥„c >7“±Ìª>é:éñ¸íWæ]©4‰¥Œ¹ÓLJ%2»æ.×w4NÛzUîÕJ£X˜©ª§µ.•B+jÒCM¾HJ{ðÙgônµÒ(l”ÊIF;\?$ÕÊ­¾gxwŒ»‡z9 µ•ÜTâa1*I(+‘?IÖô>ýJ{ÈKår)W&ʬÊ$·pRg4’÷Ã: uŒu©j-\:VÔObARDd†H±ˆ®b‘c¾HŒŽŠËòÇäíH°Îb±F\“›íójaµ½Z䜟pÃÊ%¾¶ö˜bxÔ v¥MÔ‚ž@»GDgFJ.ç[Õ´Ô†-Ò°$Ë}'},¼Zz°ÆÎËó¢ÖF\Ô_@H%/ÚÄg¹_'íÙÀ,â*ŒFðœÈœeçuÈyS—S?d6uÄ"¢2U^ÓÕ\b¤) uLž&$Å…Rªg(t\8k ÔÔTEßÓîu£•ÆÎb¨   °KÇ>³3.ñ¦v Äj…]¿¨kÔ&•/¿¹° »ÜݼqÿÆæUëã•É5·.[ywa–ĨÑú©d_…;«ipÙ`SÖöËŠæ%F®XzC|ryÉÀÆ¢…óò»Æò bzúE£Ñb´d»2«ËJæõWôóyZóêvôTåWÊV·ÕšS –æ„£Ùe­ñþÃoG~ù†îxiþäw#µe »mQ2Z_ž°jÑ\žk®œapž›n!ñá,2T0FN'ŠRòaÿ³/¢t«ÓÆ'g…%!»$Jt¦Ms‹Ùý¥-« £öá›þÛÃ7’{ò*·Ü½œ¹î«ÕÌñ¯VS ÒœÛæ-ºq¿¿ÜJþ‡B¹ Ô³r®¸¾-çJ$e0J{Å©ç/јÅ?0+åæï‹Lú où8M;"‘p&÷BG®ç×6 ŽPûþ/kïWqíß¹m{¹»w{ßÕö•V«•vÕµê’Õ¬jK²,w[–{/Û4›â`LSì@( )!@H—ò!á‘—ò /Ò!É{ HWÿ™¹»ÒJ–÷ûýácm›;åÌ™sÎÌœó=äo¡^®È¤ÖÞLÐ$=™Í²‘½œÎ&Ù Äf£ŽÓ¨äRÈk6`Ã96RédÖâÀ‰>rHç;ëšÁ-kê’kFû|ùÈÞÞ¦ú­ÝQ·ß ßl鎺.ÜŸ_SÁyf!'ª/ɲÞ8Î\*¢Þ…©ˆºòSáv9!/À™ %"ðwZÎû¼§Ã TfƒðÎ9§×ë<ÌpoDP)/l íà{h«¯°£e³œ€5u=m“sÐ`8üÞY8|ä ”­ïšZ?œá”pÊ”F¥Q§  ð³6ã]3óSÈì…z¾•8˜Ñ”1$ 2u!·‘að1JáèúÍåÅT€Žñ{t*äYXŠÈÂÀ¸QÖT¢ºµ¦5ñy6ЍUҬ׀„ s®&´/¨—pŸ„æ0žw7 B.QÈŸM„€%ÿÙ}ÕPÑZ£™amÅÍñòõ¾`cašÒñ{Ô+;ý)ƒËbPî^?uë²Û6W•¯½¾k¨zÛùÇ&Ÿïn9v]¸e%³·¨gKžQm©¨½ n¨dû!a[ýM-œqÇ›cÂ/¥^}õòÅÃG—vܰs¹óê#ݧ6T}H¶€;«Û˜¬B9%H ˜±°ÆEôkˆ¡L¿ÈÈaháÉ*áZ¥ %£HÙ¨BJRÎ$ƒP¨‚eØeDøÖDI7 ôbÅHÿš5KÚj«KŠv«ž“0DèSj¢þ|t`Ø<ø#Z¼S›s€6«ù…E£#HèY("úBcSs¼%ÞThtUï>ÝW3¼Ú&×Gl‰±¶XýÞ×,ûÌ­ûã]§^Üuãk­Z³VAª6NŸ¿ù‡VTn:óÅoMì|õŽe»>-Ô[+*ÊL}–#c©ø“­K:P^PPæãùcF¡Þr~bWFf4¨¡mÇäU ‡wƒÔÑ/Cz¥•Ï0³ÅÈìlAü, eä°ø;èEÅ m­Ó ·~Žã=|)BÙÆ¼Ä‰ú4J™©—ó8 ŠFPšzON3Jé3R­f}óDLû-“³Ë÷ØÆ¦  ó–꾊ý÷ªè˜Õ¤4+APÍN¬i¾jµ‚†Fá>²R¢6¬Âà͵hÌÿ BÌff7á„Òþà ×XÂdÔP´”)‡v|.=o”`X)ËH7¤ j°‰@ÙÈ€ â‹9“=C¥‘梯 Á…p¢ø¹‡s Ë­äìR+•mXì9øT!“^±‚f[¦ä OÑ(E04´òú)ï'är5‹3‘j–v××ú ñ×u^ÚÊ˨Ï|â$°4ZcŒJú´”Ó¬kÙ…äÐÙeû@t¢e½V%þœ“rf¹ Ø/»éîDrÔæ“sr½Ío/,rÇÓq·½tI<Þm¥cpÑ4Ô’éoµ³Óx/Y%QõrJ2ê6Zô¼§4í¬ðD¼e-¯- ˜mz»4æo.u¹hž·Q2ò×Ìœ[ ¡]–“)›Cíò¬UsðLH‰lK î®oØ3˜H îi¨ß=˜8ì€ûÉPK©ÃQÚ 7—:èLý®Db`W}f'*¶³.Ô’t:“-¡Pk™ÓYÖŠ÷Y»‰7©7èJOT—©Fé`ÑË An%n§ÙH#îCY33. êtØD5•–DBvkˆÖÂ>§ÒÁ´˜Ü”6á=+1I²˜©’ ÜX-Çw]ö³Ã·EÖ•§ÖDÎ.¿Íáq9²ŸWGΟux®õVvÇ »«¼ÞªîÂXw¥—¼†EÝðѳ‘uéôšÈmð³Ûi?;‚‰¥c…=•^o%>C$ fþF¿ÅõĊ̰ Ž©$AJeXÉ£ZHR‰LŠøU–`F•€U‰Œ•ÌF ñheaî5È›ê3è,'U/ôBq óû ´jsTŸÑDuO£ÝÞI{à~#WɹÈÎ õi_ûžý¾ºçä³u52Zap„+{Rý©Œì`H‡iú§ K‚¿KH‡AhU\:¿|_w\'•Kj†~uûž'®jbî–}Š”07Qå{Ÿ»¦yti­TÁ™fî+—j-®z_’³|õáD˜–Öm\ròëûƼõnH—¨£:è:H—áÌÑ,F–œ ´(y®H_0(ùFÐ'§JÿQû†[žœ¨ZÝÛèHÈ?ÕSqÓ 1ð6MºŒB¢Ê9št„‡Ÿ:Ú´¦µH£æåþ¶ï[~K-{\¶Ÿd™=äçWž›(×û’žgqZN÷pGÛË9NœqÝ*u 4â·Þµr©»ÆÏÃÓ3Vê¯l!\ D±7clwÙ!„”$AÕB«eõ¡é¬ßk dÞL €\ Ò hÒÐ9ædÙ@ãL QlvcóŒÏð9ý(À±de›)ëF*&ÙD]ŠÊ9ˆ¼-Îb+€,¶8´ñ¡T­ÞÆSå«R›úKªw\Xg¥|mÁè‘X‚·)=…5¡ååÛ–¥êö=:ᦜ|xÊVT[0R»ºÑ'ì4G+½}µ£U…r¬­Óì+ŠÉHR_Ô6ÙÝ{Ǿvæ´Îñ5UwºÖav¹¤B”·lè\ñðñ^Écœ¼1TUèQ .I¢k-y"PUèÕ€ÇÙX}–;‡fìÔ3ôsÐîN©L²D¯…ìæwÈän pJW¦âŸðÐ8ÿsS(ôY’g!Mr¸ÒˆÝ#‰d¡x#¦Æ¥Ö=ý…ÍÇ¿ºÏCÛùL´eK¤TªU+,ÅÞÓ©®TÆ Ö=¶ÒM;øºÉ‘þÓ]{zBrYÖ³#‡~x~„þ¢Îþ¶²º¨J!ÓÂõ=í¤n«ºº±Mú¨ÞNž8x‡‰ü%Û´ë^Øw9޽ÕhdQ@³RŒ±,³Ê,‹w8Ð=ç„iˆù}& çÕæ‘ä’©)Ÿ #¨\8Ò”öÜ¥w¡xÒÁ ·Ù¥ÅQ{¹ƒì’’*ƒ];õeµ«Äob¼·ÊàÏ«œò¶Ý÷­nßÒÔ¶š"j+ZK¯h)”8À=Rr%g}§yM€•4*­Õ„Ü­Ž}g}èÓ;2¾šþDHE-Q·.ض¡î}œzbF€ÆÜß™ÃPÀ±ª`Aƒ0AX@²³9¯ÄÜÆý"î=®¢}ŸÞâÓ#``„ŒÏàÐ62ˆ„Ñ'<—âÐÈ#Äÿø %ãt[«ãš® 2òg…ï~KI;í¡öš„&ÌùÛéšo=1l·ZÊGȯhË/<7f7¢Ï÷F:`œ—AÆCxQܤ\ÄÛ†³P¦`P¾ †aaÄlA^Û%"OÄûÀ.Þ=ýññ•?ûÙÊqBÌ9ðëg®‚6N±Ôe4 ¥LFRFz![]$¦ ‰””JÈMJ@ʤ$4e#—á;¹ÈW@½Bõ³³B!ºáˆ\ߪ}?(•âúeU ³¨Z‚‚6+µÛÇ÷d±Z2 ÿ` |T ‘ÍV4œ±øÁþ®ŽæÆª ¤:ý>ËnåÛ m¡Õ"‰¡hËÒA™Ïæ{¹^vÊJ§@i &šõWl¸©¯i²3T±átoÓ–ÎðÔlüDn3þðZ…]„<Ø¿§aC‹¿bíÉž¶Mî$£T(ôθ?Þ[í5G«}ñz·œ|v¸¿l°Æ3Ú[6Tí'—ÛZØÕ¸ìܶÚâ]àVI’Œ(¹žRjÈ?Ôõíl¹s[M2\ËÊ8Z2ÙXÜ™²›2!²rk¸cKú;IÐÄq‚š™bHš0QAÔÇ2r °Ð •‘ÙÀÅBBÀ½2 ¸‘ÊD§gˆ0ƒ³‘Œ¡…ÅÐ!‚w™¾Ù¢Ã+ D»¥(†ÎÇx½¥3C“”fáÿAœzݧ_˜ÒX2È8}ï“QAî¢É}w_1ØäÓ·ÀÖößýöÉáÃmÕ«› JFÄ×£Ýͱ¶RüÛ^jŸž&[ÕÓÏ1^%?å5Åxêá~¡á¨_#º©~ÆûAÛá‘d¨uUyÛáQôZÓ1èªìMv ¹*û„»Å»“0”<Ü?'¡õrXÜ4‡ÔJ’Öª UÓ0B”T7j$ð½r)‰‚–gC#CðWV²yÞ‹F9 ÒpÓJFCÁ/çóqz„ÇÅ™QÀJ‘àËF4ã›Ì¬÷wI5™wGí‡%ȯî²Xë__¯(¶u¹ÊÊ+4_Õ ŽMÈ mÝW½vU§Í&ÛôœQ(Ø9þi›A¡þŸ®ú!¸KØq¥ÂÃ>¸ËHš»Œ9\ÂFp—Ï.<Ôb³ /î5˜V#ûž®Çû1ä;„Õ8{#zC-–ÔzarFúÈÚϾµwÍ£Wµ¬ùì›ûÖ0\7ÛáòÁ†¸Vjài%;fèÞu;]éØœ9ôPÐqrcß¹ñ½-^µÚl-÷„ÛÖUÅÛ“ö}¤§~¼.ÚPhò¤Ûƒs¤€|›œÉÐZ:Eh!‹&3Åz¸LÕ(I;šZœë“ ÀþAhŽÁvnãy;’³ Ð` K¥ÒÀC5WNÚø ¡3áé¯k$MþâÎ'Ýô€é2Þ < Ú,ú*-¼QR7}3œëLÚñ "†r¯1€‘0Žy4!ÿ1‚ÁÉOGsø2.·'jM‘­| òáÀpdðRù±)h j‘—\—.ð =kKF —ß0\Ø{Ósë†Z¬iuŒîjo?´¬øê#ñM«¿@n¤~áð O)%ÇZ·]:Öbô'¬œRãN” n_w€Ó:É>q/ò>õgÆD8‰„>Cf3ȉ()D>^–4…ü1œ5Z\óû‹o“DżàŒL}‰ÝõmGÆÊºnxvóÈ-kK-¥=)°—/sŸ™^á®­lß?÷Ö¤Û Æ©x$ìò.»ºwÝ޶§×ÝÛ¸ò‚·ò5`?;ä­ È. Ìò=¶½¹‚FÕÈÏÅù"i©Œá§·)JÌGÈOi<Üô.“•RЉÞï‡oñ.Zb‰è,æuȶ»ÒêCÆI¸ ÐoE1÷Íɡ۷Ö.?÷ò–eðõá@ãÊòîkW–ùÇÊ;¯]•&wÞÿç K«ö~a×ýA¯_ÜÚwpi0sì…«{ôk¼[ÿd²Ç þÓ"49“͇ ÷;½¥O Pá8+rçL{fµ ”_Ðæú>ˆöR2F0*"àáë÷N3í=BÞl-³}†ctíÀ8ÕrDô£®‡µ}ê¯üçõhì HCB ©üX I‚HÈë6ðœ$ÈkÍúf·CýôŠXg;ªs¯’@š*5• Öeüßi*ÔT•%£PU¹:-h"›¤VÑ×6“¡þ e?ldžŠî{‡@:€‡ü§QBÀ!÷nXW”ß?CûKùÏëP]ûý’ä,”äÑ–äæO,É}Ù:7Cñ (Oa›QþJà\7+WýŒ ùVßå™ ö ùß…#šˆmp¿®º‚ mé³¶ÿK(£`ûQC¿&¤²È>‘¤J•y=$ÈÔ–µ¥ÚbO©·RßMºeVÔ·;ᚮԷ;qßÃÑ¡ØÞO¶úpˆYCƒû´î Rö¶o\låå§Ü[tíÛðÈ›»†ïÝÛ´þá7wß·¯ùáXçÆª¾;v7Ã×ê^øJ1¯ ÿñòxÓ©×nz„^^Õ|êµëvÞ»:ÖrïÔÓ;ϯ‰¶Üó!Ö%ÀÎ=ÀøÅnµ„̇éÑ1±j‘òs†¨ #‚t_{ñ3W‘,ó¢Âb1JÂŒÿƒÿ¤ >dx\&•Ë+qý!¸š~Ç]Œ|íbäìQ4j´¯Äf-ÑoÎeòìP0g‡â|Û f7v\½"Y:²¿A8èôG­6R9õˆ„ÔsàûõÛ‚ÎÔ’hñ`s©VqšGÞÎÇúöuôŸ¾jOp¯\鲬Ns~ x¤zró¶Lq{Ââ,m‹z ŒzäôX1Ç8$Gy§Uš‘ÃnWø‰LÅç_梴§“qé9"2’¼q}”ý ŽññΫ–'Í««·ní¾î ãN”¨–82™µtykÛŽî£yÏêó:Ë–ÄàX˴ʧß<|ð,SíÞÚØ¶³¿‚s^¿öÂÎêÒÑ#í…ÔbikŒ'&R‘öR×^ꦫã­q3¸‘NË!'»qBGÑc®gPæV伂ÀÐ×xò>ÎÔÿ†œ4p÷GnŽN¿¢7’RòÍ;¿Ì¨¦ ¾ ÞZ¡6ƒ¶êÞJ”OïE4_iî%ŠˆL¦† ˆEIŠ1Šþ€«vÖ`(1¯ÛK`ľ¹„²|Èy½cÖàLÍ·7óœÄK dp~À'…ë× ž-BÉÉ+Ö ¶Ú*x«.\=P 7ŮڱºâÉŠ5O’ãôú¢ÂgecGÛw?}›1Pbk6Ù}ö²±ã]¥ÕŽs’ëñ:ø>¤ã*8l³i³¾@DBSÎåÿƒ6›/ˆm¶ÙD‰©¬Í¶JJë¤ÓÔqßnò‚Ý=½NÓ*ÆÛî ¨?|Òì¡k ½&í6$kÞ†mE!¯";Ëá.%Iù—[xÚþoL”WMa]êš–úÝC‰%Ç¿M”±[Yg±p«ªÈ~;y‹§ª¿¤~kOáÞMÍ;z"¤Òš0…#…ÒùÑ«ÚR«Oö6ìÞºµZHpê¥Ò¡OaßÎæû}[0Þßû´ñÿQN‚œ :Ú8r×ww™¨†¯ƒŸš¨z8Ð0œl=1‘Aé[Ol®§Œß^eMã ¯žø(zemã ÿvlòŽñhÛEaæ¹É;VF[/ 8‡ÿeÙg ¬Ô![E™³U€ˆN>g«ØólM—@Rây¶“,=­4ó: ¹î¶ÔÓ=‡©ëù á9K+ØfÐÿáÐa¬Ó2§Òê´ Ò:'Í’ …P‚*?B‚’ÀïËÙ)LÎN™„4b]“¨®g$Åj Ch‚æO ÅHP–,„bÌëæu ž¬—äꮃ¼&ñŒ±=qêw’üÇÈ lŽ çû îþ‹ø|zþôŸ´ÚrIa ®vÓÿvµû$¤·ŸGªÖ‹ôöóXoßôlSp‹¯=Ѧð…r6Åðy¤zþ üüÍïc«†þØÕ$ª|œûÏI",fmm8·=Y~¦ˆÖ,;û?Æà&÷&‡vÕ&úª=%C;ëŠû«½Wq®ˆÅ]WâäœasA}Âþo[/n.-hÛÒ¾õ3›Ë Ú·V%šÂ:GçuJš":Oϵ°þ™øøÔ÷ÁL”šgoÃuåX™ Zª;ŠüHêE›;¹8qK‰|·Î)qýicçn…píQp12¤VHËž™¾{TôÁÕÍ ,ä"ÂDi&á‡+Ë÷¶¬s¹3{‘ÍÚ —ƒÓªD„±Ú7Šwœ8HBTúu`6°^T‡`ÓØ„£$ã{Ê7|-ÌÈž!UÊ÷¥c2XXÃËl–Fu²uøÓꥱöú×EVXVØoXS©rG¤¨8¤SÑŠdWå–"Šá\UÂ>§‘嘄+!ðxe Å€?〈0ŠÐœv­šHÃUŸSñWRðQ¿çg_a[Ò~}ª¬¨sUIª;å‘Uü ¥°D<îd¾©O¾¼§$¯ÖÉ­¦:uióø“%Ví;áJ«Fj]æxK¡I*ád.‡ÁPPdŽÇ—‘•Õ‘h<À+³ƒ$‰œ—!ð¨ËÄþŒ‚Ÿ;CÃÊ͹m Çg7RU§UVƒ^‡Ü(¼Šy,‰çë7„ÝCú¼ äälþÏÜ5òì …_÷d×)Z-¾‚Ê ‰›$•‡’èˆL²”*|=Ö¶¥Í_´tsMýZwT­Wš—+2mÝѺ ƒ'¬ÞOk4Å]Eµ–„ÞRN«Ó‚þ†¤ÛÆÄ=wìköÕAŠç´hdÏAó.ìdÀ•²x'<ïÔ)U—=t2ΕÁMà ¡Èèh x3%ÎHdžêdoÚ‘®xIî3M wV”ºRASEÒ› 0…‹1ÖT˜^ÕvUtÇvg~¥àiqÆL¹“•¦`©¸ŽwC~9 û­@(rQn^ñ´IA(|:l¹_Îf!+„o½z³pI¡%à7ƒÓw&Q§€³T é’‚kÎEĉ*”O){¶„¤<Â.šÝ¹I&{f·m$Ñš(FçKé²âªDU8èŽ{âŸð|)5wœdÔ‹Í^œÐÜ–ì«rµuD†‘²\UÕe¥á¶5ŵí:g€·M©±šªxqeMÑÛÖâÆpË:ßTXØ]î¶ÕÔšôK’¥ƒÕîñö:KQ®r%‘hj.õ•µ$‚I‘n€4] Çì ¢Ÿð © Ë þE 46ç’¾ŒšÔû8[ÛWŸpÀîW$ºÊìÉe>Ã60YY\וNºJý¼“/ÐsZsQC¸xycnb{j~«Ð‚™âÊκâ2¾Ç˜ŒÎ'ëÀf8½Ã_V#x1rd¥(&hqµ#Ñúðjo±eœ,SÃâ`‘bœI>œ¢$)ñA‰àK'Óà¼åù‹æÕæ¯ÊŸ·€Íß}å•ÑoãPâv2@<øq°|?ø’BXúàh.vþdŒø@Œ'¼àâ{Af½Èá“/QN çbUbŽÚ)Æpž7’í„%ß™Î!­ÍÆ‚?°*…¨WÝ9ïH—Èdª¦®€¯³³ÝÅYOdÅØXtÉé'Û<¿ù×ȇâ£Ä?È:„â¹qݳ±£@D@±£à´Õ­RTÅBú¢ŽŠ¹TÕÔý}/¬ÙÍX÷à ËÚoêÄ96£gïÃü»åéìAžØIçbÚ!{š‡Yú£ îÎXKèį<•¨.©Ž„<ÅÞâÙ?<†^8†˜Ø^Ôûg¾ ûw5YÛ?OPc¢ã9.È‘ßÀ{7ä '…è˜í BtwïÔŸÄàÁYp„»ù"G }‹®ØÛRæ22‡K¤këÌ?™÷˜¨O­âKàŸÈ± ÜI”Qסõ@‰Ÿ‚@ÆX (†,µ ÈÕ@&oR“u¥O@n¡=¬,¡³ŠQBNȤr™x6ª„«`¢_’%§èðG?e·^W¬bþcqø”B$CfÓGµKö+EÿüçQìžø(ü6L~†‘{;U¦T¬Yµbtd¸¿·¹±lKj Jsf1*J•¥>ŸóÚÇÑó<åƒô´fx‘žrjù²JJÊB[_Fg©YLHYäûŽGÇžÁ±R9+S‘%¦L¶˜E¹çóˆ:¯†ùOűûTŽ–Wn•ìWˆ¤ÌI¯Ô,¢¤U©^ÚÝÑÞÜX‘Χ¡ÒްbjˆG©ßÓë¡*/ÊD?Zç/ÄOžõŸÓ“·%Ái­°·ätR8 '“ôzaíqa ¸ï8¸W´Co'[©{ÉW>ãAò_Ü´”|å8~&Eü˜z›NÀVÙ'e Ô±2R"AHËõ¶pÄÖ oGzÀ5ÂÏ6€°‡ú‘ð”ð¹0$<2íú®Ðè$°[ ñ,ç2øÎwòPÞgÅ.Æí¡òÈ4ƒP äC‘cÐ<ÏþÀ|@«@u¯vj-8aô'l¦ ]k ÛŠ+ ¹ð5^ n´'üÎ4Ú‹ øêé«Å!øÑÇ‹ùhˆeàQêUj㢈ˆY gÅDpÔ7ôo#U¥™úf9U©žú&>^:†è÷=âij=kÐ"sMtHYuG>òo‡§ß:LN­†­\ vSŸ£.6¸{~9c„;q‚Ò³Š=$-‰{I†EkF‰Z`— zƒÂV*!À¨ Ð,"ž„` ‹>樟Gî_³EóÊ Ër‘¢#§òšk;ìèdÅŒÛáVÉlDÀ(þWžÅE3å!MÅ=˜þ ß“ïÙSE~¹<°»29¾byÔ‘.*kƒ{2ðÓ2ÊžŠä~Ž:à'þmÕè2ªJ¥7+&‹ ÝAÊ`Ql,.vÜÂWT¼Y>+DŽê³r#Üë‘ïþí፧ñ½¿>£…VSF+'ѱZ ´Ÿ3¬ÄhbžÂÝÐ$²hH½A¨ %‹ Àm.J;ÝBÞÃëŽh-ò·ôJ…þ§:Ïp¸ÎvÁúïÀõ³Oj³8CèÂÅŸJ‚~8¨Û%S¿Ñ’:¼J–x…í:©!Ÿ'ïI W¡:ªW(õ?•›µG„«0ODõ7ú\‡¢:S!ÍI ä/Œ‚/¨lÖV"a»–•ôVÒL(û׬ÌÍ—Èߎüt!¸Q#ì‹žŠ ÕàÆB¤îÑN­§Og ÷€uÇÀÖ©Öã¨}°/Ðp\„ú­ÕÈ ÚÎ,\^¯#5«iŽŠ´ 7|ABÕ$ob©’¸Ù¤!5ÓUäï¹i#ù4˜Ü¯µ)^Ö+•úo)mÚ`›à¸lûÂ\ÛœVm‡ † é"f!y²m£Ö—ºÀÒSïjHƒ¼-/ »Ì*RMNL9ò÷¤ô ·€­[§Rê_Rصû?üõõbÞ=PB}ÊM–¨Éå¯F[öqÃue³Ud3íóÏÿ~ø)N‡®ý ”@„?BUk½g¼ÓƒP 8›6Ñ=óæ»Ì ÷³éL©\†€^Äô4ÐVÎeÁÂÖOÎfD˜¥n|Ä ú%º(Èù®šP¦‚‚:ÐO$Kh'0ùgAj  ³åÚÜñîòñø¾s—¶>,üí… ›¾ ¨óò‰qêÃãÔ¹ŠÝíÚöù½5ä‹W=v]Ïîý- íÁ­/pû9@}}ÛŽ§nÚVÚqÇÎŽ£bßÉ™÷¨·˜D!Q‰Îד…QØ3\Ñ`.63ëµo ›âEE•ñÊ@ÍdpntJ—— DÌ•š%b¯²€„>À ‡ºº5·?¿¥j¨Ü6òà;·]A©××T›6ž1÷n½~IÝú–@m†¼¯b÷ÚÝÆÇ®j*'¤œâ.`|ãH¸ueéªÏŸ=\¬2«¶|jY§f¨Ì×zde*¹÷…휊â‡ïyý`híΫ2MPÖgPǡΪ"zˆÎL{}ÚÂ@!ˆÍp”ýšrVFáü†&úQÚׄC€hm®î©éI&BUá*”Ù^MZT 8r #Õ²ñ â‹DMq~º¬ˆtRžœR b$ýZëÎK<±±Éƒ-×'dÒŽ]·vâï¢+'´vžX•É:wŸiO®lO°¬½z(™XÞÛ°W6vO+ýømqÉØ’bVâ¨ú»¯º;é®öŇ´·Š‹Ÿ« âCÛÛ Åo565™œÉ¦€¿>áȽšŠÐ÷…&ÈU3£K™}D„¨&2u(bÇû… ·„PŒGÎEì¢xÔKÇ¢©Òhu¬Ú … ¤è6M¤›½¢J‹abˆ]9Äe*Ÿ0FðÙ`G÷`bðÚ~§Æ¼õ‘_ÝÔwÍ2¿BµêìW'Vßàc¥”ybg¨¹<¢RpR}ÍžCnÚ’èLw[Y-׬eÖMÝvâåÃ/ Ïýášô¦ÛVìxéL_ßRo­ékßЛõJîÃKe“nôTû«&n8r£˜Cå°ð%êUf?áAw,HÈkEä€ïg¥pdS*èϦøžõ=G”ˆª"fg? ¿p£¯Þ®cœ‘•;m3/Ýxu‹§ÑÆS2Ôî¯ihÇ#O¨´ƒ:ýÄŽ)çš[F£Jí…Š%­åÃ,·Ú ?þ–ÝK˜Ð¾1›çGR‰‡£Ùƒ†˜%"&•ð ™ xðÝÍVÂBÞLuI Õ«HÄÉmŸ^r yÜLñja««ÖmMÚ‡–ÿ×iQ>$\bwÁ½›‚¨­|£$2½G¥1›ÖÉ–Ñ!|uä-ÁÃÙ/9×òeSHøÒì.Õ_¿þå¯OøÆ—¾ñŽŠÙpæ_¿d=èß™l.áiúf#Ô­9PØ:c YM‘EÇȂܡӛˡï†/qúYû*pø7‚FoHƒú>?] 6K¦ß#¿ÃO%e”‘ì6I¡Øxnz(9=t®Îa!CþJeWC~¿næVê9æ‚!x4œZÎR(ºÏÓ:á )>*w¢§Ò„ä:°î0X&ÜsøÛïÈ4 £‘ÿýz9«–“Q¦©w§Þ¥LÓ?4p\O“ñ OWÀãyxŠh`ß¡o#¬èžJ'ÃAt °ÑŒãvÎÇŠKDqó›2EåL‰"á‘ ЬOrjð®%ÆO–Ó[ôÞ^>×zõoª)C¹… Tó~Nø£% a¥wK$š€Yø½Î kÔ—&Ø—ss}A_°Ø‹º…ˆ_žJظÓàŒYèĪpÈ[ itöé¯%9ì-êÌ–ô‡·ëé-°3œW'vfrú­j}@ôæ F"¹[ÊÂÎ#ìL öÿ̘¿1?!Ì·ÐÌIrZ5«íÐDžü7GL˜›Ç‰º <Ûhð«T`„êèü]ç%¼Â](üNàʘŸLÿ´J'å/(žH¨§ÿœÞT(~M%jó~áiæœ_Ã󌅓¡&ë0c^Ê‚€=‰:0ü„ØtÑ…x €B(Ô¹%éÔr¡µÑX ~ƒs(Hõù»a”Ú€ XuA®Lxš Vêeœð'SH?¾þó‡ºô¦¸>h‚#ǽ@ý gþÁúi„ßZšIè NF€:dƒZ…­à+]LX «I:WÕãI‘€`vv$>Xÿª+GÓ2ÖÓ™Ú1N‰ ELenáQ³ƒ’Ñ¿™ú\fpÚ6zÃS'¶P‡"-ŠÛîÛµ3ÿ`Þ…}³ S})ä¯_•’$ëð¦xnɈQœ[Ôà¹é¯ãEo%ÐÔ µU³@öøm¹¥ÐWO¿ åœP;õsò8àzÜe@îÓx„©›]tFSÞ’ÏÊ‚,Cå!ù¸¡ xÃÊŸeÀ;<”døJ£! þ‘ή`$7¢üöV] Yz\ô2Q,¼‹Äž~«R'Ñ®×âMéÕ¸9Øu‰N\«Ägé ð6s®¾?‹{9ÒTVœÌÂ&žd¿â7ºËÊð Êäÿ< ÿËZÏw“`l¶'^PâEhk3‡xÐáÛìÙÁÄüçøÅŸ»â#¨7—|#²ŽôÙd,³Àààg#%]­-¥Ö>mYÀâÒÙœD*¥™åFk]a²ƒï=*‰ZõN©B*±“ßgž'«Ù_@CTšGCûe4´_FCûe4´çÓŽù4ÄÈñÄ,BZ¶)SÞæQüæãŸQ»öy´¹¼ >»ÿÌn3çÕðÑg©kΧ.rY@Þ÷+“]=zo‘ÍЧLF=.Ó¬Cf¿h²TÅ-A§IR½^ê2ñfHb©Ÿ™Sôý‹Ô˜™ÈÈyHŒ—µ¼,  Iz3ÂF )b®9Ü:Cö7h/oF3Y»ŒR£Ñ˜5&=r³Ø>ƒ@P¼¨M'ý¢+ õÌ.“b•„掳žßÝþðw)§ßÃ9ô„úZ™Lw-+îúweµŸâ °¯_bÞ` $£„®*(ÁŒRØ2r²Z¤еr=Év¶ %Ø }~'þzXáRõÁ Žj·Î­Øÿ÷»À;v—¡n¸œÌ2þFZJs'y`ÿõäF^Ü÷žž žtÓ@ 3ˆèFÃ%AçÑ n›öº"¹Œnø7’F™‹ :G7§5µÎ…ݬ.Ûë<Ê!°ì.C=³S¡cÆ¥ wŒ5*ö½s¯Ðà´(û­æ2Ëóô„DzT¦»ÑÎÈßh·Rö×`̼A½'YiË„ Rtß°hOtœÙÄYuVD<Ý"Ä›ëÇ{W)œª~‘xž¿ß%Ôº`GlæRËóó‰wwÅñƒÏíavÍé¡Ö†Š–‘@BR¹Õý–œûV·hY~‘² ‹Í­v/ÚÈ­í&4; ½JœŠ`XŠÍN’ؾm~ ¸å!ó‹è0¾_^esS½°6KˆùEVçþ¤Õe¢³¥Hš!1(C²e»Ò ³^ŠÁÒc8#ƒÒÙù ¹£9;ŸiŒ>°¸ÀÆ+tœb©Üíq´Eª;ZʹFe2i³i̼V"•0p¹^OI¼^Ï-Õ©²N.¶J¶é]YIƒ×.}QòÈQÐsNP‡5æÏ¹ø-9÷­nѲü"e››sÿb[),›GÇä¦ÝE *µ)¯ æÓa`®˜…y¥Q_Ýó*Í›Wë'­0oB#Ù ¿B?bF/w¹)Õ5À)µ[Ñ”²2¨”ç­Þ;ÖšTi'—Dl9 -®cŠ=†ý—ö^ VšC8uA{QÊN2 £#CǤ” 2€ZŠógõÒÏ¢ ᢠöòʃriJF€–æQ>Ÿ³ )´œX@nZ´ô,äY=âDò­¤8 ú¡Ž¶ËLes¬¥\„µôWPÛW`¬>ÞmT“Yå]Xæ”7RæáEdá¥ÅÕ71«ƒÊ%ïC>K ¯ëËG›£2ä 8vÄzZ9¡ Ïã´›Mº„>17ÛÊËfûŠC¼|ª—ñ“†Üĸ¡pUIÌŒ?ofÜ% ÁHuItÁŒ?ÏÙ½hh^· ËŽ’àŒð ¹næÜWååÇ∦J%Ãä’Üaä$rÁ5KywÄNýÆsë+Bæ˜G_ǽæ}æoÌøÞtéÓ~IS ‹Edï²ç¢~D7mÊšÅû[äÇá§Cñ¨ˆ#± j͘M7PVZ•sÿu‚…±?Lhã3œ=ðµm›žùà¶~ÐØ°š Ã$Yд®Îåy:1z]ÿäãÇ[+®‡¯ÇZÈ¿>"üî{;ïzãê‡å{»&VÊ›‹5ƒI³BáQ4ìªVeî^sÿöêÁûÞ¾ií};Ð+:sºgæ=v {!±J1Ü àd "¾ Äøi”Y —5ÎE`­š-vy‰á ˆPÐë¶YõœL*a‰BPU«$Ž@ŒŸÊ^,ã”Î$¹­óØxbÍ—Mᤜ!ÉÒÕ7ÁÍÏØÙ—÷ìùâŽ"õšÇ×Èm-;O•¯;ÕO¹žíåÙ‘zÝÇF`úý]/ݲté-/í¢\So÷ŸZ_.úK­šyŸþ?ôz<ÏÐ3Q¹èdÐb %ÖEˆd¹Ût'ž» רÐ]¸D4rcHÍÂ8?“ý‹õ»J4tÉòdr9œÄæ õŽŽ“/ì»mCYÇÉ¥}‡zCŽŠå•‡{ƒŽÊåÔ¯Fû­qÏ›´á¨Ü£ÈlH+ûÖ]û“;ûkvÜ‹rÞ÷O«ß÷ÐúÆ-áúýèµ#ŒÏ̼ǘá ‰ ¹Ú?ÙÓ-/>÷Ë1l/T3ÙÌÚô¼ŠÆ°ü—ýŽáÌàÜäæV ?…~)ª÷a|?QäMìQÐUqY€>­Ww©—®i;Q;ü9¾s¹L)+)]N‚¡#KG7p²å⺮ífyõÃéOí8ÜVGþè‚´=9G…¶}ƒIzÚÆé&[‡ÊVŸì'·‹þzΞþs3´b‹ˆâÖŒÂdl 㿊L](¦xʼn{å„B®$²Îš¶MÓ¨„ÆÝ?KˆÄÇ•ÇtAÍ1½Ñã.IÇCw‘§Hò{ßÊ(kl] ÏÑ"‡ç–ö¡Ìú" Pa£Ó=³~Îz‹âÌÉõ:ÙýØäÒ«–¶]ÿü ]ïÝ»ü–úòÈÖ¶ëF‹÷ R¯)¦Šè¯¾þúä‡_ætWõ®¨Øtväø›÷,«-ÿp*ÚÝ^ΔVïúìæ“'?Ü‚×ÀAb-ÝE_ñ×Ï 6¢—‡¸…B‘(Åôb(ÅWÜýÊÖ÷ìÌD:6>Ý6ß}:Ú½%Óyz2ëžÄ¯dÅÁŸ\[rìs«{ï¸vKWdɱÏoo>4RÖ¸ûü²æC£e{î&´Ä—ˆÍô.ú*BA˜Žj…²tœ˜$ö×·片‰'‰¯Ó¢wìò¬+æ¨ €RIª´Ü$RV2ªRÈtRÙ(t 7êô£f`´“Óhµ«Ø\VÛ(á$\N×2N;¡„soBâmååUÃß*.Í|âF§ÍåìÉoÊéêG­Aó¡âÅçŸ}ú‰/?ö¹‡¼pßÝwÞ~Û-7¼ášcWÚ·gǶ֭͛Y¹bùPßÒÎ%-M™ÚÊò‚ì^·9 JS¢ yÉô¿|Ÿÿ¬ä¼gòÞûÿ¾ÏïyÖo2Ø&Þvzã7ó¶£ðÕ¿i+€ŸàOV5úý4ý_è/úw*€þmó‹…l)øÉ0óVÊž+>õçÜ;ò!øW:ý@® ðíܣ³¹ï.Â&ñãt¥ÀÁÃüêwöX œèóq‹ö3xéc Üÿòþ]“ýÞ}íBÏ[Å÷èÕ­ÉîëfþFÿ“)"ЉDy-Š/½³hï¹oÄ»ñƒïvÄ'Ü(TF+(::kBÑêÈ4bf¡1x˜+7ÃÙû³Å±©;W:„·W.Í0ªÆÜÕ¶ŠŠΆmãŒ%üe =š<è1øô¿aãJrFÈX‚š ŠNTYxMÎÙŠ~V¡hÝ}a]ª;ä’”ÊDÉþkµ&2¬=|cíÖõ+‹ö>ñ™«5R]·}˶ڱO‡•àç+Q’4-×Ç+4Ž~´œ8ïªìKN53^»éLuO1ß°û¾14Âx.âÄKâNBîƒÄfŹ˜ý€(/þìG1I M1›'—âY¦YzÆæ ÃÙƒßMW¢(z&S|yñ )‡hšQ²4õÊÌQ¿Ðþ HJ%éLïþ[ª;ˆ(ªH$EAï•)Új_üV·”Wi!=É‹Òsz—蟚åï âu1µœ¼h°Ð†>H²†ÅŸ£(73†‹N÷ÑÙ±²Q*!‘[·ø!ŸïDÊ–Ha%4%¡7-ò,±ø“ˆ¾—?„=?›/Y9ŸgqÎÒ ¢9ìúeÆ,ߊ°v(qR“uä•ÈL¿Àª×Ýûý}Ëî<º¾ A­–ª}Åu[š{On¬—K¥Aîð©Úmàå¯;ª¶šºí[·‹/"Ù’ýOªq—wD{´rK 0`‰ôìn‚ÍŠÄÒT±Ýt[õÒ&¾pÑþÔÌÓ¥ÔψqsÇ—è.Áw·^”UÁ•£»„¹/t— »\ h”żf–9M—/x´Üõˆ5}âr¿Œ7±Ï${Ùj§/¬ó9¤œ\­\§Ô‘6åÊ-«Ï· =x`ÆdÉ‘eË–úd`8~î~¥ºS§”÷Œ<ð£Ñ}­ÓmÔë~¬°Â£J.ßß$ú-œšQ`ºÄ‰µR `–$"|2¤ŒWŸ˜³‹X–G 48d4)u9™¨…ÍÆ‰8$‘@nŠú/'¯è%Eýšâ¾õHQ½×.Õ+Њ JÈéŠ+Ñ¡e¥ëÜý*u‡N!ë)p!Åô+˜{!”AZ¤‰û2j„kƒ´@+2G‹ 8ÓRš’n’)CK™ —‰/L‘Ù·ù‹,ýÏΑ(g^ç³ ¢XšH#yfðýrÓµ†MÒß,¶Ø´¢÷šÏ¯]rtrÀY­TKu%Ý»ê·ô”HÀ¦ßZ<Ô½¤ òÙ%ûÕ&H»aL»ž©Ü957G+=KÔò`qÐQÑ_¶îÇ£ ¡ Ÿp‰\ŸGC῱_ç?…§ÁêuÂAìêø²qéò§)c¼BùF¢´^Ž­-£èÊBiQ GU#Ã’Y‡yìnêBÊ%´Ù”W&h†}Ou€@'š(lBÂà€;S¦DL¯€“mû¢±šÊE‰sjJç*Z²´?“Ø÷…}-0ù]¼]§ø¾²Îho,¯Ý¤^Ÿ*R¸ÃÅv—_"Ëåóž¤ Ø*"‚òRêQÞ¬ì(H ÂC7±×n@'Iæ’4šVÐï°™œö1"x÷Xw¬ÛŽð„Lj´ck'‰æüšÂÞy¨c×ìH™ª”_Dç’U€‹^£–Ó;()s—’»úƒGŽ«Õô¸ÔatÊCôÏÁT:CHUÒ_é„/ gTPZÒRê_R5ìÇ›°IØcG‹X>b²äór²²$…ó¬`·l;„ä<×zŸÝÃé˜~‰‚–ê”·JLÚ=ôµÂ_a¯ת~Ïû8ÎËÿ^̉ça;JØŽyÃùGÌJç|Ò5( þ¦1pF—E.C^çÈçГ†O’r‰pt* KÊXpBxIÅ& ƒL 7—ÊuR  á&æÇä³ìàGÄy¢z…{h»öÃ_³ƒ(N‘\Oo§ô¬á£žb¼ópé!Ö Œà;ë_ÃgÎÂgÐ5|OøÀÔÌQÌáS¬¡Cb«˜#‡°6çÐ!Hâ]z-edöAù¡G@‰æ™MD¦B«–ëz–&d@†µs÷š¾¹ν.‡t¬•·ÈÎ^tÚ×ôò&ÅW™}⦑W©þÎÆ¼A²ÿ‰ÛŠe¸ä¥*6úqßZ?i›8 ä{y³ìž¸|ê“#¼]ùûŸFþç*ÍÐ_õq›ôzªŒmÀÑ®ÙíŒ ®xšdŒç›v$üiЛæŠ\vÓ•ÚŸ»‘,v+2Ëþ¶èeÈRƒ¯ÐÌ{­jƒ7fФ¹û8h‰yx­­€·zøJ(§mÌkÔ¨äaHA?BŸ@·ÀbF¸eÃY„³šVG ¤·Ónµð~ƒ?T ¹<ÂëvòEƒ/†»ÃÃî|VÍý—_º7ˆòãÁWáà»w@LÑ»É÷™½æ¾ŒE‚ILAD­FȘs´Ë"cJ|©¹«Rß2’PCb1–¸?PêÕCbù×1{Q̘;4ƒiwéÊÖ0Z"a:¦˜ï6´žˆ‹DUXM¤6*†/ãÄ‚AlGÔRiÖ‹Î u®¨¥(6¾Ü ‘1R‹Í&c$L$Î Y"n]&ÑwO‹Q*—*©Ý_h)ч_Ûþ#lÛŽÆ)ÁùMtyy…Í‚ÇÔ:)“x;›¨üQçŠXT ¥ÑëYغµ¢’sÅѳÃóè5æ·ËR‰‚5–_Wï¯YÊD:× ’šù!E! ZÑ­~¿’ )ŠÄéLúrÑ5†lZ" 9ïÏ~?|‰Óáø“'¤ŽU'ˆð=üA^øÉ„÷/^DíÄø lGFÔŠí1yWŠæòycªP3Ø ~Á÷°ŽÓ¡#W”1§ni–Â6@X%¼ÉÞiºpaÛE‘¦zù «'$„+c_l0'¼PŸ¡üÄ6 €Ï¨@àPPøV¿]ø:À¾øØç_Á>KP¤êAáÄࢤ£Á•Þ::¯ú„Ó ^øÚ6Lã~Hc×óqý1‰ýñ­UçƒçJ· _õ°ŽL–~ ¢Y¤Ÿ%;O4EÑãb‡Pšvçš=@`ß´áK¿9f›Xれnoÿ* Ìn“VF)¤] ©ÎB «:z¨<» (ww{°~(¾‘fPè/Ï<e´ë£î! WÈ–hLæ°¼½íAouÌâ«XòVF-ãA´"ì+J ïÆ–”9Ì‘*_¬½ÔaŠT†|ñÚ„?^m‚„V²Ò ÙÌü Ù3µÿúE­ ŽD ‘`æ× äèÌW Ÿ›3Ø+j»\0`ƒ§““Ë]{tÂÝžÖ©;ï„Ï~ ü ?ë%n¿„á‚Ä= Q €SŠ\‰î‰dOŠ…(‡’Aü•ܼðG´‹±HànŒüÅPÌ‚ׂßålŠhç®z/§‡[j”‘ŒAsœH kÄ Œp ±F“ÊË•K–û­mQU@#¡R¬Ùeõ±ôA 0&»«€!­Ø°¦Ñí3—t$ †h€˜ßiÃ×âz3ÿ% Í1„‡"ÅØÁ4ÊÙi’%Aðé½Ý»ï¨¹ ÿeÂg›o½µùì§ZÏœiÅúåçôJòWìh/ˆ+Eä^†ÏÙ8 ¢^iÖV’ˆéÕñ …¼CÖé œ§ri\*äлž¸*cŒúhQ©#ÜRbß B>/µGšKl›@.w óÅJžƒíֈ׶: h ¶ì ºN„¦ºB  ý˜>mÒ›ô°OŪHLoÔ»qŸ¢ì&½· j7Y&Hg@ïõGm‘†BóFàB±W´Žºšù\·‡3F¶ãÆë¾* 2ƒp¥êÄ¥jÂ,è †od ëbïe%È-Vž äû¹l®˜èBGBõ Ï_¨#"má‚­ÃÃÏè:œ?¥Ç^ûĨr89{TÌ»JJ9‡ÚbÖ±ž’ºg•}ë’I§ÙdÖ²žD¦À‘¶n]BÿPªHD ;ÓN“æŽØYmÿ@¢)÷¾yïÞsÎ=õ{¢ò‘IÃ1ãè6°þáÙgñï÷ÐCä6–‚2ƒý,åÈ)ïè}á?ë˜â»ZßüQ;ò‡Ñ›Á«Ì<\§+ üD$]µ#8¬Ül,)ô¤K呺°‡Þ\k $†R®ñÖúâ2Qvýˆî¢¢ÌÏ }½ÄvÁÙó$J€Æ6RS\8C\ãÓúü ¶K6å)p‰Í0‡l–@¹Á6“m}›ë ü6Fl*-é¬p2Û¸XÿUô+y©Q wÍÕEÚ6[¢=R7׆Ï{<£çåWòÖ ®R¥°{Îüg]æ×w¶ÿèÍ6ÔG!£ÿpñËb…‹_†“íb×_¾g1‰¸kÓ5e…cÜòŸLÑp„î/_ó#°Ð'Àh­_Ò#|yJ—÷úþÂâz_¸Bel*v–º´Åu¾p•ÊÐq–¹t În)ò[-®ÀOmð…­ÐåËÛ³úN¯Ñ[jó6”H¯Ë¼–¢bwÞ¿˜Nt˜ü ›&TÄŒ¨`KoJÄ(›†ÿÁÀ b²ëÈH¿B_[ò¿E`‹ _ ¾ ç¤Â·A…DïÒcG0ª'— ʾ¡ãùÁŸÝ9¬çù¡wî\E‡MÚ³ Á¨=›ù*¢3y¦…ý÷‹!ûÓ¥‚…Ëœµ >ì|Mq8ŽGy–TœŠ¥]EÞ@¥ƒ¢¶ýo¿ºÑùÅLkæÃÌ_2«Ÿ­™üÜ+of^5†SƒSSƒ;©°ó—œ^Ãþ;s-æAøò Ÿ Xúe•’c—ýe2 #u”EÄEUÓO²ßd\’$¹’„ 'œìs꿬b\GEÜBuæö›Yü›ê‹På$ƒñ—_ÅåI"*Û€ø½‰S3§ qžyí h²~Ž h”(Š ‚&‚@ "#PÜW›˜ÇH£¢2Œ•‰† 8B:—U"ˆuv<ƒi LäDUG/ë|nø©}™ÔíÛ“îÂã—Së¿õåÁÛŽ<|²ûÀá&Ýk_ßõì? ÜÌÜuÛÃöp"l¹õQ{$±ˆ²æôõ̳ܽê΋$Ð b(²Ê*IÎ9I°NŒ˜/Ÿ40&YÒë*¢Í\Œ¢ˆ4f_ÿ8ø¿§?…Iƒ Cu]ÓÓ»u!£xªLˆ} ž÷2¢âexí4þúÃ^4³§¼“9©øa!kû“âëÿ²æ°*ª:8-že/Óq2Æüžd1(†í€f!;ñd­š¡æ8€ð:QĘ@=“&À£&ž<Ü™ ç`d-? "a,ózŠ–£E½x–ðÎC†"W z¡( TŸ¥4óÄ\é¡(¦ŠtAð¯Åzjƒåµµº Š.¯3; JAMiVÎt´Êp¾l ›e tµ\e·;Ëã¶d±T ƒ ¨ÎðÞ¶5]BæÿµMt (ˆŸó+t‚¬gþ„0uP?S„"oD‚S2¼©€Ç/^l®ŽBe$ÈVlk€V:¼Ì0JPþöIÁ’†dPàqÂïs=ÁB³^«RÈ8Â\²š>Ù’,ôüx›’p"éO¶Ó¼‚ÿßÒ°Ém5ÉÎ\ö7–X]©¡°¿Rø4ãdþ4ÖW`W3|m5|ry0ì°Ä;K+&[+KJUðÎÑÕ`/û"d-B ÏòÑ8ÞCƒÜœÕ<:¢µŒ‡(*³ª Ú"0T¾%q«ÒdSºbv‹‡y+3 žx@m3(O*áÄV†ˆ"‘n¨'ô½G 7ÚŠ{àF¸žlRÇ«½[–_s­Ì½ÕÍXœæbCþÀß™­«;3¿&¸ÙúÌ«ðÚ? «©¸¿ÔNÁ’½Š-ªd™½$¨~žc &LµŠ½×Ýz·ÞësÉLè·‘ƒ.[w½ )±Düûƒd“ Úœtb¼¥7ØT/‡7%«³F÷V—¼Êè×Â{ç2GaQÄöô¡;\ à?F-Tlqù”õFµXR ‘"‘ 'D û¸™t«È.Ï%.NÜdÇ7hæP†(¾¥óÉ­PPå¶it´Š3 Ž€Ö…nA©Žú#ªAí+ÊüA¤ÿst|‡ÝM8ˆ/Šv£Š‡g¼* 6„µ‹òš¤Xñƒ1±JÊ-ª…(¾ åMŠ(÷}(; Câ2¸ž ]Ð%9q¯0Ê><u±a4,Kà¡ÄÂÈ1\Qˆê;Ñ!¦€:˜80ñ"-þ‡JÖŠðø,’w”WÆ9»_&l£5Tkges6Ú©ÕÉ9©(¨¦ãMÕï)„bå^Ý }¿i®Ò`ÄkiÊ&ݲ1ñâlL|Q¨˜òœÔ¤Ÿ¡42ÖR:ùª¬gÞSð?·•[,qÛòÊÌ Tg¯Ý¯mCYfƒJÆìÇ„Å-â°¸”â ‹»7Í*ätÅ1+õú‡·Î Jªä¨!F-¬e,?àø[9¿Cûý_*ù;Y9Ä+qîÿ7¨_R“ð>V¯a¡ß‡;‹üMÙii°ÉïÑUB“lIr‘ úׂкw ®b³ÞU 2¸B¦`|Т]ÎìÉÜ12ZqêÕ·ÛÍ@V)ÒU q}EüÞc¥tÜ‚DH¢i[xq4Îôã{ 3• ɉ· –¼ÝÑ NtŸB¶½Ý” E'3×\|“³ÁU¨¢©°s$ó/øþ‚ðöÐÝ‚ÖÂü·Ø´&ã"†/þшc9¨ˆVŠ“Mu¥×MZá$Ÿ¼Y¬õÈ:Ú  5›FƒGŒÝUH¾¸ŠrtÜbÔã…nŸB®Ö´W¨,dqXPÉš2Œ3`sÛœ~g $`‘Ëe‚Lé´ê†Y¤ÛJVî4ÙÝ&Ç šB“ծרåj9ã4›,Zå­Ålu›Ô2 ‰(´z´¤3ô05Ã%LDúsè9²§¹¤=ªã0)o´PVk‰Ôoí/)¬iÀ¶Ñ[ô0ø²<²ÐkŠt#lë%>‘âe|"§AúâÏùD(bª_bܨïÑ€ú© lrÁ“iØ<< z‚ÍZ Ù‘‘·Âa‚h¨«¬—‡Ñ¼B°Àè’i"P7qK{¸~u-é_(äš6¤{ïÒUuE ð@RYæ.>uÃ*põÔõ«BÎú5é-[R»Ÿž½¯êíôT½sÍÆ×?×°6íZ³áéÇÉž•C¦+bͶ:Kÿ‰Éx¨kKsíÖ‰6£åøÆÉvÔ‘£™x c¦axãá@Û†Ô$>i‰›á³»p='Ê\R#4¡&„ÙC!ƒIbš`P NûÍ66ÐétNãÓ‹^cÎg;”¼Ùâðƒ·§n/®»êô¶ÌÜfð`¯í©å~Ñ5Ó`3—õ$©†šµ×u¬ÿôµ+¨‡Aãü™‚ƺé½É⿈G~ñtŒ)%¢h_†U`n—€…p"9f² U¸©[¶–A¿Ãf6ê4<®ãÔ}<¿[dÖ¾Éz]È\éãEa¢®«tôñ«;¢ƒû;ÛÖ·kTj¦¤÷‡v¼xMóÔ³¿ºùÖ/ÔôqJ T:£Õࣿ¸sÝ#;j½¯‚ïïúÄ×<—ù¯×wÏ­îâhBÊÛþs#Û@¤Qõhpl Þ«ÃNâ6rK”L–@Pʹ@Ü' ?¡‘nA0é¾ g!íwqÑce2/š¸P-II9‚ÑN·p:•Æ[3Ñ6õÔÖ’¡«ZK{[ŠT:VÁ¤úÿí¶íŸ9^ÿÂoojÞUJ»šfV”¶yÜr§VU¤­I+è›ûy|ÖáGß¹mÃã;R*GÔ©dØ®©ÙÁû¾wÍóÀñÝÝvKzßxR£™°p ÃÌN‰:ëÔÒ› Ú²Ó8×üœ@1ÓdE!í‰ è&ä–Æ¦«"WŠQùmjXÖ .Zú5´ë#!¸éV——ÑJ%£—eET,Š“ÌêIfjú¹_ݘ™¼·"¦G½0ï­¾a<¿í…ƒŸ ¯ÜÝùîá߬ylwù—Ó™_}sûr,××·¯]ûàÖš>~ô?Åç=.ñ—†°"{ …QvóDÖ9°Ð‰ãøY ÌF8VíÒjÀ3Bí¦\ÙcõK¯ðqX*R>VK'2ûÆož.Mî:s0ó“‚Šš¯~ëÖ“äƒÏÜüáÛ¥c‡{ÆïÛZ—Ùé,w€7oB4˜‚ï-x?¸÷€t®¡äü~)¡_rM‰m5÷¨téÅ©·Ÿ'µdwUƯ?NÎVϸ?|ûvðõ›Á˧¤ç¾øGú«L9&*µÛp¨­Œ`¡†7¶Àµ|V D©ÊÅ@”•F*Š+ÜE…fƒÚŸaÎu¨BÛ·ÐæUd`*ÇÀEÙ…ê_«Ö>±'-è-*E’%ãLJVîk0« lqùþ#'›¶½|]çÔé÷Ž Ý²µÛ0_#Ø;÷*›¼¼7Õî Ó¦'vT‡â!…¬©¢»ÔÜ{ï›7?ùõë{署»grâîMÕð†/þ‘š‡²©‰X‘n‡WÜt’f0?#µdªÊèü@0ȸ®o¶Øåz,kd[ÒõŸ]PgçM Ö#¦Y¨à]µ1rÖL£^WÊÕþúµ-›]G·¾ª~£gOOÀÝ´6Ý0ÕR¬Òh8%]5ù?Ç»oš«uÎ5Ä[#úækÎn;öéZzÒ wxþÆáت±c¥}ËK‡ê½ÎPH@L½iªdôÚÁ¦ýë»Ìšx}«] ¯IÄßm̼DýÒ“„}€ŸYŠfç¤ÜÜ\Â#$.–ÅÒLË´}ˆ®tœ)Òû£Ô'r8 6„¬úýôýä/tó¯Wƒwtg-ËÊz·µU­ð¹åEZµG×Ð&Ä‹èïŸÊÄOeì+»•ASÈÑ 3ƒëQÿh¶ÊÜ6ân1ªJžK@t¸3È®?Òcâ0?Á<Ëñ¹VGHÏÖ²äBwe8C‹{né䔺düXZ‚>½/Ô@íx°@¦í±^Df–ª¹%Lw“¹b©‡k]«  ñÎÙŽ}¯ÞÖ[=wÏx|Uw›Ç÷5­žT‚“õåÛÜØwÍ·ïèÝò Ð<=qW-ånÛ¾2±Â_WOã3´¬è›{õr¥Òç.~äí›ç>¹§^ÐÛuÀgT­Ïðž nÕƒ?8ö(xm‡ÛI6\]‰:Ï0,^_¬#0Ü„èI©,ÊeÇö !ªJ¢l3éüÈz‘áÃH†m$p¡2P–sÅ@€”gŽ?¯×ðPQAfP>7ÿ°^8ÞW›î‘©ÀvN IE«Uó§È]Jõ…æßgÎ7<£äá>Ÿ¾øi…÷¡–rJ°$Ä‘lñx¤.Ü,Î˨Aü¥£òDSÏ®JU&t<ãþèWvÕ„]¡Î™zD;³Pv¿ÈÄ V·]<”䨈£Z߸Π¾aÅ7Ò™e‘ú{vFЖöÆ«»Âì‹3‚,´âê[NRŸøðó³[®9¶½a¼Æ ˆÿ¡š/|iäúñXtüúQôzžÖX~ÕdîeÌðY#P† :ÔÇÝÅ™}`‰ŒqÕM㘇„ ¹ƒ¹ïÉ–±óáb—õÈ„" {ùʳ@@¼T^µ$ÞÓM˜M9H“W=ôä¸Á"Ìj{XY´kK›¥8àÓÉÝUõM]‚ÙIÿVa¸à6¨O}ø¶RGýÌÖ]Ø\ÒuÍXœ–)øq̘sSÈþ‡º¦™)CöÏ8R‘Q×ú²tŒ` pb¨±¬.ohfÁBP¹´$²ÛÌÞo0À¡l-Äã;3_e à9‡ðªÓI'îBDz‰Åùh£ Õ_„{h‹'CAñÁ ù4ÚߣOŠí‘ùs)˜.ÎK˜m/>ÁpaGdUÐíòç7nùÔžú;óDuíéÉJìBÅÄÛë][Z&>þnÎ|• û›Jmñ@ËSSÏg~þý½ƒ÷|÷êç€ó«ºZzcCV\xxôé_Ÿš~h{ u“XóàöZñY÷B.=úCý4‘.Õ`Ý4Û–c‚ÉSJ%)Vì÷ÂÑn‹Ù¥E¹(-óò¦_ž¥G֤Π[/–µõØ2dëm˜¾u¢¸þÀÙÝw?Ó:Ûæ™Ûú4øŸŒ^Ô6•Î|m³lühÏÜÙ£í·”®Ú×6»?«wUÓq¦„°>„ÍïŸAzAm‘1LØ’`Äì‡Ücø_‰›• nñýJ~wz±®Ä.hŒ_t|nvèØªâꩇLUo]ë®J… õýëþ±›V—J_™KÚJjV5« úÙæÖòÑ=õëOŽWGêWwŽ®7Ø=vƒJ>зºlôêSÇÇS¥±úÑ&_crÀXá³!z:é©E‹58g§Úb®Á‡”ˆøÝ~´¸7>Àžö| ¢2Ÿè`¸ñBמQÍìƒ_ß¾îî ñæë_»aàž«zßvŽóŒeÍñ'Æç^~ààj%JxÈ\Àž”ksà|êH_¨ºÈ#/Ró6uQ¹…ÔÍÿŽTRF³.C8’–D ¸zã¨F;  š—Š÷í‚÷m€4i&âéA˜ŠÍFP—.pœ"×*~p†™0kµFÈ(F±þ]¬ËÕ‚y*áëJÚ #•òÌWP ýç2Ÿ%÷.|~"“óZ°Ä矄Fx£Ú®ÊÔŸÈDooU*À»'À §Ä{ @[õ7ðžÊ‰æ4Tk°5Žzº!ÃU!\;d Î âá~ Aa¡])¼F9Qn úa#+Â7)òÂB±)Ï·†9 ×±ý†'†Ì¡µ7†‚ëö^לÚ1Ýkë•kX™VPkV&ºÛÓ³·ÊI•ìžC‡žÄ­ËÜB%VíJ÷\¿s´¾È–èŒõÈiV­ û +'6E{ª]+TE*°á|¾ÕÿÃM$P}m4ïØ<ßš”€"·ƒ{¨æqÁ«?M‰‚êAˆÑPo‚E¦Y½ÌZf¨oêj !]¼¬ ž”Ý6qM»¨~u}ùªæ¸F£“±j¾&¸fëU©á{w¤Ûž?˜ù¡Š%yÀ¸Oe†÷¿¢¸wSª|ª=\è kÔ,%ôÄ:¶êM÷N ß·»…WéÜê”ÖJØ/¾O]„ÏXEJ«¨Å¢pÏÌ€Æ |£" ÜS 0ˆ 2‘ÔC­á¸?“)ìÅËÁUX³t<\n`ép”â>ä÷ùÂ8Å o•;lé\ÑEn÷qð1 GâT´d?`Ê™Ûo t¸6=4¶¯Ûs´ñÞ×j[åjN¥REš&Ó£× ‡N¯Ø[µþ[Z—¶Æc%eTŒþ±'’ù ÇUL_ÛÝurÇXkËd›œS)Ã{ÍôúÍûµZ;ø5ný*ÉÖg: Œ¬Gþ˜"h~CÞuª“¡0Ñ„\FÂ¥§8䉑œPJp\Ôõ¾`1òOè]4Ñ+Es™äÒ|©õ.FmVÿ4Âà&Ç]ª»MåÛÞØ ñ>lˆwÙ4ìE¦»1Þ9Ó±ïëÐtß$™îʯët¤Îì7¨Çþxèœh€ÿðسÈ7Gêü}Ëësz †q"\üg‡ôQB¼qN‹Ó¼ÄÄÁCÒç™q>d6«Z„-DY‰ðÑ¥S߃œt€Gi\‹f-Z†1G/3”e•âxqQ´ x@ò8EŒüØ ¨ªBYC1ìuùÝr¸ bŒH\(Ñ#ŸQ­Òš”KŸ ï×¶÷´™®Ž•]r+ |=Ú•¥ÆH÷ìþkÓ­W­ô¨¬ó;:_|òlygÇY°ïø‘Þ;¾{¬©UîœP¡–±²]¯jGt¼êâè0”uD?²JšK ¡n(ÖÀ‹Ú1$úz»k«}ž-”rø¼'—¸ž+Q2ŒXbÆ­~Œ[H•ãN‡Y¹H‡#»â“má¶ã_Ø·áÁMÉÚõ×¶• §ý‚©ÈtĨP™‹üéU%Õ3ÝÅJ^ftn5hÞ´O%v'|ÍSÕ­{‡Jéû­n³[7·®{lg]éÈáî©7t8 ëzÖÖûÓÕV§Ñ³*ﮋÖö¬o²VME®¡ ¿¯XÙX^h­[9ÛZ5Teõtl…tW”y‰Ö`[ÿ ‰Rá^ëVôw!åÊ׈Pú Æ,Ìöʳ; arͲC³*6†Q‰©EF£Ëë7`5ƒ…Úž«9ätŒdqh ÏçÍä2Aíœmno´ñf™PTè,ñz¾QN*dä»óÏ@³ÑaûèW-#íW­*)j7Ê–X:àCš m=÷½ÚÝØõ!?Ž’±$ÍzœÄŽ,!‰ÑRHEÎBŸÅ‡šK±¿^«À¹PHÌR ç!JâM£N•¿h±ÜÓ~¶ýnõ=‚jòޝìÞõémQ•p—†Œ¯8¼:%æoä…-ÛÈ_uó¯¢OLe‘F§NÌ?ƒðH§V/à^•y‘i…v@ ÑäiEÒk x2hIIPÄšªÜB&Á' â‘"‡#z Dûv¡nÓ$áBS,M¢¿zš7ƒgy„\8GpÏA…yaöå&¦SKç àz@̉€XèÄ’ùy•¤H,èÊJkªJ›ÊšÒZ_0íiÁ¼œ=½Ä©V)f"¡êX46–Sgé)!Þf r% ¬ˆ@T+guã?Ú3ý讆­ç|Í jÚª?òP•`ä5óJ7¬› ­¬8¹fýK'{cƒ» 3JÁ(Yä±Skgºnûæ5_Áïlé_o¶~$€5–ªU)òdÿàÀc¿¹Ý›*Ñ^ÖA¶aŸD€H¦:Ìq,T R,Eá8±â«™’k*àsÚáxdJJQcȄ޵Ø;E-öNk9Ó|·úî^0½åáe¥ãÇV:ŒÀþhTÿ‘PäÙtºï@øØÑ3¤þ-ÒŒ<\<¸·£ï¦­ÝzƒoþO>ð¦Y¬¸wkãú91ÆA·b{®uÐ+0@{ 4Éûð}˜ûˆØTa·!›N§Ój=z—Ü$"™81KÃý€Œ#žòTâ$.ÙeÃØƒÐè¬É=¼Ëôðkÿí¿SgjNþ‹œìÚ‘·¢25dS‹Y÷þûjKæ—G3ë3ð ðôÑÌ”¥ÂþòyGÅÅ‹Äõ™ïÒóHžÉB\ È õDiçˆÌ‡ÄꮳH¾ Ky>ëï1bÏ­¨ÚáÊÊ¡(ÍÐ#KG"Kõ{««HDB.§Ñ Vò°*r á9þy¼·%D ñ£´Ê8ÞXJ9•’ÊtËŠfiŠ#x9àhž›€ÔŸ†ÃÂ1à2ΨuŽF (2³ÜüK¦¦k–Î,jÄ9w¹ÙÄÂdÈtz‚¨©Š#ŸK¬Ø¥ñû\^í%w ßòx¥çcèñh='”·ÛCˆãä¡‘¾¹Z-OWºFS[ÞXº@¡†¿.‰Bƒšáª'fU Ñûuu—úO‹ÈÕ~h2dÉžkë2ß ·Ã}ð é”`°$>: &ìu$Q„¯5ŽEk¹@x\» î¨BËx–ð¯LqåÇš(\S õµ©„* Õë²É3¿Vx¼égŸ<Ó^ahÁ†õ™¸Á£Øqoœölö´¤`6 (ÓüW5&ð{³~þ[&;¯-ËX2;Í6 õÀóÂûOÂû7¡ؼÜ´·¢Û¤YÜ#8ÚD˜ŒZÖˆÛÆŠžé¶rhRF*… ’Ï2Ny 7ùç}è̤¸ù%—ÉG¿bÄßÏž•€¨…:ÒÖÿÿóŽžÑ(¬ÕÍýåÓŸ>ÞŸ¸®¿{ËŠJÔ ~ÿº}_>Ù±þìï¼÷ ½ðQż£GþóŽõíLeóކø×#Ïf~ù]yyG.¸lŸ‚ë)C¸%RÍߘ˜Ò%* WÒ-Y@.)Éžñ 2Q)Ò‚wÚÎ>1o½|ï™üðm8{'¼Þ;ðzBü‘Z鎋×ðÐ l”CäŒvþAHgÉwnF¼‡­ÿ@ý…¹pÀ>ø²§€§d|,Þ/¢FÓ5åÊ1¨4‚ËjÆüЍÇÞÃWˆÙYânx×9áI-Šj½AºEx„곘ҋëî8Ò ˜«#›£wðËiË(~âm}b}Ç®•áÖ#g·N^2Ê|Ûo¼…Ô÷ŸHUû7¶tŠnÆ8Ò™wÚ‡âëïœ:þv4zé•™zÛ3UéòÚ=Ïï¼å¸; áAbÏËÔF˜ì¸ZóÙÈš=5ÈÑ$Ž} ÆOË BIþJq(!…` V <Š,AkþZ|~LIè±÷}é³/yð«É?«æ_ªn}b]û®>øà/nnâà“'RµþÍ]‡c³ŒîСL¢c¨bæžéã?}j=45Ù›.o8pfÇmÇÑÑŸYŸ¹žÿ»ÓZTj72C“Mð´Ai¥^ °.6 —¢”ÒdQ¼»¯\K¿G˜ JqTö¤Ò#Öå, X(ºôèy|ä[óÄ»¯~G;Ú>¹±óªþHÛµŸÛ¶æ@Ø$ó‚Ï ßP_ØÔÚ{ÍHÉ6ê} 9ÿrÇpÅ̽kŽ¿ýädgÓ˜øÀž†ò†ý/ì¸ýä•)ø¼÷1õDš8/ÖIª’€çb€`mÙü“$å} Ce‰%Y”@„ªEJ¤fdij D$‰LÌÍR×6‘)ê^ÆÊø5—¹ä‰+ÍK+ëSÞ@Ðïñ{<:äx)å‹’ JZ¨EìÈ41Åâƒí”Ë4*•½¸±¬ghïluO…Ok=C“k˺M&ZŽ}~oq—KÆZÖ6utBÃFPí5•rj=¨“ ö‚Æ}Ϭ¿õ‡Ï¡©÷Wût5Û^w䇌©”]©mƒežN“B=Ú‹è ­³Sâ¥iÁ õ‡š¢IüäB!©¸Äf©=2{•ÈŠó-7é ÁmDYj«Eä%}ÐëÆ¡mBè_ J¯wéÒƒùÿÔÍø©ÖOÍt@FjƒŒ4²'\>ÿÙ/CéQãßØÔ ùhsÛ¡ç9Ð3™œ¹2Ò““©JðÔG›z:ÀEFºõ8|Þ‡/þ/]Ãì$âD±:-TyH‚ŽãÈ—è †D£Ñ\Ö«Ê›¢ªÜì÷bÿ‰‰¼oÇÒòæÆ’b›Åãf´‘ŠÖÌ9ÚLýœ0Bmkåy C’T6Ü®GJ71ˆ´WVKJŽ@5Îþ’žMúf,­1› ^ë2»Ä*GV‹«»Œ ØÐ¦Áv·µ>±!Hv~~Gó®þhóî>}~•œ½?ݳ÷_›Ú’9ÇØ<ûzVzvt¬=}×53+™ßÒÏôvÌßY¼çŽOoœ†&±êsße®‚²äz‘°ÕA;j9* ³ÀO¸üOƲMn¤òQ´1øÁbFÚ%ÅÞ”àP–@™Šã–EŽËç×ú‚>b2.¶5²io”dŠäsÞPp}iH_¨j‡ùÁÌ}j‡Vm€á~ƒCþ_B5V}z¿LžXu 7Òì·ñ&…à-êX¥¤^™cr-èÖè2Ç4…2Á©ËØÕ:p—A™)¦^ëÁT¯]!Q= ô? ÎÁu*'ÆÓªòDëB&&V”a¬µlÖ/.‡40(eÁ¡*ùEßa Dîù˜'âó#„Ê@ŒBÑSìéu‚J3§w€\Ú"îLŽì\í€Ó5«³‡¨h ë©_è-s¶lh,2Eš3ç¨7Ã+*]E>KîßÐX»õ©“?~x¸º¢¢ªtìHïüGÓm…à¼ÑkÕ´z~6=Ó!ö$ÚáÜá^ˆ$ñ Äv@SÅ€äX-æ ;`ò?Éæ#o ˆÐŒZáT 3 3Šáôæ¬{"JÐ Â|žËvi½âx(BÁÒX0JB^Ñ=2ä>^¬Ë.ÇæEÊ-¸CÛòÔºI‘ÛLž~L.¯½¦?Ú„˜Bð{{§”¿U’®«‡csÔk'‘š+êõÍu™fÆÐ“$|¯§#_ËG¼Òñ}J i¥åtÙšB]®¡å †L†žÉ*æüž_UÉD™×ƒsº|ùÙ[RÛ»¼Ð›.`g ù½Ô§7uîì ”t¯YÛ]2Ôm)÷« ‚£ö¡5m[WøKº×Í®ë.1„Š+º*:µžuÎU4ùš&’mëÛËÞâÚHE›©Ðd¬K¦¼ #å-kZJü¾’Æk™ßluÙ” Œ÷¸=óù-FMT¿Ï3½ ‡Áİnáƒï­‹>•IŸJÚ}i®Ä•!CÌà¤túc÷ŽË›sª¾–Cg[Kæñ¨±©lnÙùYMMO—_n¦¤ˆ,sÜŸA™,÷Ð^øPÖ—íA…¢Ü$†Æ™ûbt¶Ä+—h#àßÍåö²î¸ïܩ֑ܣ½´I³* ´ªÌï:ùx̰ùNl| uC‹¯5`â !âL¥^P¸V§È1ƒ®YJUºÌ™Có/5Ôªµj~šÚ6P¦Õµ•ꡱ¾ê}òf#žÍÓù ðÅ€à$Îû@â×0ÁCÊss4ÈécH¤1ý/Õˆ‰AY¶Kò`Š=D¤I öeæàFÄ¡€×ítØm¨fY­di"br”{æ×’Êå"‰\+ªv‹À!Áµ;7«;w©ÐÖ8N¬/N»ŒÐbÜîTZÐ*ó^_ÅË µÌPÙQõ±ƒxé oîÞ¦Ö4›ª¡áàÖ§àò|8†yuÊõ(äU/q\$OÁ- ƒ“­¬è}'Áëh"«BÎ" xªK iÎÅíY˜{Bꇶèû±±ó._Ì’§Å½T6M«Tê/9îÓ‚Õräõwï1© GÊôòjbu=¥Áj'"¹Íf‰2?…6×uÔƒ&Õü=†°Á6‚?zëcVµ¦šŒsDAµ¤ƒzƒ0#_#…ÜI('jL‚ª'Å> [Dɶà-­V5=ö’!‡#Bs¥¤T-½ç8°ü(Õ’÷ÊH­ðƒÏª2™ÀOtóke¤Fñáëd50œh×y”™÷÷e¾v¢MáÒ Yt²Ì“Ôà}” yÉ’µÁ£ÊKšDVû-yšEØxxÎf×2-OЧÕp& $tIÖÒ2)K•_!/(æß=œœ¤¿4}Óh$²rwwzžsyW®Þ–ÞýÂþTý‘WNdþYБÔ'23‚YŒ®ÜÞX;Õä)tBih).Ò¥6ÝÚ?vï¶z ÕxÍbï†Ç©ø,Uĭ缨Ø_²ÁÞK‘l®øM$”ìcXE@-h…³‹žަÎ{f”Ѹt…P:Grñü’Å|ž˜Q«GUIäÆ›Æ Ë•år]rÎ|\±^‰k¨^©i3˜œÿ@E¾ÎSFM曃û{|oÿ´e¸T×ÑUà-Ôqr#¥ŒTÜt{pøøxÿÞNÏÖ“‚Ít'ÚÍÕŸÃpâÞ/š9“Ãk”Ò,­0õhµ›§+'Z‹yΓl4uãý·AÝr®Y%Ñ”nˆ¢ŽÅ€bŒ8·ë²I]êü,-}«ßï .¤h-Ñ –dèI9Z¬;›£ÕK•%2on)ªw ·íttËK••‚ŠgÕŒ,ZoZßäÚ½-<Yû˜`ÂÞB’'_ îs‡ßcëtÕª¦â½½I9Ë3r§Ãä­ë w(•6°xtÄ…½øêy(g‚bÿTËkÍ‹kÃ!@D‹Ceá²"ò¬ 2"‚—Ôú/) T‘x¡‚@ðRϱ‰8¯2(È=\¿¿~uk‰FP3!ïê™-‰Ñ;f«:OþãÌ¡‡í‚3”ªâkn”[y•CóàÃv¯]ÆaTíΧ6]ýýO¬:|÷ÑÔÆÎ A_|)óÝm¿zb%1Fl·§5ÀËY™ÃA7»8xÄJØœ¾ÂÚA‚óH%:mä Q{I—ƒò%c4²¾š[׷Īú:ë[Ò»Oo‹€BŽ 2¥w=0Ö „«V_uhßêªõŠý–ñÃÏ<0SAm!×ÖO7ºç6ÔM7¹-‰žÂÛ>c˜„ò¶l‘¾æñ›ÊRjyž3•±ø¤ËwUW$X»¥ÞÍjÊ«Ñk.Thóh –HCŒÝU:Øàõ5 ÅÈWj0Þ;YT;÷7Ä,ó%uÖRŸñ‡ß$p,gJ\~Êå'ñ³ñœ^Ê%.‰px)d£Ó$AOdÏÐ5Y1sïô äŒN’öÿÁè×Ú]®ÊvŸ¿³Ú[QªóÌœÆ[2ì Ž'Ú{ÚÁOÒ‰†ƒgvÞrÝ‹wuEmÔQ&Êÿ÷i 3‹óÎZҲ樔w&Õ"mR•Ÿ~fÅŸQ`sÞgÀz»«+=®+e¥%'¥a h””F-MJ³Ôm¼®­rÓ@¼ýº—w¬¾g&Ù°éú޲ñÖPqðA!˜®êîHõÆÞ2Š”Á=wÎñ Žþs°e¬4µ¥¿Ô›I¤¶Æé×WŸ\¿ÂåjškšûÔþt|âXÏÌ›»\ŽÆ•ûöV™ƒ“%åCí)›5=°¥ÝÙXXàRB¡ _¬›êi°;Ú'¬¬®¶û:w`4óÖIÄÓxiÎÙq]µýå@Vw-è¨H‹«ö›sŽ8Q¡BB?ˆÀ$”|´Ü„¬?ëT¾ÜPŽ`8fd¹X­’»|¾¸QkDºp(ÇÇb¾Â¯½¤ûd%Ù?ÿ=¤“±œ~<ÿ¯`»Å°ÑÄû›×4zýfdÍøíUr¤“ߪò| €Ïç,=Íð€èŸ‰e$Oà^ÅâÆ´u×ñX¡x*ГPg’²MlÎN…L jE9 -È®Žœ ò¤48orÒJ_¢ Œà189Dç*B©IEWÞ„3èÇÅ‹y5°À¨•VÇ›àûÏ"^--k k/ŽÔë(ÆsY¡†“?u°ò^4™ 2·}xÖÙ­ÈØ­£Æ3–6ñO°Ó•¸¶‡‰~Xá÷d9 ­ËlæVú»PÇ*'ÒÀ6 F-:höW…HŽõ»HŠ£¥žPèËË!½cÂaï4R¹0Iä*]Åt×À¥cPð^€5.^ô·\m@ha0¶kr1ÝK'¤ý c9‚⨑å¦0¢†`LVDª¦"L£Ú•’`Øç’Px nÊ:Ó¹¥Ærlvò±‚|™:{õ’n}tM÷ö&“¼íáXiY ÚâÔhêX»A_,-(µ¨9y£ªoºýxj”üíe îe€zC«ÛÔ¶ Åæ_Ç›êªlÃ2tÔá*v M-?ÚCjÄÀBƒß]ÌÊÎÖÌ­äiFMĈâ´Æ X:NÀ=-ö“ GI{ê%h.ÍŠµ@¿Õ‹¸\ÜRÿ%CŸÝÑ%2䯻2вf‡åHþh±M­¦´$Y^RSZôkµ¯Ì1£õÎs)¡”Œ%;ßâ­ñåmÇ3*N>>§Ö’üÝ=ȧšWÏO“4DK›ƒhSj†«µÊu²Â`‰¹¤|-EBU$Qå{"q¬‡ü¯<¯Ò…¿<«Ã%1ÙˆÚ©ÁrÜWT[‰7B1f¶Êœ0QMt@©£Ò–¬/ñ›(†ËšÝØ‹0æX±þÐܬàIŽ#† §¤:5ÿ¥ƒä(´(C¨zUä¥Ýµ5 ¥)]ÓQÛ‘(‹TW{Ý–£AêR¢$­K"Ï—¤8Àïç8€EzE¸©Ög6´ïBÚ™-«v‡šOw4?¹®m'þ`óäÞ1iÁcýצj¼›º‚páôο¾±ë|çÙß½HI˜ÿ‡Ž¡Š÷À÷O¡Ì£šïiÏx‘—xç­Çç]¹€öÎ[Ž‹ýç'3çÙ&êm¼¾h}¤µÕ’Ë–ý¹ñª¡¶©h5Õ⊉‹ˆÕ3QNȱÈÁCq`^n`n}›k:k;/³¾ ?ö–õ²/é?¿g.]ßÊ…~Ü ZŸ™iÛÕ‹‡6Ã.¾¦åñµíhœÙ<¾7hdÌZ0ÑwmªÖ·¡ i`ñ—É|à Fg©7<{&Å5}z²¶2³ ü }°¿r²³®ñÕ½Ÿ´Ä'æ‹ &vw'ZãàŠ‹~¦ÊÌyÊNýŒAºŽjhI¬ˆÂ“¡0ŽjHŸÐÒ'’—´˜$KvŽ@˜›!8ŠD©Ì|€y±/'¿d8MrôL6 %7+ϧ3–V ôõ4ÔU%+_”סìòý-Y)Õ94ò“11&ßÐ ë|õ½½õ¾òµ·¬šº-á5J•«¬5^ÒSéô7ô õ5ø£#GVÚìzw Äa‚’‡Ûƒ+ªËì–²X4+(•™´ZW¬5™èN›¯"è,ò–5•Ôí­¨I4‚©ÐT®rº!·Ç_VnÞ7QÃY‚Ö¨K¯R;µZcqkiq•¯Ðê±A;Åæ²:âiÑÿ÷CÔõ¶œ3Q˰²Õ™ ¶Š抷0‡s£ Ô£I°t4±0[f>Ÿ+°`™q‹VŽÎ³Ç°v–l( "ånkH›(Y¸5Ø>Ñ”lžì¸.O“£nFZZ-é,IÕ×Älâó–u®ê,“´8üœcÁ÷Q?&<çÒ‚À†Q,¥Îå°xô{ÂbJpðè’Jpì»u¨}u[î¶žöŸš­vÖOÔ5Ít•k òc|Û¡ôèmë+Èûoß=`zÔ ;ۣݕECSï¬ ™K‡÷a]i:ó ÝAýj5ÄÄX=¤}”BqÃ{à +qOæ}2& @kNt€cV6£J6ÉÐCá?v(ç4qãÜø¬Dsˆ’®€º d/·$0zÔ¢Ùy‰…yðdÕF‹ËãÅ5ÑŸË£õ=rSÄWy‰¨B5—$1Wä÷(L¸‡ ·N^63…º »IALÅrÉEW”¶L$µü£P¡w¶©ûêáØ¦’5ÃI”÷Ó''ZêztÚ =ãCj…©E§)Ø–:Ý Å~}E.I™9Õë7ˆBÂM¬:o7“b $r4 `%qÌ8%†¤²(™¦ àÀ%ߥ…'ß#%œ¹¼(ý^ 1x€_pYî¿Ël¤ÁôÕ¼C{ý]w«^É›åÈÛ~|á)^/Ü–yŸü ÐUï½§6d~·ëÂ-¸‰4eþh QpÁ„hêh»2¯~"NücZ0ДˆaîÆ.p@@[ÚJÉw¨’(çHÏ8[n ¯ÉKÆDÅ:MÜOt.,ÇyC¡íwÙQK Ð‘¯Üð¹‚A„ÕàÓ/nšK©»„^ô؇ŒèIpߦT_¢¿ÚQ:¼·¥~mC¡ìÃÞèð % ¶†»fS:ù}ñXÊD¬¡³‡Zò4Œ&»–FöwôZ¨úÛJ… ŽÄäµ}ô7ë›7¯©oD7]o÷"SJ$‰TºL€rE!YÈ2Cðb0§Øc¾?[ƒg$ ôåZsqUÕç•/b›Lò³¨‡"ÊáȦÿº*ɳéV[¢5ëoŠ«á¢!óÈþ;Vv^=VÖ\õˆ:3ß“¹Åßëw[¬”âÂ+©Ó‚¯@‹$³YMž]q×higY£¼#â22ªñ[VÃÇÜ·bÝCmG»2 gáÚJ­O ž¥Xú¨x.×\|ü¨ßWé¸#!@Mƒç0`"? >ÿ@±Ç_Œ#?cg#g[2Ï×F‹eÉ¢vM2¿ÖüŠaíñ†¤¯rÝ }Ññ¾FCµZ¯ñ–6F£qk *·ÊÙ)•Ž”ÝØÍ˜Ô#Ô—“­åÅ5ƒé¡ÛgkMþŠ¢zµÝkw§ãåÍñXÓš®hV™Ÿç¡ÌtêÊzBJ¬sBi/sµy@#®¥™11Fbj'Á2ì–Ë CÚ°R¯ÓJÿ ¬8=NSÇMQ¢9<¸*]?k{N÷€iUæ1þïÀéÌ“j0íÈ{—y)UàåY¿¾@ œ*‡ù¡Ì§NµIøüKå”ô)4apÈ^L–âêÓg˜Û.|!›2åÒ ]æñÖ³ySð~ѽ»á½o‚rÝ€:ý$Œ:׆ «&¨ÝŸQë1ˆA_AÞån¸¨5H 9G:”Ó4ž?'#ÕJ²'£U“ŸµŽTf ú휢AëTòós"ùEZ+²Ë¬E6OKdSñÌR±‚¢n–ºNå>KË+¡`ÀgÐÑêÔ÷¹€?‰î, "HÕ>f“9»ºÊ€‘¹ÅðÚÏ­•V£¶+ Jž÷4¯oˆw–š}ã]:™É ’œ]w"£y°ƒgµ#A_E ¼Ê+˜jƒ—%YÊoL[Ó[ºÂŽx‹¯j²+eþжöòjµáB ù+cT_îšJ{bC¼>øwáZ/­ïP_¹¾£ög¾¬Mñ§ÎÿôH\î¹P‚xÊ\¼ÞÒúõeê;FÀW4™ÆäÓž&SG3Íp&Ô­.þ±QïÀ}¨D=-äPãÃâMYä;3v `DžŠ„ËI„@ˆÎÃâ1›¤ÆžZ³>/–—Õ¡Qqø–ïœl¬ž½c•‡2—ªuº;÷´É+¶7õ+]ùÌ|ßž‡§CŽŠÖ@m· jÞ÷ÄÔ†nßæm•› Ê>´ùšÖºÍIYÕ»g…7½ý¾áã?~lìY×þG&Ó[z#«1-o̼Lý‘9NTω²L§B , eAx…ÇÐPQÌÿ?”D_(ûÌ*#ú)lœ ržBœ‹ªYòtJ (ðh”§s¥9ÄÂ\¦o­ª2iözZ½ÏcR B)ax!¯%•Pž¼ w~ÌÊOê®™9ƒ™”ݯ1Eš¼9i/À)-<ù`çt…š=@sÖò`¨Êo °þ¶¹ËRåN± :j/5A§ñºÀ"ÈuÕNÀä/»˜åP¢Bí§ø) @FÈe“ܲK \Xn伎¡iüæ¿arc—гòÆÂÙ29Ü!¹\±ì¼¿õ‡eä°Ã-jkIÕ@ë:l÷Ù½ðÀÊm“úã¶ •¢oô‹šb0‹:f8ÈËnÞUÏc!û¿±E&ï׸œýzgÀè/Ó¡ÿÆ3ÿ¬wM:§Y©wº"³êrÛÉoZ¿i4fî3lÚ„Ïpè*ü赪 H‡Þ© ]bï¹™ól)ä-·ü'Qù®"ÿ:5 ÿFÅ#ÍRj%ål‡ju)R¥¤š:K µ;ü#[jeP©qŒt(V(³ET(†}©QPÊ¢xåQ" ŸP6«µ¤R©$T*­*k$×k’PJõ¼’lž’Ò…ˆË_‡P«sWI§.EÛÏ]èc§C¢pÕÖ0°²±¡f¸vTƒêªÊòx4bLúLÛZç‘ðSá"rN¸žDÙ-òˆ®ÈÙz âä¨SFS­›ÈðîÒot—c¸ìÓeæ¤uð,ÿ ¨© Ä[«$Té˜9¼AJ…ŒÊmQö .Y›àQ$;eI|üÝÿí û/šþo¿èÇ` Óó)O© /?5]–…†œjÁ-;GÂ2w´·µT%Ë¥±P•„a]^]q‰Çï¢#xqyØRÄ¥Š8ó…Ó³™½³ð?SðlÞ…ŸŽuU[ÔtÔé¬ös2ð¬ÆðÄ1“G« ­…]P2¥Ô™·÷ zz´¸h9s€JÎÏŇöÂÁÝ¿PMV¾ª¡;O,"½PíáTPáòõö«Wj‰V°UÜ_9¢nD¦ø\/‚ôFê@U&àmW+Å}Sጵì;qYËA! #‰¿ý"~œÒ'ù$ Ôü Wš_†s89Ëõ¡‘pÃÊ'.¸ü`P«@yOhÛâ‘}‡Ð ˆ~–"‘ö¡dÇ)¹œv”»‚Š!ÿªYºÿóïþ®ß5ýŸñUóÂïbx‚¦Ñ|ñj¤T^—{}™K¡~FÒUÐ@øy×[~š$u]•I‚hoƒS›ìªìBžÙòD¼L«ƒ:b]¾‘ýqb—Êr×%:û©‘¸GÍ‹$îì—ÑÿÖ±:ÔyÖ«¼zú ëoûx¹+q×ò*¾¨ãs+ Ž_AÔ½ÄyƒÄG¼• äå#ñSŧºeÇ–kZfìø¨^”P›œÒµ¨µjRI‚9Š_.ÚYÝ^ªAWQn&@ ê¹…‹‰ÝO?æ èñÄ+\æÇáÕTêAxYµâò—Aë‘Z|#ù?ÿW]Ÿbâ5þþAÜV /B/½ˆÄ$ ®”Ë\"]-ÎF~GÈáË^eù¹«ù’ÕVO ötµ6'ë“u5U—°›áïc·+Ù^Ùw? ²Ú_Ú"wkŠœ+uΠÑ[ªƒF˜·,ó:2ÆÃVhÖëäÌF%#~¦|*L“ŒR(C,­ä—W£H>äs¥\-Ï‘ò XÔ.ÌÕà^!óŠ\­ØËš&‡£ðK¸šÎ'é»ð-é\ L´ü$ÌÛä¥ÚžÝ܉q?µsòp@D‚ÄäÉÁP{¹™¯•ˆ¼î Ò¥×—! þñš‚9ïG@ƒSõÉ©eð+×{ÛÁ3tðÒ9fN[!_»c:„Q®µŒÉµÎçor $`M3¬u¥Å^°Øï{ÂV³^«Rˆ,,(p„efI;£JéG°‡(²lââLeï…Ïn©œ›ì6&¥j¹75X›Ý3R-XzF¥h>ô¡ŽÄx6À¿ -ÕƒÔÏ2{»íû•î”§AnuZ#ëo›Ré6_9ÚtÜ×±©®aS»Êñùüq(ÇCG¢¡Â±™þ7yä? BAU Ê^†Æ&‚r²“Ä ¤C|~#ÑM|œ‘Í•:ÈKÛjR;3"lHpÉ4û#‹ëâCµÊɪ<Ü®lº ÜÌÐ"?#Z …óî &2Æl šº³ÝÈÉä÷YLj¥LB4‚FaIØŒ›¡µ’ x]R§•ÕÅ<µ–Iއ­h”eý™!W¬“;'çꦚÃr‘”ö¹¦æ·Vµ™ÉÊÿYăZ»£ÊêÕ‘¹=”hq 9d/h?÷Ùù®ÔÒ_)Ex@Ú´éHÒ`U è×wï.&Îûzv··Ú´êÚh³L¢‰J5®d¶LcáKùG_yhôxÝŽ‡×ÁÙßÌàr¬»þêI¸i¢#Óš†š`\e7x?¥uu¥¥uëk=NÝ·º´.*¥Ììy”Œ"HF¢´¬î‘B"q¤FÒ™ 6[ýxõAÉË÷ÚêFͲA…VXî^·y{¼ïâB퉭å)—¼zÇã[ö=¥‚rΤ3R6K•K³g®ßQÐ3P–Õ.¥»e®~ÃÍ6y0–2%6õ…£LÜÜõ¿pæà¸Çˆ—’Òw¾plTKñz(ˆIÉuCz¡!.¿Š—XjL>/àá OPi@Y\’ÇŵlIANA®'‚Ú4–„‹àõ…†€Gð^öŠïÖ~"#ëïíhC½!·ÝŠè¬I‚2€*A®•î €ä Ñ’yIêH‰\[¥Å‰(Qhn &šÝ¿kœmÝw¹»bb°Ý˜ä*Er{¤#­·oÚ²Yº©E¨`ÁxOÒ)–Jùe©nï?ÜïÜMþX¥«‰¶°dYÝ\¦ìwtªPºª½ <\7k¬ÝŸîÝ«¯¨sÃNάÊ£oÓHÈn­…T8 ùÓ5¸v&¨‡2ªnˆç¢:†P+ý•@]ù|>—¯\ÙÌ@³"&€Ž®¨‡Y̧®©C±öÁä¶Gö?w ¦ûž×.Nß<ìYº~weoÒ¢­ìOí¾wÜeoÛÙÛ²1mÑT´Fî&ß´·dârËÞÞÎ}þú=Üxæ ù³£¿Ôùkœ†ˆ[Û|øÉ™–]}K¬=P–ð阳$÷!Z ÏçZ¢Ù—0âUàPZ¨½g¸$_ð@ ¥WþÊÒb Ѥ¦Ål4B `Ï\‘„gG‚óàJ'>A-^8‰‹ùgáÒÞû™ê•­ñe‚;öîOÁz¯µ¾ÍMuõՉʰ×ír ¾+äµ V ù.®z±2Á‚ZäD•äB[É °5ïèðd-Îv´`AËoæUtln·ù´×o«k¶ˆzO~|ËÇ+C­BéŠ÷žSƒ“‰&EˆìiÊÅÕ+™¯ÛdPÈ„.‡ƒ0õeK`ÙW€û-¯•Evn«Òß®“Ð.Û°#½é©ƒ™‘üð쑦’bJå¯îŠ!ð‡Šþ=‡Mº/(¤ësÇM^“¤éÀŽý뽃­Õ] DlÔHk·?¸¾sÿ :ÃrQÿE½B81ÆŒÀeR‰XŒ$‚+YÄ*ë‹,‘™JFPð3EŒá‹¸H *üF½TL8“†r; xaÙ¹ñ«cö,…ÁXeÈ ÎGÚÂZ‰Ñ­oÏýé.¥þ—ˆA„oVÜ« h4öô:²Ül³Ú—3©'{¤ƒsíÆŸ}â£ÕÍlèÝݬvS¯,ݽëxGšüÞÛ¿ßjêEà¯ä.„7ßðÖ[Ž3ʈP '^Ÿ@Du/ôùDl~"q°f>¿ B]Ls¡Úh$À5:oxxë'üNÇîöáüƒ4¹’çù„„B&ÂÎCðÆÇT´y1ª×Ù½^"–5`¦wWV/ì¹Ò#íŸo¿¥~œú¦Ú£*5›ÌiêÇIëw“»  ·¿ìfÔh);~~Ð\ìßs­ø•Aácg†ý9÷P™:_éô°…ptY¡§_¬Àµ@o«Xó~œ÷(Áª6L®€'àòß«íêøšÂê°(Y¥÷zB&¹ª1›ÿ®Ï`±ŽLUB¸Ün¼æ"ý{¬ùJð4xßTÐ ø¼@Çæl¸Í«ãë…¢€µ¶N$ì|OÊx²ÁèÈjFǹ¾@'x€äŸéK„–°[2B àqÌP”%Yø!Fv¶uHU“ê1S4¬P;âÞÔª[Ù\ 8E6«É€Êø e‚Š*¥\bõjm-ÀiÙ\òÞ¡$Ÿìz¶«91‘v8ëC´¦:÷ÇlMF Ü‘T }iiäèÒƒä¢!ÒˆôU—™´àK»|Óæ`Ñÿ®Üc¤×2˜½*gsw™Ê´hAq¦îð2¨ ={œ.2+^r=£)\B#¤PAG)²æ\uÛ]*Ž*&!«Zm°9¨g®\áK›»>±XE‰]ý»2_` ß~4³y^"Ö¶+q^Ã/?Ä9‚±Ne„(½L…³2˜•°-B Ã^@Œˆ’G›c‘?0Éâ³³bU ­øØ ‘“Fˆ}No©{?èPLYGÎJ+Í=²î–‰@祗oªŸl2‡Z™ÒS=”8¿!nkÚÜvž&U°séO›Ÿ}þJ’ü1ƒuŠÚÕ¸ñ¹³=E”¨ÈØñžÊÁ[Bfû®}о”kfr®Î^ÿ3×Ï“1òiŒ$ð¢b`K>¦òðä09 0mópZ†YÔg¨ Á‡ˆäÕn»Ê‰“±ŠÓô2•_JÊ,ãs6©¢¾Û-šn<–¿rhy 2÷NnÚ-7õNÎUöìï4‹º¯tËÆ7†²!>>¡hm _»V(š‡wL'º«t•Ó&bùþCÖÚñc Í=ÇçCÕí•b<ÀÒ‘±}/ž™>Â{ŽÍ½W½G×&&®<~%ÕpÇÈ{ô»¥žü··ÿü 3t£nÃ~ïÍ=ÆØ6óÃýER.»œìa[øÆ-`”úŠÇçQœÙQH@24$ŸÏ$ݬH¨æï'h>—Os±3ˆ12yÞlÛ¼Ñ'ØFVµ‚J—3vƒÆ|¶®GcC†aé^;NØA˜U:†©çË·Ý©Á)¦+@•œk”HËÏê6 ¡ÔúÔ\ßžf7„B_5Ìø:ÃÈŸÀtI‡æ;.4`ºÜÚ:†gæñ‚apG] 9ç2½FóµŸ 82ÈÓ´DYÆ,C!¯XBC ˜ò ]‡’ò[¡ŒÌ}½@ÍÅ•UA£F%Wá½ýwUAM£›"ûï:­vË—n¯Âx5P +¸N*(×`½™  Ф© >öCs±VH.Ñ€f§ ðyagØhPÛ5v‘€PÕ Ó:’VTØ®ÎU#]Y…íê±:ÉmëïÝœè¾ðâþéûÐß/ ZwDEKß–Úɇ[wöGÅdXê&¨ŸÕïþàìÙ×dþ>0tí¾ÐèÑnJм½»œüupè`WÌkÞÑ]Oì-×ÿJ¯‡ú¿™ˆw1¡ÉB# ¸BÀÁ Ðè ù2Á\¶#yÎï›[°lÖÅTÃÄM|ù=ìÖ+ ×â»™"©°¬Q0àVá*]8Ñ`„eŒ²¼ÂÖb+ŒctW¹ï»À)ÅÒŸ¥u÷ö ·ÿèÆÑsA9)_zÆë®Ìîèñ›âÝUMÛ{üTOîÿœÎýÛ¹}u™Ý˜<þý‡Ç;›Îä64nëö§÷>>×´µ»¼a/ã{ßtýÏØG` DW¦Â™úÏÃMƒ*‰R¨w¡4šG$ z{¢Ù'ј0&*Ãå^‡Íl‘``€À—y“j xQ.+ÉíE[5TÌ/Ìì3/¬ÏÂ¥ì}zé…»¾u¶.³ëÞOŸk‰û\Cm‘ÜÉ¿üÙ¬6»j‚6E6ÙÞ>Ù°ý®Áãÿþ‘§¯lØ÷ÒÝCäÜ·êçÛ½‘¹‡·€–Ÿþ¶e¡ÅáH%þùú_®ÿ¼ºž¸)ƒ+BPrbš$0c/|cèÁÌ”9AÞTÁÁÄ  ¶:æö¬Q%û ö¬KªäŽO[YOèí.·ëíØ¶ÄÖËK°Õr“*òŽCM*’[{߬-¡6r’¨-ÜI;©W®…ÕÆ6í•JOÄ–dŒÜÄŽïkXvÿ_OzãñIßs|Òÿ‡ñ}íãkï&úÒÛûJÇ7ãØÐø nò'¸~~âw)”}=p :eϤf¤H-ùUc‹Mà|äáBÙΰÉß›/"°¢ ±¬ ãì.`ÅR¨øÃò¦…)XÖê7jD<š7¾¼-ÀÎn¡ÛîÖØ]N„DéŽ&WÍ\”ZízH‚=@Y}džÂT†ÐTv=» Ëëm•b¡†â»#-ë ^øòÜ<·æd”Ü|m½~¬ô$‚®1w•âอ‹Ì|aÓþ`;Å~ac¶TE’•÷±…ÃKfVW,)‰Bñp‹yô÷å7¬¢$T3 g"c¹ØF€ÄÆÆm4©¿@HÒ*[¤»1iØ‹J©÷Ïï‘`Bªˆ]dt¢)(ãQp|Aàfz)•@‘ŠU7&(úSò;N„­OCyb±´ZÀ òbI‘¹oeí€5HÑàkà|BÒ ).½°ªÜÀ*JŒ \Á]€ 9ù†%eJé0xƒ&,.kÉ”nGdXŽf‘a2ªZ{î× Ä`߬Q×ãÞâz„{2IC  ýu -r+ò´H=±|‰–^,,£F´^OÃíºÏ(51€ W…oËÿ"#Åk¥(¡2‚ У ' ar½ULþÐU¨© лsu5†¾+}ÂÄäÉ¡P»‰ËrÎJÈ3y;‘X2¿™Á3ýªÄýÊ®@–’·—Šö2-î À’û"±âÒÄU(r:ü(K<ƒP”§¥=#žX¬âˆ‚ý¡áƒ­zaß³äýÈ ¢é`¬ K'Ñ9¾é¨ ŸoN¢ ¥,S¸J.Ô1áÂOåÑ_dY2_…Úi7PÉ »‚§Ä)%õ~U«ëýÖDž: 4ÃGzÜöôdʪøâe¿ìC|½±agj0nÛÎçŽR¯ ¤Ô¦:¹Êž»ÃZ²Ã¶dGy¶ öq=ìãǹ–ˆd‚"î A¯c¯X:„B®N§+ÓY ¿Ë†ªáb==Áb¾`ÑÒ®ŠŠØPD'h0š%ïh+.çÞ0:'§ƒî_Ìýêð“iÿá]»¦Ö“]2h]r*Í75ï Q¯äÈÜÇ7LeG˜z' Á~⻨ª IDůűù¸_Ú¥“Oòø¥ˆ…"ç8´j9B}be³‚“us¢¤5TßoÐÇM­ÑžÂˆi=]í­ â`¨Âé Þs a\Uz=ñ¿AŒÏßµ®¢×)8j¬p‡“Ü@««i4"æ©"*‹ÂnMT½O¨ÁöëãRIOÏd‘ <š1°ƒàúݹ§Èçéý„—ØÅú(¬Pdä³)­P³` ë£Ð•øg 6rlêsR²P"KßB‰û´hœø Ô¢!«àjî¶‹4µ‚sð@†/ wnnr'ˬÈ­ÑÎé!«Ú+ unκ ?nÒŽôôŽÉq¸Ó·zy êE6üI+–~‹9£frOŸáz]ëШ(>ˆÈc²ê ²tEp8( m}Æì ¹Bá¾ÒRàŸqÛ¡®ëÀƒâ®\xUb%õBíœN/+H¾«Î•¨ yÔý&­^(V’|g;;Q)î«ïý‚KTÊ« _ªÕS¯¢¯ Ú5„I^?{˜‚ã‚ú%P1£Q{ED¡²É"<(ßè2ÆÊŸ'˜Œ§26*jª¤Â/L¯(dRÛK.CîÇÄЬ¼ÑõþžÄÃpj9Å ¬©b­¤ew#0»å7ÂË«†'äÓ¾Èé¦Ý[eàK*UV{E¦ƒÇÈž†ú|9¨ÓGJ뛹‰—¯ZPÝǦ"âüJÈèøEÌî+€­Õ€Xv ul6EBÁÈfË›•"$yWÞŒ¥²µ‘¦…(ÎIe+k–­ÿú؆b…²‘r\¡lˆ„¾Ú²ri^EW8‹,TaJ]Šýé2IØ®ÿ™SÏná"ö\••w$Øx]¢ÄAMŠøØW-A‰ÙĵÆ-xêÐ}E+‰Þb&»Í첸à{L (!‹‰DÅ„ð…ý_y'Îp’)@vL;:8_«ºRuFr6‰= mOÎ÷în,j¿tÐt¹:±tžü‹àÁÜâÁ\½B¹ *oš=¼öLG櫳»ùø‘]p¼õD"Ù ©Åf_8¨ìç"ÔªIœ†”¯ßê œ~?0tòŠ…¥Ë›ÄŒèYQâ‘?:Uìd$ç{BþŽÕÍ'gkÌÑV¿ÌêM”à)kÆ®³ÜýâÖɇv6Tj±Ö[žPv=˜¡6›·ÜÒÕ4ß䨘¼e*µÐçÞ¢¦iuËú½é]Ÿ=ßY·õÞ±¡{ëhzX‡ªw—¯¿ÉÑC™§–¸/åUUP†r5°wó‘NøEþ†Qµ,­›0E3÷+oÉ8 3@6¤ÅüvY}Ól ëB10.Õçë–œ‚ëá&ÅÊ.¶Tj‚` ¾Ë€¬™­á]źp?QŠ{í*܉½åck4À&B™¢§ym‡8â0Ë]EÀÉ œüx]Χ)¥Ólö‹DH6ov5‡Ä|îÒ[R.A¸×rò~ò\£·¾BWäþñ<%Iد·S¿Ç¾Ösâ“ið—`$!¶T$ ¤¬£Â_ •Çü1…Æès©QWÜíb2þÍÅ)öYØLœ4„¯ž£­{0i jƒ#'úz„jÏjjiE¹^Œ¢™ÎYåäÓqMyy@œ›q²£Ùpº»ÌRÙè®ßܰ·ûQîMdØÃi颦r¨¡#í2¾¿oçžaëIÁÑ™ Ì5h„غVí>V]€ë‡êCihQT¥…cÞ-à·¤”^ZúŠ„’ðH­<·n×L­¨Æ­í¨V”B•¯åêÚÛ÷ep†©ÿÔÆÔ‰ª;غñÊÉv6Àh5V Æu¢ŒýNîG5ÑJ\ÉHêÂjŠË³èHŠ›wºù Q¼Åñp8X&Ñà’½%”Æ‚oþ~Áãrx%í–Ý ß7"ò ¸ÔŠ*†ªšT¦!ÕZÓZ”ÛËàäA=uÕäapW׋Ù\AF%3‹*VÆŽ<¥_[úªˆ’ñH,·nÇãÛbƒ=b­0^)£ÅÜ̦fÎôZ¸´¿}¡©fqªÏîpyº÷v}yKóÞpâö‘¾V]e׊yŽh™}®I[¡sB½V'r¸}ƒ~o[Âf e¼å u±‘zûÒwLÆÚ¶¨[å0Ê1ºùÓ ô.ÂN´_ÕcSU× &\f9B¼´`¨Ò.Q(² LåÍT§/\{¸’ã ìøêö<±P}¥iç[£·ŸÌl쨒«…~×úÍÛcëÚQOýzî¹Û6sqÒ+íÚ}yìæÛ,. ŠÙw+có—Qoƒý½ûë$F3"9¤ud{mXŇÎ$lÕÒ®1¢þ{e"#‚¿8 §Óã§•»*ŸÇ-MZÐQàruÏáÿà_Ú[œî3%¤j¹¿q2ÓÓT¥oèÄHø–v`çÍǾt&«tW{ëå°ÿUnŸlÜ5Z#Zús&×ÿFí§7Cé´'#rÀÎP28óyz×Pv£P8O^¿/”äÒ †»¥`l-ù}âªÑé-Gã]ìUEÉîú¥wħûM ®J$÷g¦2ý§'+}ƒ'F²¥Ö ì8×yw>åA‘÷N+îýnØ{Õ±#šsÏRDZ<•IT@Ѭ¢¼(ÿ#'BXÈgjê²%ˆè>H8g³˜ ȵR@”Œñ–jC‰–@:4x~&VV3šŒµÅÜ µÐçžX87k½ð³Ó—¢lcïGrû§—&8Ñà ‰¦æèϽâ¯íÎÖAbÝýM:H‡áœ•CYÙ—q£ü>ŠæLåa6Ô$b&?Àçwáü²àä`+à ¿§{eæ@pñ3K÷~ñÖŽðø±‡>±ñÈWnéx<6{Ûø®Ï_èŽo¼ml÷çoéÎ}Fã 4N“Lãê´“œüý‡s¿ù—=£¿zj×ׯܲ!:úðÎ,<½?=þä¯.á¿Oü:£‡*7;Õ† úëÆúNÏõ7©?ã܇&÷#iÓPú§É©5åXÏçð…Púl¾l[Ñï …°2åUÜkAOYuoÄ·KÃóؾëûS]÷þøŽ}‚TßU;’0ècý©mwOxl-;º›Ö§Œ“wÿ>6Ù‘KË«ÆØæhýžΞýÁCçÀ`Y¬ÍkO–k[Ž<½¡egO¹9X[–jeõÚÉùÑD4g«[h^´ Ò§©€H¡­CNáh†ØŠÊÏãs«œ> v[ý½a”|ÁfáÅŒšÇ±®Œ…o¹aà|óÎeL­ËV4Ø­ü2™Ø,&ÔôÊ(ùµbéuêðd[@*Öñ9$'‹lOÔõS¹‡ÉèÓ/lj,<åÀ)xZq¥ì¨Ý°uØðYèA‡!w¹]ÈVì•e†!²{—ÀcQÙå)ƒ‰Ï“òæ.¥‡«ä4í¬ŠÇÇ=4—Úý7·Ò¤óYä:O¥Qç³*–¾°XZ+l!«,••;{NÅì>dRÚë]©l rÚT^u/Ș".ÒêÕš*ÝðoùßK9½J3$–¢=eîÓ¿½BX‘Õ_Š$Ü&Hg³d'?ÁŒÝE+X «ÂísÛ‘Õ_…=2X§ÁÙÚ^ÀŠôµ@¥ÈÊ ŸÒ(˽,µëÛ¥V…\'ÌýþAYøK‘ÁTQsåñܧÉWEòܧäJp“Ì YUàRUn§Z^}ûWøì8 7üaÿhBâVI,uƒA&kF¡@šìŒ]…½¸,]úÆ¡'/\`bfPÍ•«t¶Ó&ˆgÙ\_”n¢|ª‚Còœë ­ôÖ6 '‡Ïã/G0(†M£`;ˆ ›Cù¾iÓlÃeù¾7j7‘Q”û"!_¢RÅÔõ7¹ûéK‚D¨™P™@áGH¼J¨yã@|kéÏ|ögü ]òK>¿ Ï ø¢†åÎ a|¸l˰b`s<•Å»ð¬Þ ‹­#(-”ôÃ÷³ZЊÛ3Uïu'Þøs›c"£´•E«¼î² -ˆ2ßíja^…âÙÉÕeÒìË׌Æ"5wÒ¡þüF~í/ š›Ínëôe~tap‡·éJ’üä;}=GS5Îéú¶½Yð*;u9T¢’¥Ús¸Ú¦ËÓç~öÔºÚiÎ5_»ÂTEÍùÄž[oÐ<šü&÷=÷÷çpýZ‰ð:3ùJ’]€R@$.Ÿ“_@xE¼ò þ‘³üÇü2¦É§-˜Ââ‚FÎW“‘ˆB yFò(ñÆ¡f×|!çÛïëQhéoü(‚}[#â]„‚•è÷ó †²AË›”<,Ó´ê9|‚Ë玽ïÇÆ%ájoD_OÛHûHKsº>„¼n‹§ãø®VÐ*»A‘¾÷Ö{Ùø€:/ÕþìbçÁÁ@ÛMWˆ@Ó³Iò—R/ËÝ3}q²¢íüWŽÖL÷¤ 8DÞë®í:6±ÔM7^á‘2>¸ié7›sS#j¼[ZúNއwbxÍ^àñ)Úkf4:Ntx_vî¹³ÝÊ2¤9K¡æ;bçÙ Ë×>u¬¯ü8Kúø®K7#ó ±îz;Ïi: ©º›§jÔF€®@à„¨*oœ&daI-U¸êª‹½Ê[ãêÚ8é‰üðpû‹ šÂ) Bl@GÎ']V,áSˆ{Bù™ŽÄ–· Š )¼K[ŽzZÝ–`›²1«ZfÞ#l%b¿å¥LDƒæÞžTõð`ÏxïxwW¦¡º5ÕŠØŸßçRËtïnCZ“.×⊠hd➠¥/£­g8i jƒ£'úûDj·Ì”iEKÿí¡²škÙÞík>öìâÐ.b›ÿøN¶÷xMsCºýÀ@Åø+ɥȖœ€¼O[îèBó\,eÌPMîºÅ®3Sbòò̹Ÿ>YÊVßhŒ¦¾°ûâM<CþKNª©ªg¬T$ÊÑãüœ–1¢“hÉ4IUž£àFG1ûÈèZŒB!¬yëŒ!•{ŒsÞO¤‰np†ÍÓ³ £Åq ⨮’D), '/&øÈ6 .æØI!„z6[æåIHXÌ2^`®À@ÄÆ{Ü¡¼å'ð6óØ|òÜ»>5Ì<Å$wÞð¡"ÈãcïûÙHé^õ0>óÁÂû~féãÐ.54fÚZ3ÝÝõµ5ÕÑJ¥ÝíVxÝN„äÍF.²tSðR®1×HíKb•Qy€è‰–Ôp\ÍkÛi‡÷8ëÈ Š„ñÉ“CA ¥‹|®'L§%HLž 2A[¯k`Vü´¹:Ptå¾ßJ‹LþNEÔN‘½je–¥¸±’È‹wþKÇÆxÁoó‹÷)Œv¹¸ô’3f—#ÙtËõÎkpïµÃÄx&)PCl*$)–ˆš¤W=ƒ(>y³oðõÿ‡Nÿß:%²÷ßV M‘@Sx >:ÒÑ>=52?:?Ðß>Ü1lWxáê€ÓÉñ]°À<%``LȺg9Í&08¤LRìOëv`p 66Žó2©}5Áöíín»ÍÚÐÔ^l ¨ýýºV™P%âZ!ó1JRþÚ›Ò[×ËÚgP%º =´F>VŽ ÌL ]ûŠ3kugÓÆs‘ÇT.—GÃW *šÚ2Ê-é-úæã_ìßéÜÞh2ã¤~±;@’Ž.|˜Ô-†¬ÑÆÔÍGÉ¡¢ß™­[2”»'ö}½éŸÒ-ß×F‡Óh=ñ2ÖËçrAÝõ”èˇ ÙLm€@õ‰MxFáJæg!Ê:Bt‹é+~ä1bºKË vÙÜWŒ˜;ÄÔ"‹È5š=Lœu¡vI¡)Þ½%C…Å<æ÷ñ„eé̉ÕiÌïýœÍœÑÀC·•hmiÎ6¹ìîr·;áÖH0¶¯§Ú—ô°Ø¾EXEµœ÷*´;UNêäÂh€Ã?'ÖLl1ÇõrR¢6É=™JO¶*4PX¼k¢u¾N-ìV íïö7:qú³­ìVCŸ´y¸aobòÊæÜfªIÌu·l`±zÅ"ÐÅGÓ“ åpËÎCº“ayJ^&oÞ}¬µnɇëZâ2”5>åÞ4ƒ¡Aá·L<-—ÉV.ªå 3nJø1M¤k’—½Ìdˆóä(˲(2Co$h€ï\¤µ±¡Ô—6Æ®º®þ®:WtúÖ‘©‹Á /l¡Æ°¿¥Êìªíîëªu»6bkFˆ‚ËPµR6ÑVBÄÓ†ÃùY‹i~‡yá38àâŸopçDF€0þÜvTµUµ2$Z¶&ÎÅ/„žÚ²°H¨#éŠÎpv]\Γ÷H‡æÚom˜xö‰+ÕÍÌ0ÈœáÁa©HÛ¬’Wí¨;¼tÏ®iòûoÿ|³©ír'3ÖÇ8Íp¬õ@“‘¢±ÆàXËKrMBžó|ÁâãÆܪ¬4Kñ|+Èlh—~—Ö«£þë ó·â!ÄûydŽ7jÎæÜà) “T'4n»/ ªSún˲0VQü¥È·æRuÀ¥º‚ÒUPh›<Á¡ib lë·'«vÝ`ý®­7Œ–¬Í÷#P“ C8Õî-Ü[©W ¡EÕ;UL5Zš¶$J,ç£kÂY3P( °³6ÔÅ“Z='œ»i#¯Lsüâ™ÜË ÔM h‘^ V¼ú}©žzeéƒ`Z°Ù’8V…¼þ6”oŽÒ5™¤îTønHS|@N|‚¦ø4*ù›@^vçÂÞ0ûV°,cí–ØQOΛ¹8²Ü8(_ºrWŽ Úãä8ùüµ#@AJMRª;÷S”ø÷WjÜÃ_Ú’:È.f®ÿ™~‹ú9<¾†‰â‘õ”"Ç47`HJÆ~/ ò¹<.Ÿ·(DŒËÜ).ãŒb¿tY™˜Dié)É:VÓ7n|3â†O˜ÈxFG15923:ÓßÛÞÖ”©M%b•ŸÇfÕi”r‘Ù¢äE´À(Î)õ, ,öÒ,AÉFò(¸Ôy€ÉQµ’à‘–Ãa¾D-&GÁ©ÝMS >™TF»s›«v<å³Oo?ª›mñdïóy®t`ºa¢©ƒhO^ݶû´¶þ“àȉçwT·$æz‚œµ~®;*ÉøŠH$>çd(næ 5¢‡1;Ì|~ Bܳ»~4a°ÔŽÕ4ŸœkàTî;ê(w(¥‰žžéá¡õ¤v× DZ›œmÛÚs¶òîíÙô¶Þ &o·þ÷eŒ„¬æ=™N!àìPÁWHR<@ò)Hq¬OªX}žieeQ,«ð¸œ(¿šõZ„,¤ÚÅÊ€ ØÙÂÒZ&ç"éLº« <×y¨Js'ø' cüD¿{në?$æï™ÊÝg½ƒúݳgtÝ31ég„Ÿ‘}%5”0ŽO‚fò’-Õ¿à¨x iÏ`ø¯½oLO5¤s³ûzš+›}u­Ø/3wý-jŠzƒð#Kܵ$E"U¡)ŸÍÄ(Ô˜^X–õ”WP–—Ť×b›¥ø1–WÙ l³åéÈ™L/Jy©ú'‡ÏMEºÎ~róô-\¹Jø Ué!ÔóôX…ä akGoz÷Özõ¡‘–T_h°jv>2Û²«»Üí3¼g/\›¿ÃµŠƒ%ÙDœ¼W²»Ïîbc<œvœ†Ãc ñÐÌR¸ëës‹ÝQûaw˦ÌK_8?#}G_¾g€z%½ûÑYäí]J¦¶? ß9e|§œ°#)\¸T…±óog}D²Ù®P¸>œè¥Š’€`Gœn¶<'îɮà 07ûí5w¾t ÷hâ¥u5Ï5|ÙóGáŸ=/ö]%MàÓéݬ?úòåð…Åfاܷ§s_¯ÝBø2p.*a¿D8N€Í¤FžfFÌÊWUf‹û*äHÊ*ð=(«8rãNÐ'Í=ïV,}ú0Ù™çlȈªë¡Z¨Ÿ cÓhN%ä? P:¢š a¨ùÄO¦ŒüT>4û¹™ø'^s]×é±{0Ææ0މå(Ž…Êëy»·Ç;#VŠÅ¶hw*5VcÙ1¿S²ãæsæxW¤a¼Þ/Éèrçøì|¤ãôtòМ'a—Å7?8·írä#2¡Î¬3T¤lµÝ{æ5~¯)à5x1¾æÆú ·ØäÁhÒ”\è g¢¸D<·<‡ïþ$ŽjAÌ—]ýõë ÿ!áÌØÐ8)rŒS w‡J$ÀQÒ¦š7Óõ·H-æýXظÊøc&ô—‰²¥¦eÅÌãu(®†òÅ”‹á3*»Ó4Rq¼  hßQEÁ7„U½[›jëh—R)fm]›´÷)0>xv]•J_©—*DóûȘl„4ó ì›Éå”D‹BKQö:0¹’l g·×mCS,ŽS)–U‚NÎ*z…Ëp)÷¬Ü,wëÞ¼Óbäÿ³Àb©Š?yøI²J"‡'¸<·I©çËü¹¸Bö*ä¹nìðD ¤ +𳂰g¬"!’ª0(ær§±N¡E¨n ˆxyIu_ô2J“Ë9°#¹¼]J©b O’þ{vêD—r¿»\{µøÄ;¶Aþ÷GÈÿT„·”wèVDÔxGIÀÊpê=>¿sò®M‰^æoüö†ñTçáѰ½~"Õqd$L†½òàhã¾G×û.þ»1»£§¼v磳_¡vÇ#xü5°O•°OAGb$Ç顈/Ïï0ŸÇjFG7ŸKA²äd•ÎJÝ•ÙMv•ˆŽòHYÍö¡Jk²7RÙ²I¤º¢úõC7¯¯j9ññ-‹—ü<ñbU%Hí|lSv{·rd!¿»¦%½çëÿ¡±ÞŽV.óåC„ž³•ú<ƺ˜bÝ4ù·b:²¤æ®Ìã®äï!qR'J@¸±P7¦úØVh›˜¸êbFµ, (Dº™Ðu\ñÐÀ½ß9¹åÃûÙ×@í ì{ùµøÐ˯‚çåÖÎÔÖûÖþê…ÎÔ¶û¿”žÍ:_xòÔ­/Àu8H\ælý÷±«>½Ž"ÒL?QÜÃãÑþ”Huí#˜= ˆ¶‰°—•“ò€;–,È=5†IäÊ@i%x.Ïë¡æeæ «ìÊGYŒ6К?‰ \þÎ)Ôçé‰VþÉeúX}gèø¥Ê©3›Zi —'á¶Åî9þµ[Ú“‹÷OŸÿJO«ÎÝ BÎù#y"ª‰s³Í\2áóšE\’RȪ›œ€ÈÈH3ë¡Í“9³ÉW,ˆ !¢ŒîBæD#˦8(î’ìË7$ÑšdÄnW<ê ¹Ca­(&pypd à7J$½!* ÍàÐR”ƒ ³ïÛÇüãMÍ•¡ƒ’Ã{±ÿÇç÷ötKM¢W$p P}òžÔÒ§¦â›î$ËãswN>º“ln§Úªëj¶=8=ñ@Cµ¶Úœ¹ÔØ\³ýÁ™ÙÇÓ5B¹R+¹ök‹|bèöƹf'èÏÀÿC¹±$Û­»þ&w žiQ¢$¦3Svdè•€/$K!Â…| œ#øœ¼4-ÈÞÆ1ÒtG{<ˆÞžöÁŽÁt}¬-ÞºeVŽ‚¨ô‚tAŽ˦`E<[Qz~¸5/=¯g÷ÖŽgè4„sr~±jÇÓþô¿Þ2rqC¬óæÏî<öŒ¯þ«äÇ÷yÍÕ#©#ý^sjäÚÇ6õĤ9_î•bq9aá ÕP\f"W¡´¼k÷Öõ»>¸áì:°¸ù µµéà3ó=¤éð‡ç³Ûº|KÊòžíÙ #$Ã= ùVçó ÞŒ+ h„ÖTDPeÏ®ÞîÚ”ÛâÈYãå¹:L\_å¦!QŒL%¯'Q ¬€Ba¡Å´*ëª:ÓK|þ®©žƒåþ–©*{m…‘/ÓÉ*EB©Ú¨±¹3pfIÍUš7IÄß´T5¹=5S8íöÕ9©Íµ-1­:ÒÐí::vggR­ Ý *XÝ4Ë=“Ú\c´ÇüN¹ÂWÕP®)—«-ºÚ¥r¥‚¥º¢º-ì«q+u•ˆ ,€ßRoCšÒ <…,HÛ ié ,êR»hx„•Töcl– "Ej¶¥u×@Lø¼tôæO,.~pÞ'¿»8Î\Û÷¹smmç>·¾áÛÿü¶ø6Æ–=º–½‡…]ËÃÃ$©?JGο°¸ø¡ŸôyøÊmüvÿçδµùÜþñ‹3UU3ÇáÚž†å \[)¡#ü/AÑ /ÜåÅ]6éû·Iì ®bYÎ;ÊtgtÐçú0ˆß4ìKlydáÌ]»OMo¼Ìä§^ñ¶ÏÕõž™‰ß´gSû#ßGà»ïasÚ«2a1²) ÛJ¥šÊãæÈ²\ŠÄÌ‹ ¬fÖÑï²óPd4´âÂ÷&òµHìFuDWȃ‘a“Eš{ÛX~TœÓs¡¹·Ç¡Ü̆ Ç'÷çeŠÜçí`¯Ci>3Þ¼w0x-L>C3¸fûg \(«AŠm€¦8S$` Aµ‰µo!5ºŒN‹  Ú|¸8È…^ØÀK1ÙEQSÞ¡%¿yêpîÿLœôº²ê¤:Á«÷øTWF“Ť~¢Ö<=Kvó×ÂÎÆ‰Dj~ V.0(À^{îs i.†l‰Žòl;¢Í3°¿`yˆfP÷¨&Š`ç^áAåÑ ›0“ÔEî÷›Ÿ\Èý§œzåÖkŸ¡:nebÂï¾þ‡ƒÇ%aŠ•‹ñU€P«¤b!ÑH;dàUJá¦V¯o­à·?Ù.ê8ðõë/M¹…ð O<·a Ù½ô£—·ÕÕm»<Š>§fšÝøÝY¢“ÓO‚ ¹ ]*·Nød" “Æ@H*;[@·'í ö²ýꊘv.ÆÒµ[./=«&Göß-Ágoòpe©Û†×Ý6yöàáÆƒQt^tô Ì{‰Òƒh A’É‹@ -S¨˜Ù'\RoªØ›rŸA_ö¥ÊSö2½×àE½BsíÞy«´šbíA*ÿ¡PÏ‹hç–x¦vìûSjrêÀùxx©ó_\m÷ëüIµ£BSCŸ5NƒLíäG52UU}ª\°©àÿ~»*ww­O¿ÉN5 ÛÆy‚ë“ô=œóD˜¨$2™z. ‰ˆ”DæéJ(Œ5" Žj¿ª¿6µrþÃ!„ƒn6tr®<€Í~ù‘æ¹?;àó‰Á¶CÅò#ÔJµ½ºÕ™žL†šêÔŽGç—Î(ÉSç/šCÉ¥›àÇÖ…¬­&º¤"ë9œóP¯ ¥ÑÃ]©u­•RuM{¿ä–™ª+û=-Qëk¢'’œ÷/ý²—Ÿ‡‹ö·|Î!‰;Ml-0Få©°A%C•OqCA-Xíqž?«ÕsîäŠh‘A|¼2õ¹œñ^ê|évs£ËÙh&÷ËtטNï nçtÁýæÔò)D©,¶$›Ì¡S09‡«ÞÄ@‡ß˦õ9︠1R;)šä+ ‚kSWªä<š'á×®>ë½ü —¢Etî*ê—béM•ÑT©%¥zØË<1}PMnA$?­sÈ-â¥N¤ßÃù˜†}•!+.œƒk`s‹þ #d…óÖEæ•QÓò`¶ÒaT 7‰u®TÈqkMv³!oreÆcëj\ò¡ÊñÆš´­@\Ÿ¨s4 cú¦¯ÿ7õ8õ:<§=§GGSȲÁM¡Râ…Ë ‹S² r¥Ù Ô6P ḛ̀­{h¼¢k}löö‰®==^ǦZgWW»­ŒOr.ûÈÕ¡…oÛí@òËö !÷Øwvw^+oÛPÝ|þäÁ¨3`¤%:¥°«©©K¨ÔIÒ̹…°|ä…xô"j@I<:`ËÁæö,>u˜¬¢¯=ÇØÀò¾âŒý7fÞÄ3ì+’pA”ºx»ÖÀ¹WÌĹïñËÔÇÀÞ«‘“-Ž´qéväÚ ò6¼·ðùlDÖÆYâ@Q° JLÿN<½˜«ÙüÔax¨²=DÏxî&Ϋ­Ì„์ʺzTŸ(¦SÂ…§³­Ìj1›´j‰X$ð 7pc\I-Ì(¬JÞLŒªÝî„¢yRêŒFÊÅW7lëë|ª«ï@_ï&íÆQ×MŸÚk~Ò¼ûÉÍ~aõŠMŸûqýt“›"ç–>H¹§ëGoË=1viS ç÷¿çÔnº} L3ýU^ÿõ3,ã!ß0œÝiž@%$Ô9< ƒz§²sID1i&B)“ÔÏ=½9÷W[cKGÏ@Å-ŽèªâÉê+ʧÿ1÷§/Îê­‹ÊÚÝ_Ú¯aÙí"µs£ØoÈøX¿Å¡¶"‘ØEMÀéARî 7AkÞ}–¹¯¨¨—u_‰*Ê’{'Më `äæŸ|#ÄáS%ŸÚ§ßy‡HE¾˜ëüyÀ`ô+Ðû öq,ðýÈwµõª'½3Q‚)$ˆtðÂS2iØ…2‹A›hüÔ‚ø‚8N0©lgŽØÏŽýKýXµq´W/9vÐ.ž§• W[kGuªssî䋯pÆ;pÄ(5å–Œ?•ˆfºÒÁJf_~öï{°.Ÿ÷ÒAnT.ÍMË*µCž©ä’Üz™S¥rÊÐ7È!~Rnçœ =þîÉûé]D9'^FÕƒxDYIeŠˆäS<¹$ª/¼¿‘\>9$.x„ýÛ,¬X«]æ[as>oëʶ«eÂkßÀ±©…B³b ”ð"òÇqBÞ7½"ò‰rP.Â:rã´ÃQodIÔ— ÈTéP!Gl¦C ÅU¹<å†d“¹yï( x6§ÑeÑËDrZ±n¿Þ+…g°ë{¶¨Qiò),ôf¡Ü!7(5)[ºÊä’ª°T.•|½æh[@B]&ã0)Õ•© :f®_§¶“ϹÖB>¼)#Sš4¡ê·&1Ä.ƒ4ƒü%('Pš(š3ÄCŠ6Ž4A§FZ~e"£Õëܨ:§Þ©QÉ$<šÐ-9d 4=…üvÈ$)/’"YÌ -é2ÚÖiìq¦G¢þ˜Z(ÐuÖ×V¥3I½ºd‡‡Ú§ ;üÛ64ï ÛŒ&]NßbÙ}½ÓÛású :•B*ð;°#Ó»U쥮ÿ‚ÓŸ™J-x¬–Hù@ŸŒ}V½HʳwF «§y&l~ƒë¦¶oš2Ù´¤¹I¢ƒa§55”hØÚ[1V #`#âM8îpÜ â“e<]I äVÀåpš àøùä´å%±q!I]¡r•Œ›O˜ôZðj=Uh¹ò~TbEÝí•MŠw#èÕÇé´»œ¦Ô6ÊÅ>Ÿñ.¸wâÄ@EeDÓ‰""éêÙnĦؾÁÁHœÁhDÆßæé¯Ž½d†b1ä)/ëÓ4ÖÁÉgÝs£.Ÿ3Ûõ—B¦Öœ¯ß­ƒË@iµ¤‡Ò«Ó³ Æ@½iõÅ—e#=9·9”w·(z,)¹^Å½Þ §ÒË1çÄÛä7ÉçÙf¸–²¨ºDV0,Ü ÕEHû™+èȤ Ô‰$ 9¸ší6h­àtC[Öž•s4EXCŽÇgž ”´õ¡2³ÔÅÈÁ·ú‚öýs´—mNUb†rÄ#m ÃÙ¦ 'S…­Eoί×;—ŠÚšø¹ùÀ­ÎD£»eÜdXPÎ,ꊀ³ÑÈð*áëYk¼)à™e+ð^yš|›|€ÙB¸‰ÁuPãeÄ:Y«[j>Qñ|_$N­­æÃƒªRpîBÛ¶ŒšKõvŸ¶©6;P9’‘Î`ª#¬±vkQ­×7T‚Nòíw+×nìJÙ%çôžèµèöîÙ‘û¶£¾×Ô]l#‘$Z‰ïˆ<Ê$ ¨¬—„ªµK¢€/º—L3GM€¹eЙP3ª¬¯êr-Y%p¢Œ6€2 Ä2ÓCL—¼üŸ^ ¿ú¥–¦ö`ŒÓP•‡®ëÛ.:½0y(U ×ò+Èk­Fæ Çnk°Ô^â¡nmÉÎ.8¶î°Äü¦eË ÉLÒp8ìrc…Ùomèc~Íd…̬1xƒÉ–ù¥¦•ýQOÃ@rí^“ßaäÚ‡)FF }Ùù¹7×—µÅ}ÆÅHî?&ߦ6Á½$ÒÄ¡3¤bVWÒMØuµ¢Ú\RÓ-¶¬g$Ý$îuB[.5¦â߆ˆÀM(êÞ„ÐS™Œ‡Cz½7  ‹Éb5N‡bà»­–ÓLŽ+š: ºÛ¼ÊÕs’ý9gfÁÎNãNcÃâö€·a0iç4Šxþ & ^K€¡G:æÂhlž¯inªwÇPœ8ü­ ò…%³ YvL|‡Ü¶¢µ×ŸÄ|±ä›äÌ6(‰ý¹Ï&GŒjdØ„ºªÀS~1>]°-ªàÜlxDsØ ìjÃÓ¤‰~D¯¤ fcý Bj„fày󌆂a±úZ+º†#iÌžOa»¯AŠXE¥ˆ×Sì¦äÌ8cÐ,z-'³» , ä&c¡¢‰ËK½fÑ«EÖê0°”Ùa ûìrêM]ñŒB –?ç¶$‘6Œ•î÷™žk„Ç_³Ù²­ÌõÁ5±\¸ŽÂôO>â_\X3^ òÆh1%ÊÓ)1 RöT;+Á•ti-Åú‹¾}Q>Øl±íÿ´.ck˜• E»d…‘VŸ§i8ß¾m~Únù&H»í¶¶•»; ÿ±pµ ø1ÿ1â|¤æÄÙ牿ñJ{H…;qˆ{HÚ êàC 18YG'ªâ²Õ ¡T{!UëþÝ2`÷úwñq¦ú…ÿú#Æ01‰2•ˆ„Ñî5rÞÖá‡Ïÿ‘-RÿM4]Ä-­(ä2hõæb$Ñ›·Áš š˜’&År,…™X%XÑа]/GYR?CJ” é‹Ɖäè ©Ö´zÁXÅD±+Ç˨½¸É š¦ÎYáqèlª™°–„/Ä(ZšŠPùa=”µÌ—Âo´ó²ƒÚ«6'6èÍdÇíc}ëZM ÚPJËëo‰<-,]¼8Ó *í»KšüÒÅK2:À¨ºÁUC½‡ïÖ ½a}ï(ª}ܦ†/}J/|ô©ãé¤FpRK{[wªé„úšÀ΃pÝzÎÿ‘z ÊnŒX]Y1€|Þ–È Ø¢YºŽš·ž@ÝÈPê£ ²õ„Œ2Ô¬†…HÉóL¹XÌtÏâ…Ád(˜&y?êy[O÷A[ñBg©eF¢Cž– Áº)ùvª\óâ’ç´;VŽ[âVG²©­Ýi¶¦³,©5®`í©î,•¸aá›ã9£[i2;ùŒ7QI˜©¦Žv§Ã’Î1¤ÆG¦{ Tzñá¡‘#*ÉAM>OÂo·»\9¥6¥Fz Jƒe֕˺J»Úd1©=… ïKà“¬1¥2¥öåF[ÇÞ±bkæþ@xúõÑLÌ#V§*ºJ ‚‰dH¸v꺕B1B¢Nt•}7óÃpª4/vMýpÄEêÙrmG°’u¹[Ç;vÎ~Xmò,YÒ0Ú M“.áA·£]o¢t¶¢{þ5ãy™Ì”sxõÖnM÷\½¦5Òµ(3v¹G—7ùƒ{fÏ¿f"O«Â)—A©¥ýžbÄâ.ôL¾hÍÏ™ð·ŽBí);Â5?¦öP,´?•Ä:ñX²T<$®Ä«¡ªyaæjÇô7ÿ_R<Ê< kr…rüƵö_Ù¯k¿Öú+ëµÔžwßùÂòü_ázzç"÷ *Ê ù{%v~L?†pÖ°D£,eQàµ`DË”ˆÃÄ÷Qr–† º*(=9·˜PàåHc@M_¬Ì jy?ʸ3êš^áH·~á§vuv,ß¶¼cöͯ\uçê\ï‘g6ù[RŽömw/mÞ‘~vÒ,¹Ø•íà}z sEû¼«—¨·ûVŽ_´hžÃUhîÛ5{Áñ]Ý©9kÊ=Wlvøp™X½&ðoåÜ`¼·9c¶U†V6ÁuáŒö¢¶›Déü_é½Ô;„!Ö|qœEmé¥ÕÓOâ<43=ø|}^{ÈBeÍ!üTäP«v–âD/ j*pKj?ÇzÈïä>“ßÏ=’û„æV™rÖÞû–Mܺ4¤Ý¢ÿ]žèMÈdüßT\@vcÂNôÿÁ®jAH×A¡ ‰tuIe!ð¾‹çÿHïÃ}ŒÐ}³ð<—3$%#¨¥ 7ÆRV)³  MBÀË&’ ð‚¸ÞÔÛ’è¾1[#Ú_è)µHÍ2à$¦æ€c¡z<4 ðPü äE…Î(ߋܚ4u£æVçÚkîž³ôÆE¼RuæÚp''†;õB@%3æRo »ô~ƒ«w ß§³hXwoo»%:À¼úùùŠ›­;Žž{,0ïêë·¢>@®&?fö-Äb”68Öå$ ˆ‹)€ùÄ‚õ/3ÒËèYý+cRï)/²²QC„\$ou­Õ”ºÖ¾/EÐÉmZp¶ qÁUUâ Üm£@p2ZÆ¡ +´öà)6ý²*­î´«’C½ ã!?oAYªZlÌ_HvSêÕD)Ä!7£gß“«ˆV–u(妰ɧT˜lñ4Í Ë8%£œj¥ÇZjQÉ©EfpSO¸Ñç–[TJ«u“ |Î^šsx¼DOÌ[¢ÉnF¶µîæ’Y­]·¡ßNn¯o# Îÿ^¸š*õÖQ¦ Z`]¸o³(ÜÁ útQq Á#Ë‹&i4ù0<Íq*öd±6„fNí:âŸ\6VQw´›Ã|4äEM LÅW_ Á]D€å†é” Õ®@iÒ¦þ{îö~ëîGÖ7‡4 ¾Í›Rã~£ýáî%YµÌž1B Rš×VYÑî™XuCj• IˆèÈ{©[¬aÍGL¨}´0xõв^wùܱ)9~TìN¿Óß8ï,k­ `@=>¦¶’o1;°žYUÑ ¸&Š®˜Y;Ëí^‘¢›ŸEþGÑï…“ØÁJGƧ½!8ÒIþªN²Fƒ~™ašƒJB¼ƒDÉ û¸}¶•¢m‰hGÒêmLd»l:¹õŒ-aqécBK’Úþbu:|æátï¶¹±X m­ºÜ«6tà>œÄ(Ž¿àê8T1à)¥ø$ (h‚„Õ!ú-ÂS8NBR?MºK+v°ªÃñ© ¦†Ut¥bKS±£Ô‘Ë4Äø€lº×GjguÄŒS`¤®59‡LnKû,…\çç#–RÖ’ìJpj–‘1´É”âÔ\®‰ï^Ù^tÞ&ÓjurY,®¡Ïq2¥F¹i“;® å=ª–˜¯1nº€I+#9OÉf_Óß¾u( —™xWÀ©Æùð?‡ká6‰}o-•F(+ÇQŸd¤Õ(iTªe@&±xŸ»DO:ÕPLµ¦[# :tÃ8!’†0ˬÈqSóž×2Ьpg‹Qª=>1rUHíÒiÕJcrˆ322e,œ õ¯ïLÌ*¸ùŽÅ…á,í÷¯QÛ’!§LžIz²~CžÙ§´pfVÁ0.{L汕#Jg‹_6oäºåyWiN¦¸¬/ÖšëwvûM©"_°%šmýœÚF®ƒë¿Œâ´^–¡îVÂmC1”Zl‘c¨›*ǰ· iy¤~GbN}Çi«WÈÆbÇkWÎ_IÔ†ÂS‹&© /™â´|ÐäÃéºxd}Ì‘’7ÌRÖ‘FE€Õ» m[by_$Ø<;dJ&㺨\£4˜½öx&’(*Š©,Ï[ý6\F,“sL¤Í×gÔÃÑö„EfðÚRJN“ÚÝóƒn‹K¡V°J1V{”|üÎÅFŒŸÑHµ¸CÇ’€$\1KQ¡"£’ØáìC×mžö~-Q ”…؃٠š³r«b1BØ€ oÛgmQ¶¸ik¬=îvªH¥Þïb÷ÿ¥¶ƒWàzÑQø:± âF¡ÃRWcN"¨öÀ ¡€˜¦L±ÕÈP8ö'dùC\~]kQÆlª„ÕF57ZÁëò (ÏË᳦ˆ¤ø=ÊiAqÝÌ´ò±ç§Ë›ö´í—ÿ½ñMøËó â’^/ÙPxµ®@Á"¸Ü!ÌEY«­1Ãwé-¼Yqã(XõM¸–æâZð(”IM3¢Œ…)i¯äØëh•á¿%úŽ÷Þëýá‘ tü*©€kÅC¤¡]~UEç,ÈOëûc›„HÂ3Zõ =XÐ ­ŠÄI6:Tíùàh%›!ˆ†b¦9ÛœJ„xÔÖæŸwó±–åéËeª¿ü2ÏXµrƒ,ª7¡ÁT³/é”S”Éwx=Tù> \MÎ0^Maá}£[©´i­v3³LX‘ìsëF»’ÑÀs1hòEŒÆÇ ³‚W>ƒV[ÔWÛ¹_[}jÛ G5?ëî4k„ëÎ õ~ýúºØê î¯Ô¯.Ö8ù×ÚêÄ/€çØ~¨¡"• ¨ôÐr¢1†ðÖu¸"¯? £l&78h_;¶ÞB m‰4†LA³ŽÑÉXƒÊjb¨½ä»“7îó¶¤\2eN+cƒ~Qßþµ‡¤áºfˆ|uÿHÝÀ«IýhqÏÈó{aZ¦ÿÄçTÂ/ÛvSï®Aqêòuê,>?QÑ*’#PoJg”4d’ 8GÉ6âz†^Œšò ZAœ7ªT`jñ/¼Tu8FΕEƒB6?¦™¡j‹ ††+ •½—‰r1o„¨:„BÛ*®YòÖ'Å]È®.5wG¾G)Y´àlW[<‘—»&çõ)š (g$dA9#œFÓ© I¿²ß—g"û#¬ÆS8{$èeãÙøšqáׯ8 rN h€|E6#|´’V“pʬHþ'õ7øüÄ¢ *rê0qˆ¡nÔŽ)È?-®O.¬FIq}?áƒèT™×/_<ªÿkÕÓûáãh^*¦o3]“›Þsf†ôQm.ù:ùuøìãÄ.”÷C~„r¥¹ð( 1 „*í³ë9xŒ0,#–·`û€ÂçËôQè ¤üÆáp†a‡ªÜÖ,µ=`ÞD¿§ž°‡¬{†3ŸrÈšp P¢*ôÐï*Xc0nødmb6§Öt¨¨;Á÷­!˜Nýy^U÷4ÅÚ ®yœw‚xÍ›%R¦4Òœ!g(8ÑZŽS7!ö@CË¿ÀUU~N‚nãE.š>JJJ°$Á¢d)Þ (YŠaÄd)$) ñ’H hi"y¡%1M^œ :ÝÔ¬mj*i* düPG)*-'ƒÆ†-fK·óJð(qª0#qêï&׫ДrwaV\ ÜS¹Si¥Ã5o!:^ÔŠ¥6¢4(.J ãz¼ÎÍ-Á{Qo*Â1V‹pÂµÚ p¥F-{Âïúöž{~¬om{+³U ?ѸԀÔ2%hìJaÐñ´a¾ í>#ÄM<:££bCcxœ1˜„7™FLâDÇ¡•BácC¶:¸~ÀXÅáv™M¨hÛÅ»y›Õä4;•rÂŒ¸h{Š m(‰…‡g´k¹²ä@f°èŠö­jF?#ý«˜¦Nå/sM]ª_f™íÞ¦ù¹žísãðg¾þ†çöhÚ{ÁÄ@'×Ö'r ~‹|‡c‹„ðâ 1ZVàcy9Î\Z!..O±Á°#(š#Ú$(vB£™Íÿz0Y°6˜Ür鱕@Ý0/_&'¦ ’Häá Þá ì6½Þ¦W¢BOÑQ‚‚ü5n %hS„ë ר–tÒÃ;*(¾ÝÞiûNO{lÞ2poüÁ_¾æ…®-ƒ1pℯþºyN ¼.xº"×;Há4¹^<3Ï@¼4×H/q°¢é@f$ÇÃåLÕååÂõÀNma©XÓ-ri’$NÓÅ.hu0!#Æëöý´¡(TÊG õ–âýò©€Q]6H½·H¬d §l¹4] E•¹Þâ6ñ!w²˜t»æåy/Åh­j»\¦'ét,œurP›Yg‹„ñ8G~ ›cah­B¥ð;ŒƒÁ•ä#]y—Üq }ÔäJ7{»ÒµVu˜¬zƒ;æ;2.”‰ ˆ(ù5’g[ˆ(±¬¢ðÀs[‰ë\Ei%h `7βC\%¸grµFÝ”jGÕ‘×Uäþ`Ô ëPëï‚Ì€FP•aY×¼¯bUÖ™éI;b#£—q/ȰMŒ+Ó“ÉFñ  Óøƒ,õY)Þõ½²õ}6–o'ú‹nSI#gó1„ÉH;K@ôücćŽ,A‚ ‘"Z'ªtŠ"—Ãy ÈQ ªšá0ëh¯žaÄ —Ø‚€ëߎÔehÚpxV"Õ &ª×UWbŽcëÆ‰ë ŤæÉx(äèõáZ|RêüA5@d‹XoÑyQK®«ƒ¡Wj ¥Y ©P‘ììkm7]¡<àgÃFƒ¬¤§-áÆ9„Á)§W›¬ µ“žÝ ÌjUN}“ùþÅŸsº› RuB¤ Eì "tý>µ…ü<+P+o;£´¼Ömžƒƒ‚%)9´”2djc¸|Där\>îC/C{©~ä´!c›Ë‰B NÞÅÃïpèá ‰,(5jåZ³ ¤|H)‚P6b¯Üsàqûã¶ö²¢ìœm¥h1·XÅÐiEÚ~;<ÓŒ‡Ùðæ›o.ø‰%ì £“1´àMaÃ:JåÑÞøj;ù>´ ‹ÄDE•ŠÊàÚæ5Õ ¹VäK¢§?U é¬XhÔ{Y]ãõoŒU €HÄÂA—C¯U)ˆ"(rÕJ‘†²' ²ÔHñHøá#Ôbp(:„Àóé9 ž¡v=¯iåH«áI9g ¹ M)g.d‰ºuOê$תç éR¬µÝ]ìOÌ»ÌNR-:óœ&Úoô7´[¢-ál…»Þ¬k¡IóxSÃxr‡5µ|ûO*R¾9Rübÿi|\`ýh˜Q‡5¡æß(Ðûv¼ÈwÙPó¼L¦/eÿ Þi.€¨ðÃôÜF¯·qnþþ£b3Æ#vx?ß‚z½D ¼X"H㑨X®ŽÎ}²zîO9ßÌØ4ó‡êÜl/ùÃÉx÷žæ¬ÆAÃvÁN\–«½Ø1 þj Ú#y*;´±Ü7Ä)¨1÷¸^MJ¡”ùö\ÄnÕ+õŒ žq«8Ö‘(»F®_^[è(”½ŽfŸI­ã•¡öRÖ–Kú;WU6J®A9ÿ¿ƒxë )çþÕì£ @dÎÈÁôR![o F˜hq)P©9„¿ }I?åNˆgàoÉçɧÐg‹Îèqë)¹‘ÂÜObÊŽ4Îv„Ÿ`™±Ú¸©w¡×›Í!Є”8Âp…‹xð0òe?yŒTrg7šÚª2@¼N2ö°9”uÊhê, ”ò³ï)ÁJ¦¤¶Ë„·–Eü<#×d•rÔôÙSY• i9¶Ü_¤¬äÀu&²Äªç€ì4 d'/U{ ü‚Ö!W]œädˆ¢:tÚ€±Š.IÆ#ÙhVov„ƒ&¹YšXÍ‘_ßœ¯63 ?щ¿Íqƒ!Mذ÷>é†ê›1ÓŒ’ù/(äçÐ$ŸAËj˜"žl¦=¤mŽùšâ6ߨ•“œ5j’f,|-žó¹Uø Ÿ"ß ·ãyUwR´åhIä/>t>ÐÔJûT×ã œ'! ‚®>~q ^j ªÒš>¦Æ²1£l`ì%¨xù`¢šÁ8U 3Ã)20Ð Ø@êwÌ›Åw¯îh´TF¥Áâ±¶õ¥ £ ©àîþ†Œ1ECù°raG~io4àÈè•jå¼Þx1¢ÐÊN/Çi[ÉŸ@\¢%lDa~ýóÕzhE¡¹¨þŸ1¿Ã®×Âí²“QgÓÛä¡Z‘¨i3 ª3™MòcvYÒ¹t}lpCÛø†øàMË úwàÓmšß‚ Ûrì†ÍŸÙÕ|ì¦MŸÙÙ—½¢¶Ûí~»/ O„|(Sãòv1Y @a. Iõñà«JÍ.g Ú{åU8lÍÍoº·m_NÅý<}šUS*ŸÔ8}g”oK;À_„¿MÄì*gÀsd’5rÄû‚ÓsÜ´,!rJbKÃiÒà»öÆ®›ý*k̽ÒB®‚gæ·©ç¨;ág 3þN¨«(K÷j°~G âeHáýÍH«TJ¡eª2É9  ðt†ßè¦a¾Mè öy½ D£ |̪&!`þ©žÓð^î m*–èt†ùŒÌgÊoY?æ•;åþ†›ŽßÝ‹1Šœî?dž€ŸÕ&uÆSÅ5Ib]–X“DâÎãèbHâ<' tZÎLça)pÕ`*`ÁÆhXη&í|Ó`T‘)Ó¿ä³ó­ñ&>9»äéù àweúwKe|§úT} ˆzÊ ±^¯öÝðÙ–« ¨œokbAE }÷ì¨"ÕÈÐÁ̈-ÞÂ'Šîù8þŒPŸ=Á6>ÄŒa…*Û‚L]¨¢óÜ43;‡-q „´F)ïZ[ ò GavÖ½?¸v¢TpF½6¥LN»-…bÞšžÓèevdÎjÐý„´[F›Z4: Ǥ¬^£ÌYDAlxOß…÷ä&* ¼ ”u¡,s¨j‘õOm"pÚ³TºF ÞTÇ£b L­T©Öï Çù)p­»¯/>kUƒ5—Iè" µÒì 9{û{穽¿vîé­¬é ÈŒ^kJokl¸qT%üÞ“…2S¯Â}«ƒ¸N¤7”pr[)R£À.}¤+®‹#ÒC ‘²âŽêÕz³E/Ó»NËìÆ}`ûeÖk@k6Ï _SÉ¢ïse¦Q^ ~§ì߯’ø; Z€ø[e*ZaÕœæÆË…OŽ1 ákŽ¤Åš²ƒfµiòˆgÁ½ƒäX3Ú›èwøÌÏ A•“^÷s¥zõzÒ•@z îZRäŽTJ=#$†Æ:’TƒÁ ‡ §Ü÷?`¡Õç?«OË ßf}R±äþìtþ™ü9üï‹Á!ð+òð3§ñK‰UO= â£ú¼:îv:ÿß»äÉ^qEJ˜ o°|ZD@=ÜIêÕ ß›Jä”4pHÈljðáð§Õ xä„ïn‡×=¿g3þžŠø=VчW'$‘q “RIÄŒ7¦¾Å/Òª¼žWŸ ßÞÅ’Ãrøüž%$ê_3,xÅùÔ"œ3ã”ÿ2Žyñ0æ#Ÿ66~ZsýK6ÝsÏö{ÿ›ðû›$ž¯™+…cáíûg4²i;¾æ$¸‹\Gž%ìÄ„ˆûTZ¼‚í(G¾â¬ÿT¤¬;kªýéP5DÅ,ÎdÆn{!ä â²Õr1[†;õG%GÉ8_ÎíWÈM´ÂÕ—*u4yœ|`™ßbnÎ÷jÕæ‚Æ,w{-ølþÄíÏcÛ ÕʃN“’ž²À´l£ÌV\‘‡pªUâPB ¯CéáýØŒNÄ6ŠàÏLIt±^îÌ©Š!ÌrÀI­9³ìqG*NcÖøo\£2PÝ$Mê0ÕšŸÕ*}°ÄéµÚÊ2ä_Üjòyƒ¶F°Q#ÿ‰L'óýX¡î)‡³&o9nÿ¥;ë×Û<>O‡Ÿ ”$us[•gLŠæOãx9T·S†ÇŸ91ùsÛ?öÐKÜâu¼V†Îgr:·¸„XpGƒ´¢q³Dÿ,rˆ?ñÌ—Mž¤çÛÁ¢졾 â?âŒýþ¼0¹Œ>ñø3s'¦ôeòùˆ-{pþ¯ðâ›ÿÝÏ£ªŸÇ’›¨+áýE&ïïÓÊì"îùïH÷Ç"/¸;ÄO‘ˆqÑG¼"ø“ü)rõøŸûÝñãHfp~râ0ø˜²v#t‡aJå@ÆÌ¤ú$P¯žÕW‚5ÁCLÞ»Ë^Oz¡>âˆY¢~qâÍ·\ªÿ™þQÎêW\ìÍ:m†ûZñ«¯ ¡/»?tµ@øÎNáÛÈñ¥óvr7pbLö%\3U9ÿöÙ§‰«+Šj>Y«#˜Áú!ÐäAI ðK±B-r]\‚dZÞ¾î['A$ÌÇ‚pï˜ ZµBd@îHˆÜ ¿£õT½Oô³ÐzhSþÇŸŸ!x¦@1Ð Ä117Wá4ëƒh•¢?8ñ©ûº_Á z*e$¼%Ä)Géb„81gb€`ÅQã‡q «ør9;T¹r®ÜPŒG~kÖž5èÕJŽÒ¢Â7ˆ› 5Î$!Î Wxho‘z°ºhP-W4IÍ"ø(Òá^ÆR¬1Ä–¢˜¢ˆ|ˆqdîTÉäØÒܤ¡òÙ¶ ËLrËe‡Ú²iRÛÔv;U.pêÛÀuzh‡ÞØtÝî¿ÿ×5õ;½&|ó}|?¨?Üß™aA\ÈèÙP$óá¾ǽÑ{Ӆнu-„E û%PJêÎ'6ê@ ƒú»aòÛÀ[HÍž<%w¤yá—½§O2£BÎÀ—øLÚÉ›å“oÁo%‰{àWÄ¡<,D±ªËÓª«ÚiÊÔ]eP£NSñ*Ü‹—.ÕkЉ|ê»}§kÚÙºúéÿ»uÿk7 œÉM\?²æ±}ݹ‰ëFÖ~v_µdï×ï]­~ Ìê¡+˜8òƒKú?¹mpÿhªeû«ÅŸ÷#íÁFqÞbðÃääR‘¼«K¿Ñ/²àx?ü’À^ògÂæ]“„&yp×n¼ñ÷&cÔïñ.ø¹—Á¹ß?7„òRzŠšAØH¨p8}ƒµ\OÕ8“Ÿù5Ml•®s*†Ó+Á5:›íøCO„†’°Gç5±f‡Rø&¯xâäq«E Áì\¥IÍ)ð‚QõŸSX ¡Á`µÀ·%Ë9P„|öC<È phP–].›JF Vä¸;U­,~njª'6bJÅN¡EÞ ¶®cêê‚èÊíT%eGœÛV¤R—ÍÚôêRW@•©ºêÆÁC¯^ÕÜ¿p«ÎÄ*ÔÑåó)~AA8ÑLvhü“_¥ÀÿøÓ¦h¢©¨#Í£±ö˜yîíß8´êµ/¿±Agóh3×?öÕ­žVßûûÀ†¡v÷¼AøýsQ$B•€Ñ@8/·ZÆKèD: #Ç>NnqYÅZ‡ˆXqA«ÐÐÄ-c$‡#5÷W wï½oÑÀxÁМ; r˜Õ*¾£°ùÕÛFv¼T÷wŽll¯UXp}|ÎÑo_½þž³mÆlc‹{öU=y}8ž¶i=n—Úµîøë‡²/n‚6‚É¥JôdPÿ>ÿ'ZÇ\ŽzîFÖÙ»Pî-•Æ" ‰úNCSëÑî­vÖjÍdZ³­Öh4jÁE¡€XyþOÔV꿉 ñœ¨Ù@pnÀŠz3€ôÇX5Y §4I!†#¨V›)³Uïe¸Fn$’Õ'êFWR°!IT)—¦Gy¶ÑÏëMÓùà’‚ÇZ­XÔ2½ß&ªß—ù,jW¸Ü7Ö¬kÅ­7%[ã.•žm·5®{`{SßÑ÷>éÊå nê [ϰLbhgïå†csgqµYŒÍÛX{èûŸ^BËT(¦\„²…²K#Ù!á(P¢²_µ?¨:Ù‰™Ãc"'梩ÆZq©CUvÕ„ã ‡×=K$»™ÊÒÄÅÆ‹9Êz‰"Ùë((;¹FSö—ýS]ñ"¯´¯jÜûøŽÒÜ£ß:â¬tvºõ2—S制$ï~Ù!°ðWïÆ¾@!âQ[\€ú²N×¹çþ‰ß»kˆQh•m. ”ÞçN\Þ»s(Eªw‹gùðù?Òw0{ÑZFkùÇðåîJÝ?‰ j(4'ˆ ‰x Åàd#r “iëÛ­vv´4¢¦ ™y%×ô‚ó¦¿Ný‘BŒ›ÈiƒûœD%TW P ½eí®:Yà‡ñ~¯;¦¡]©!=d ]P‰!nØH;YÄç1ö˜ÃW¦¿¾tKÚC%%¹ºqU—ýl±»Ù:™X _çÛB ¿ìšXän°ïQ7<ùÑ3[[Í[ææþæáP§uò®±'j¯møÌ¯äÛ­(šL7œÿ#û4õu"HôKü' èR9|Ìr”óŽ<Ìë9 Ó*pgY­°Î&"ˆP¨¿«Rn@µàðsx{0«Œ¸ü.Q˜Ú-(VBWmˆ…µª¯ldŸVP>ÇSKÏe÷/YwØ•½cMïîá”ÑŸr¼#JƒCKþÁdrs<ºl¨È)µ²s·¦‚V¡Ã­äržúú|«·ûÖ®k¿ÿ©Ë—7o½gyÇž­ësý ‹Q%<õ»¶µ³"Ñ5áçÌîsÇÖî>TÚÔîà+ce ý¨K…Ë©ßc½U!T”YÀÉ@—>¦NÑ–Œ CìB&«¶èå8üÀÕR!¨¯†Ä‘ðUø‹ºg]x Êü3Dks1¿1íDcþ€Â,0‰±ÑLX~ R«h¤x$TV䯘|Oz$»üè¯-«æ<¹zþÍëšz=³yÉ)R®ÌŽõÄWf‹}I£Å-hû;gïIí[S\1§(.oˆÈkOyhW[ÓÖ«÷¿sßXo»axk§Sn •™ y¼·Òºû±ÍGoŠÎÙÑ/öZ8Cý ÊÌçÐSétC™¡–%»ä,I¢DhÜ'AVUÛš)`=€‰˜ß /vA[ Áé[gNºš}^›®¤œ{Gà©Íq五ûZÝ5Ê'”7kÁêðž.8»$ʶ*®˜[’ï#?[?¯Ž¦?;®ïïBšµ©×¦uÈ áEÆÄl@:dÒ!¿‚÷½E²OEåXsbîYœãF°2wW™*¤lª>l¬b •LÄ~Ÿ8€¥I”âùÙez/”i„¸ ó™êzÜå„?‹{ØVƒyðTÆ…‚˜°­¾j`†Ñ3R¾’x΋,ªú¡cyBñ? †sq¬ùbë19& ÃIÔMœÏê\Þµhï`Tɺe8g±º3”ÒÛ=?çɵÚY‡Ïj?ØŽ=]iõ­‹õŸL9ƒ­¥Ò¿xM·Vï¦ÎõGp®"ŒÐ 2]{;%-eÃȢу›• |kØÆMçPƒ«:èŽA Lí”h>8°(;|ô«»rSKe÷Ë£ÙyGÆrWìBÿ΀.EeãíKþðáå›î[µõ+'¶˜„—ÁsŽ<¶ôæ[<ºôæ[Ñ3i^À}ùDÜNbÀÒjók=U‡Û1`á;yíäÀ廽ðÙ+\¹ÛiÒÁ½ñVÐ-°“Þ?·M8C߉z£®(Ví…¸]ì•SýkD‘°{d]DîØ#la§xÖwÊÔLôïz‡í³ûïf Jáë:Þjw) ­ÚC÷<ü˜Ýª:EU[”à7©ÎeÈOócá6“Åb»#KøsvÆ¥4 ?ý*«á}®¡~Jj˜½Ï¤ãÇìVévÿ]ÌΔ¦0»lhØšM˜½ö«Kau¼rèÊf|åÊJläðèZ½™•«œ]Ý9OlU£ðù6²Yžü2¸çùfs*ÛT0ëÜxGÜ<ïèû7}ñ ¯mÖÙ}:ï¼eÛÜ>?·CiïXyëú=_¼ivB̉é¥×‰:øÿ„ÔYæN"OtG+º$`貎 8Ö q %‘eQˈæ§*^Q`VV¡ŽB§4Yæ‚KHrÓ%/CŽ ]±ÐÒTè,všcf å3sj"µÊtÜï/”ÕÄ¡P:U;ÏÀU=¯Ø3?Ö{äô–‰Ë¢>Y¸qV\i1(HÆ´i™ÏÈ¢Íõ/¼;|MkKx}ç¼+&7Q½³FËëï»éƒGÆû*çºL¤\§É­¿!ÊdºÌS ÏÎë‚•bÇS»n¼¯‹ÙƒôIˆõ:‰wăßDyÉ2’‚ö3A© ~/šaºx@Vœ—ß•¼Ð¾?aØ@Ý]-ƒ@aƒA97ìÆ‹¯Ò‘ á•`m$KÐ,½ø"à:om[Ksc!—ŒÛ­f£Ó¡~Yuð“º}Z5T _5Q‚4šl*,iµÛ̵gL“׌®Ï’žÆ@QE®,·Ùmö”‘<±¾¼»L²hÿ†¾È¬¸EfS¨2þ@ÖobŒ «xÿ„»hªG¨küà>wÞDn{ò£§·´˜ŸmÝ6’ÓzÌj­5ÑÁ6öùÁ-ä"¯Ä¨%J;­é5Ùâ%¯'k‘nâ§/ÝW`,žKÚ‚qS°èU2ä"aE0œÑÙõrg¶+,â¾+„×»›ðMD¥Ç¥AÊšRS@('#XÙƒ½(¨úEì´¦–6ŠßW*dR‘¯ÉߤC Pâu•3rc]íN†òÞD9×Yù8¯¤¡@xG7é’ö뎽ðÔ5­{µÒ7MŽèR¶ëNžºŒÑ¨L 3øt°È›Ù5:mtpKÏî«ì¦µ,ãHÓ¿1ñg7eP¡ž/ÌÄúœç¶™x&pnQdQ6Þï¢/ÓG:Ò}fN£gb$KwDpðVT»„}v^¢­ÒŒ™¡Pe)A*€Œ#eãȨêa«ÃV¯Õã°až>ê×ëÑŒ9ÜëªF´dÄi—T5?à-;Õ O¹·éîU™î#/íÖì¿^½}IÓoï>¾ûøÆ ¿J/ØÝ½æäîêݾ1Ù!Äîºz|=| ð´ýö™…+¼Øg·‚py…U’‡H꺅Œ_ôH£[0ú©g&ÏAƒAøÖàé9—öö‘/Ð÷‹®Ÿ¼›Üt=YÏ·}Âs´~G‚¸²¢0Àðð±Oy¦%nŽW;©°ß¿›“‘äÒ‹žiÚ„±ñÃe`jô´‰x4à³YŒ½N‹Â !×&˜ÈŇ9šH½­5¯¿¾qM9:k}ûÜM‘Y§O’¿ÔN~½©åPÿ‚;6·/8öúžÑÅ´Š¡ÍF]ö Wåº%W úãöÉÏ3[„¼ÇŸ^ÿ™Ëš<µÌî(Úµ&ŠM¼%¼Æ¾eÐ…,Ã8M¢¼]t Ý¼šˆ,¼œžP«”L¦êæïIñÀÙEt¶675–KÑ_²bl[BÄ"Ac!R38êÜàoÅÆ(!BIqæ´YÄJ9P`nu5†#Mæ?1–/ÂmeßÙÎkÞÒg#‡g?:p(ÖèÞ0·F.Wúûâþ„áÏ·8ªo²þM}õoõ7¸AÙw2É?…sMcfò›…-V7¨\àû!~òQrœ IW€lÎ4jÔù0©qÛ„rÌNV&_sÄÀ7¬n 3Ðc?k¾’Smg›$˜ ¹ï¡V§i¬¨Qé‘l!NѧÓj¹P°m°ä+ô1ÇÂ:GK ÕhÿÏÅ9÷;|êßè“É›gŸ:ù,ù= ¾kqM~èK¨T9aHœ‘Ë ~tO>ÊH¹5à?ÿ÷#'S!o¿Ç–¸ÖWµKrsY¬ãPê²l5AÃdkb£z v#Uc/ÿ|t],鹇ö,GNˆWHà ‰¸iÍÑ0ª’¨µ_•¤#‰%‚ì?Ü?ª`, ¬13ˆkãŽ;„OZK¾d£é§8Ê3ê°ß{Ø»vö³'Ÿí_ô»È>F¢™|  €6ÒOCÙ„‰R%:9€.¢¹t Ø¦AŽY`ñ“¥{}¥>„Ö·NQmð7㱂iëªÁø,P„¿y.|²ÀðÀÔ3ÝÐp…ýYzì†?N²“oTŸ)Rè™ÞÝxß>ÜŸ ùºjíg6#%AKšÝ/ÒŒP^°ëoÖ?ø}ßÎ@Ñ t1Ç]Â[³/Ù`=û Ã£þ¾.»fàô1áKHzèÍîÉ_øJuÞ Û >à=“Òwâ5·î“áý‘¿L Ÿw' i3±-U· ©‘õYÕˆð2#aä bå¹%á¯ëV–ö µw`òñÓ'OÍ"Ï:õþ™<€»‚\<ù8¼§3äà?~.íjt‹¡ºUUÏ ¨“IøxQe<=Œe- 2íܘêTæg>:n;=[ÐÀ½vjøÓ,r}ÏÙS_†¾ã2á9ö<üŽ,qmEaƒVupD••&„èIÔe¥¢ÙÓ@NJ®k=ª Lð !eÄÆK—ƒ©ácÌ@ŒŠ¬> ÓžYUÔŸb`KœÏt^=?èß Ý°ª™µ¶-ÜQLûO›|öØé“Ôs³›¯è¾}SÛ‚£_Ù•I1øÑæà!òÕë–ô«Œ6Õäç‘( >‡N’GÐI²\©qZ£cª]çÿDŸg2D?| ÿ+JCo…6Ä`{’bä,kÕ¥ú3ø=㙯qSu.¹äPP‰@Pí"Tu3ºÀŸˆFùp#€ª $á¬èö4H!Vn*ž ú;/hÈ ÄD3bÙ°Ð`ú|fS©´»áªc[Ÿ>еâÔÇ÷Ù’ñ„]/ ˜U6oܵò±+úÒ  v·¤3KŽÌk_Úìô·/*z›­é³É-ÒÁ±eJ[ÈhK9u­—ïýÔÛ‡®oï¡dJY·3Î0ÌÂ>¼}̓Û[ Ï]qõ›7öǯèxôØþ¬F®…[úå­GSƒ?ÝÌgWßÄN¸¯ázçʤ»ÚsX?ÊXRòìaf =Ó#“„L'Ó¡Þ‡R·'ùT¯{PÝÕt£A8|ôÙÛ…Ãzð¬î¥<>é˜ÀgwÑGï¬ÛÑ’Þ£·ÂïwéJBó}¸š§q)ÀZ¯_ýІdÓˆ€ º‹ãÂ(¤¡ÁÂWî³ÚdÏ+ ¬ÒcA…ÏrBP+ÿ ÿ}òTâ÷ð›Qò6½àv§­Ö´| uNºGoN€u7༎û¡\Àûò±JØ„Ül ‹‘´ ÚÁÕˆ€ôDŒX3ÄEõÇ?DAlÈÅ»D·-ò& ÿ²(­Fs–• 7[ùSà#á§ ÌOiÊš ö—ȯM¾.Jɬ"ËJÕäû~ò‹äZ•F°Ê42ð–S”´s®Ã:žuЄ²ÃTíg , ü+3*>C½«d”aޱh½:†Jò~ªb^>BŽ=wÒøD 86EÎí>&¼ÄŽžý}oíñIçÂóPNûá=8Pn¨\äO hÄê»QQ•Ö¨(-h¶êy”Uª ˨¨=24õ<§Wÿð¡ÍÛ6ENõ­yéþà xuñ¹’2{è›G眒¾œlvùÈèØíëÁÓÂÈÄŽ”1Š¢!è~^‚ëi¼'qGEzyê \ª¬Ç>h`CK~¥”a2! Jl`-¬žr`•„ÇÑ/=±dJÄl+ò¯â²øŸÖÈšÅÍ¡Åji#.•.³Ð²ýø¹’vùÞÐ>6^œsêáÓ}Ù‰¤ï°{ìèʸ† #.'ATøkÖ›ÿñs²Yc–'þÈø—zª]çmÃkÁ‡ê€H€q¬X51Œ—,‰D}šè<.+Hd”Û©¾u/Þ³ožGU<]PÅö¼ñÉÁÓð0xC’²0ž†R6E'ïÆ6æù?3§˜"²+ˆ9•€(Œ×TÕ¯C•ÅÁ“Oëá­˜jÉêfº§®ˆYÝs{æ" #áƒÑ 6²©:bßi¡ÚZãšZJÊŠC£î6¤I°Ï¹û‘·¯XóèeJ£Cwî4õŧ†.í°jÌl²´ïê{v~þúÙð>rKº£ŒÊ¤YjÐÊä²¶¹uÑÑísMä•Þ9û(Ý·î˜ß}åK—µô…”±Ø÷þ;–©e]¥9Yë¼»~ô kÊjH ”-ñxÚi - Îäð³apGã¦ããÇ75! o`uóˆE•‘<”‘ÊÈ OAjJN`º˜,å4,ú…z{1{ g^.¶XÄ㶘ôZºA·²®r}†¨@5æ¢ÂjB¶VåEýéâ¢ûáîðÉÐÊûwµ]DV4’ñ†:y1Å eµt¹RøÈ«—ÜùÍ+/!/$ä;k"ƒkx£ð"}Æ^û* tT²Ù=xïÒ¸‚i‚—²×#5{ý¡ØêÈÃc· ¥®V‰x«ÎV ·:hÍÖz]Öðšµq®0²¹ñCl£ìœ`®>f?.ìsº.o¸ö¥íó‡K›LšøË-û_8°ê@—U´VªAÐÊ¥&Žo;þŸ‡¦»CcÒãóçÔ«ïa½:Rcé§$÷HÇ›Äæºˆ„ßÕ·ñ|Ñq·IC &Â-à10‰Ž,?«hN¸<é€Ý}ü…§®ÙL¸.¾hlÙêåÕŒºÕ×Ó70g^”üÞÍÈEß3)Ü"|ôÙ<¿Ã½è!¨^AˆŒžß^á9ú/؆١ÝîÝëD?Iç¼™“AtF-åjÉcÝÕNb´5Œ8öiä¼pdÕg¯Çu:ØÖôGPNHtpNµFqèšÅA¯P÷í¸{Y|aÁ_‹A›7,éL(Ú&¯ÍC³ž[·ñäŽæ*èõôlþ6e ù­ƒÏäI8?Ñ\)k‘ $Ðì!9tÀ…­‘:C¤[¼Kø¯ŽÐ‰6 2ƒB¢ä§L!úÉÓO]yåÀä7fŸ~ÞHÙAXÐÓ¬ ¤w_ÞÀg›!h…‘bl ÐÈÅ­ P‡{Ô€›aUײÉdr™\ú Rþ6ù/8nx 8ê[§/Ld5ÁC²tDŽ^T ¯Ú ~&»ñå³Ç÷åÆM/Ÿ½óÀ—o˜\»ìÁC ôç^ùe7͹‰ëG—Þp¡‘ªøœð¿ßܵðÞïù°s÷¢{¿7éîØÿô6¹^¾æÁ­äª5ìlmÝýÈÑo¿Ix û…eˆQñTÕŒfhLÇÜÔ6´Bd„Œ×Ãû÷W1¬hóƒŒáIÑÃF^>yöèé“ä UÜŠí\ü}Âs’:B<,*'… ÈH7+pv–ô‡’¬ê®44ì)RFm½Ö²i^k/j%P(0ß¼^UY©z¡ ””BùO.$”Êêuc}ˆøœöœ¤7P „éNpD‚Ð Jð?\Ä9>ùˆÆ0×sq¦‘&_£Ï*³(^K2&¯w›C~Ò´¬;¨ÓC£C;:›ì çè¥PFâ QJ”™Ë*¥¶Ú_TMFni¹Âóg”ÁÎÓZþš^t%J™hŒ«®p|Z) YÅ#Ò5¨Û¡Å]@*òÏŒ,Å×m\°}¬Õ×Ê[9«\ÉÛóYwÓ¿0¹¾öMæetúN“J»p íÒ¦Ô:xÛ‡ \x"[I9Qu—ŒƒW½ÝÊ€ªáʼ>æ×Ãÿ1Xín1¶€OÕXeNçЖþž5è˜ÿ¤å ÈÉšÏ »É |4ù¬Áļ ±»ðy™]ÿ¸Wø½‘üÙ¡QNžçÔ2ðZ;ùåë'OÂ{w° fò¬ÚD.¿>£ùð~µð~5Ð2ŠVá7¦õ"‹Ý'9*ÆÝ´Z­SëùõAÖ˜0botPAÅÀk>ù}7|íÈë­§nÛþÐæyeÏ׆&|kºÁÊ Oé¿þÇÏ ×|‚Lå_Þ[ZÔæGûmÑù¿1?„÷&:ˆk1ª9BZ"„bC©öå Š#9‰Ä™B$Îè.eH ˆKÅLKÄtðT`gŒ(ªŠa ]?VÑe3åR¦#ÛŽÅx}Pnžš+u|h8nç"ÙjÓ2lv0KTg¿hî?¸éƒ¶Ó‡4³æ®\.'ɰ>oºaù £®6ùnÜ×¹ä¶5%ðbeËP†ÿ|âöU¾{%X±ñ™#}à×çì›'víµæâ>ÖÇÛK…´ Xº$Ò‘²—_{Ìß² äïë+-nçñZ{Ú[ßcÒD?ÊȪ†ÃÞ‘.x¡Â|²R£^ и(ö-J<¿P f²'ÈGýÑH,‰¢¢—ÊꡬxÖÅðԬŮD¸9X}rÖT›CZè+¯[O‘zÖ·r%œ?à½ãȾòx÷®‘,4qÚWu¿¦þ|×îÑ\¨o]çðå J:§Ú¿ä‡×­}pG35Y^3Òi‰½òP㬤‘ܹך{Y_ÈÞ%ñòëÁE·mœ³¡â:æm^ØëÉ9½a¯sAWozx§¸ïNB Æ@YdPdZTt×HÔϦº|™p®ÖL¿t†ÈðN$û¥—œ®HÏ«­:vÈOÄ[®ü¬ö¡¦U}Qwã‚R÷š¾ŒÖ©ôÍýòžÅ·¬Ìï^?±Bçzyx6†Á»¯?æm]TŽve«óH mïÜýd*òè•ÿ3å×6 o°Çá¾ðΘ¥Ψؽ‹ªžþÃü‘™ï_ ú#^áá!þç1/5åñ‘l€pÍ`SÇþž&¶Maá/G'¿œ^µiçkû -$77³rùò‰¥I&ñÔNüš§Œƒ¾ÎÄmçÿÄ~ˆã0$3@¬© ÔV+Äö` ÛWgg&Eࢃð!1µ¦«@(† õÅlÊr;Y,!½j©.ç:3€áX¥¯È/—# ï[ŠB]_z¢× _œÔ<‡L?Íz$Ký cuq† [‚/¿™wùHÔÇ @Ì%‡˜+O4VJfÀ9@3d&Ê¢D¾èºR yÁóD>MFC¸L¦êÙÀ•¢î¿úš•2nsÁÈÕÚ5ŸþÎW÷ØÜ¯ÙƒW¬ ´h"fS®oUï®WnlÜ|ÏÊ…·nêV ÿ£7Be¾oÇ‘7n™µðÞï_õ8°½µÓšh ju(`[tÿû·lxtO[vÙ-ãfÍqoÝõsÇúªËY«úH4;f M#HÍ‘X¨ZíSUŸ˜XSÝ(3!`Ú$S‡Z"ëÖ!­ô.EŸÞ|~pÿpP±Y°i˜Àç.õ'»jZéGQôÔŠ;†^1ô$”9ÄÐ †žodv¥¿~R!±(‰˜N/Q0PëÑlª/èì…¢þ0ÏËüb 1ÏϰâM ¢ê3]5g_úc ñðP»È†QÚ¹ØZ’a/Ä=UvºZþE/©ƒ@¨©ÉRìEãÀU]æiÈ žtpY" ­}£?VXÑ~o†-wu¡€þÒ±Å:¯]k…ji›¿·pV…’w² ٻDŽ»â‹¯\×°ŠQ·z;{úgÏ"}À+M†Ò=šÔ=¶]ÊMŘð,xoÝD‰xጙ‡RbFTÇÈiN>¡ˆ±OÉ’(e+·Qª^ù‰Ú$ŠrZd›kW]z|%yñ¡5‰Ãû59Vqx=QÈ!vdOÉ[ÂĘ˜L-ªO”èÐQeÿ0‚ ‘‹%Y¨w6Üûâƒ7å»õü·¶TV·Y•ÏÎÖ´eVO ¹Zï x3³åmíÞ}¥Ý´V•¥Þ9÷ŒÃÏ_yu~ù-KÈ퓟ܽ=;vdøø7ÆhgÚ©Oµø×Œãó°\!d§ž¢Þ',o^:÷G{Aîo \ûƒ²´/’û#‘àíÔ?1~Ù•ó îZ!Ü8þ¼æíªø{ÍKZÜæô@‰zÿŠ+NÄû—.Í“¿½âŠÉ¿ yJqOCå{…ὺ©· -B£,¨¦–‹ ‘¾ƒçªT WT#<#ÿ1Ô 7òOèox¢áÝ1Po_%üò Ð ^=ì¢NÛ$Oÿ ×9‘Éw´Y8Ã_9ËÁDÉtÕ+EÊkÙL¥Õ7ùGW6<±ÿ‰âh¸ÅJ4‹“P•à$„µh 7ÈÁIPïcènÿÒ¾>®âÎÿÍ+»o{¯ªÛW»Zm“vµª«.«XV³´’,wI¶lË7\Àw0`Ls0LB/GÚåHB©F’;.\r¹’r!`=ýgæ½]­dÙ&÷ç“X»ûfÞ›7å׿;ßjïèÌ õ{Ø^±1)µ‘˜”(ÿq I ÎCtð+ ÕÈNÙÚ@§=®¹ÅÀÄ‚å Kcòf^•º"–'Çâ”B5þ­)ê(ЍŽmÄØ~©?/{Aoì‰]Ëzó¶ås©‡ fî¤+ ——ú¹ Ö-t2—8þ-û®2È¿g;|ÏßÁ÷t£x«{ˆR X!% e¤E F¢‚Af‰›p x(ÊÌ~À]©›®“qÄÿ¦¸¨¼ º`îrÆ?ƃõwk™ÃÕ>¹#P"ß«Œ»‡cOŒÜ«¦Æ&'÷¼j/±¬¡Ú€ÉíâZÉ?îíîž™¡·Áv\Ë£šÅò8çby.o$?“l+Á·J»] Ó¯pó>°¢¹¤{aGIP~¨°{YìÉuÜo¨7Ñ,áÛæ€k!Ý64ºyø4†mÃ8-Æñd…®UBèšMïÚñl¡ã۰…mÛìv8Jå›&|_.çZÊálWBw©òS¼ÁÅ`Ëö€›.È;#˜Ø%ÆñóSܧ'Ÿ@Â0‹ãg#yÈ¿’ÅjnÿÄÅrî8ÿ´…P^z:ó Áï'ÝÏÜŸSBDì@ä°<…°<ª…±Á-¿x%ª ®¯mZ¿ÈÕ´ãÞ{½a‘Û~S6ùñq^Äw_z¾¡ÝA÷?šUØ>X>ó!m¦Þ%êˆ.¢>Y«‡ë¾©ÜKA†Gé\Ä1ÍÖt´i‹Š¼v½×‰Xi]¢¹ÇŠßtJ Þ˜ÇðDcè7Ű1ø`m(ï¨ÞŒí¨ZróêòÆC_Ü‘LhÙ\£Ôª\¼«Ûg¯­Ò»µÑxIÏÖ¦êå5V‰rªÏXb5å+óL€Û]\ÔÔ,±EZZ›kµæVnºÍÞß7¨kÉÕ²nûƒ+[·-ñSLâîÑáS«¢Á¾íMë¾Ô̲"Ò˜ Öwìó'ž¯ë²× ¢9sêÇ-xnµ+Âx&Ë&¶fÓ ¹ÓÓÝKAAI¡qM÷ÚÚéõp‚Nùõ@uóòmŠ{‘z>K0ðrtRÔó _ù•Wl×PZ¼aD͇ºŠàÅÐßNèÍôVBÉÌàfs57Uq»ÁQêƒjú)[maa­ìUæ<+Ù[zDœZˆhxû³ ª >ý‡6 3Gõ¢ñË ”¾¦zÍtÑÿ•.О=_úÒžÞšB€™‡úöÐq"ÌQ 9¶SI °Q]°)²{m-â¥ÔÅç"lx«"OÅ¢“¬þ¥øé(Y`/:©3Šžb—¤fíÝàлçuZѽŒŒý¡T«ü©äÂuüxè•à°^Ã]PêH%ø†LÁ=/ÕIC©@ç„¡¼ôbyy9ÆFõÙ06ŽyÇrLûè^wq·öÖ(o\W4½vÝ?¨7oœŽÀ%Ä&µ%é„}õEø|3’•Ì€–§jW#t’ɤq N9° «Ñ=¥ ë|/Gwÿà¶V¥÷q§Ä»üŽ5ñ‹i1ý¹BMôà¨ç~ÚÜ]¨0ƒ/Áçovúøü ²ˆ&EÃiÎOÌԂÑpü¼Úb›µñýƒo/¢B'¿ ëT'+.OdjgQä8F мŽ3”c5ØvØH=©ç9âðÙÀ§)“¾¤‡H³Z ²dîù79!G&>¢¯FÁÇ´iE’súÁ38iƽîâmûÕæ\((Þäu¤›» Ä7ÝÄOyÖ‡\ •!éÿÊ3¬ABBÑ”„žàc ’¹1Po ÈL&í%¤R4£4–®FÈh9-“_­:!—gj'£W®_V*—̯QÌ:§ÝV`5ë´št@Va¾, ‹þ½ ry”vÖÜeb]É‘{µÃÄšd2»5–Ì Þ^zúà UСáƒRZ,`"/P°}Z ¹-R"Þ ÞÏÉúÊéþ-Dó E@![ wzÊ êI—KÏÓìòY3”~Ï)‰W>5UA ¡Bð­gG›uQbPÏê® gá=š±ß>¨×ˆîg”ì›:ë½ÜcÏž5ŠÝõ£µŽ¤w˜+§,*eSz½èA‰ü=©Usü üR)çî–å žÇ•p‡^¨Ú؃úϨ€ý—4*Àu&5÷ŒÂÈëÝKÐŽ~ ÇI•N=Ä»Ÿ">˜&@V°ëd8ìÐ ßfÇæ_~çA½Zô(£üLjPÞË}ç7ê_¿ýÞ zœ‘ŠÞ—X5w–÷Èrp7lè]2l¨RÅÃ!G“éYÔ:ð~ý¬ùù s ŽuÑœl(b:yå-”¹ÐÜAÛÈ¿šiA…Àü¾lŒ„+á†HƒÎë,*BpÌ1Ây —ÈÅé¨ùQ ä¨`ð›n6”AýŽ•»«ŠÖ®–¡ËÂå°›èÄ꓃뙒ßÞ9tjU´íè×v®º1Øpqú\ÅÆ(Þ¶xéÑågrÔ–·—è¯Ûc‰Šínk<Ö­¸s¼li*5_wï꿸¨&AÚ?ùÀÕ0ÛêÊj¶=:Ñ1Õ*œ÷<7óg&å`;â­Ìpˆ)pºôœÌ&Ã!æ&Ä"R$&'”4³9MÙ[¸È¢ $J‡=&$ÔX¸x*©imn¨KÄc¥Å>[QogÕÙ a<¤dn @š…!%î 8—|~çÐÍÃSi_ÕÐEêÝ1Çø¸÷ñ(êâ5§SÛûÀʉ/­Þ)Û•_äɉõ%’#u^…R%) 6Fë¼õýÂÆdLcÙ3éO8”;÷X¢~êér¡§]Û‡’nÕ³¿Êa 9 ¦“DòÞuÁ-°o#D]²f¶'³“FÏë‘yJ¡H4޼ytiÙ ÇB·ç–Ê'—ìî.2„— IuŸ#Ä÷%‡tõuë“ñTÝXÒ*ïJ¸ä •¿h]_ °¹.®É½~ðœà´»¥¶˜‹á[…·â×÷3ePì<ÆÇ-PŒ#,`…€WeKàš1:ܸXPÈ5 M‘‰Â›ù¼íž5¡?Ëìq×È2îÂeЯ8Òײu±]šä¾e_’]¹2è)"e[k{û¢æBrËœEápeEQÏõ½¤B°¶ŠŒ¹ ÇjïÌG"|'ÑÃï¦çˆOê€"ªy(’‚ùW¯)º >2 _ØMfÍVN¬@r` 𣮴85ض»Û%ÕL\~ñº‹!Rºx¥5^ïD–m½bæoÐdü5QŒ0$:è:ûƘæaHЉb»Û7CÂK¯l VÁx¨h —wî~håÄ£Ûjzï|óÀ®ÇÂa…S'w–wÇ—[Qz÷m‡Î©~¢V)øÍØð™±XÝŽ Ë÷ýàÎÞEÑ:D!6›4µ[\}ö¾Û5Êeq&U7ÑêQC™G[g,ôVQ ‘$A‰v{Ò’cPSÐÍ,I[cÈK³t%4°YZ8¶$ Ã"A` š•Ðìç"ƒ’@g† ó”ëBW`ÆØ’j0“W¬J*zºrœo‘Ç  ­-ÌÝ‹]á9±J€Ž?¢ÜèJ³c|TR…ú8âÃBÊ8 *¥ƒÿò,i`kÍò:{×ÉW·Ðyú—H.òyòø±8‘r´²Z•TgÎ׿• ª5JYMã“°`Ò_^â‚k5eßlÚÒøn°ì—j‡^¢a]öõǽ¨ß:i½[›û ݪÍý¹©5Ø/‘+9ŠO?PT•ò®îqõ3Ú¼Ÿ“²—µyTùÎ+©ämúÜ·§m èz”„Éqt¶="êˆ÷øéª·ÃþªƒNA%”˜Z ®"ág0çç³!ýaƒE¡w\}F}›˜Ì3®ÔJ[w\XÕ¾©«TÝbò)Õ…þš@b´µD¬ʸ!5x ãŽ+%¡â¼x9Ì’ C®úҊ¨ËÂèo#‡#ëé¥ç6'Õ½a¯‚6Zü…ZOëXíéeð’e_ÓJ7#n@ÔÜ뤹þ*ßcÐ è„VMc'<„ŸeD%1ì‹À¹Ã}¢Ñ©Q‰qBø!µ’¥BFI¤„$ÅŸB•Ü'•áP°1…y=(¯²½ /׊†Jýµ>«øÐ  §–qÀ¡Ã¢ûÜøËæq0Ѹ oø‰ÎÊ7xêY Ÿ¬=a1²o€šÚ3µ÷HïUÝ-"ó “UÇõùâ¯s¯TŸ©¾ľôMpáÛ*î#îÂçö#Ei 5%;öÂþœ\ÏÙÇ‘Ëö†59œF©?à4àOËöîÝ» ›Kô—þÝ4|ÎÅA8OªáÜ4ù„‹˜òèÊ “N3bñ3cá@Œ„Ä3¹.üŽ.‰ *µ@-¾\*i°Z\ŽBØ]–|k¾FãÔ˜lj¹Oo©o€ý¤¤x@ƒ8ªÅ:NY¸zª¸6œ_{d»ÕK<µé@Å£S`ÇÉ‹VnI¾<}ÙýÔÀö’ƒ=¬R¼làøªRÎC-ùß½{ÿôj6oÅy¼w@ÿ£Î3á †ˆ/$•C@L+%ª„4ºœ€‚E,‚²LBIᘀËH,’‰á‘¨•ðùŒô›I¥D/‹í9´ PõOT"ÌR)Îmm`aÿäÄàÒ–¦úde¢Ø‡È,s,°©¦b›F¡å-´´sjDÀ"~rx†F?X ¥µ{–ð»®™$Ö`¢á™AÓú{ׄCC» ÿý¹ úuY­ût×®î¢ýÎÒI¥Nnµ—ئ®¦@Ñâ-­­»œ~V¯RYía=jS—4.i,q/šllž²“îé_0öâží­KŽmèÐéÜÓsƒŸu‹o¬[51ý}½Z§®*.¨ŠE|ybU«¯ÌS)“ëÍz½Íg²Õ”Güed¸©(äC6¹aŒÄ•p¬Âp­&ˆj¢ª¢Ab<¹¦®Ù¶R.#ëͬˆ¦IƒN#¡ñ²ÕkÕRš0U Z&'d)̈&oPBÏI!oì^Òµ¸¡¾A Ëã±²Òh$³’=pÛ44á¶ 5ìá×0%¬aôyöÓœV3WqÜÆàÿ{üZ6åÞ€ŸAÔSÞª¶KvrŽ þn€¼2‹Ñ°dqscMU" y ó uÆ:×)צ©OJü…¤‘ý#ÄÜ‘šÇ;¼5€L㸎@£žùQÌŒ;7»0¸xOð{öœ}¤§i¹õÁ'~úÀ¯¶Påå_ü/nún‘møtíšXhõŠV7¿6ôì…›‚%ìÈ·ªXÚÞâ/pÑ2zR¹8]·j©Í©±&K|©¾ö<2„(×ë÷¿¼sò¥#e\£è¹5/ÎÜ{ßÌK«ûØgŸ&‹KЧ÷n¾\rÇ›‡þôÞ½EïÏ[ýéÙE ±ñ;FÇnöu_·˜ï+¸Ó´yžˆ@ÉܜԖ—øLx‚õ;\ܵ|([æ{H§ËÁgÙí?Ìÿz©¡.*¦yx,>>œ'íàyœQ_,CÇ…1Ã3O팙œÏml-¬ç—m­¨®-´UvrBn£¿c¼ª²¨)œu/>Wyì‚Æ5DÞ¢É÷}Ñ<©ÊbÓå”ù¬äanøüÎÊ=‡Ž¶¡¼À¾ëÚën½óÞŽÀ²±ÍÉ¡Ûw¯ö׸VŒ—->×±ý»]à÷¼‹fÊ–öxºm^[VÒ\bòvlA»Ä*øÏ70w‘ Ú™â?çKµjèÊÊY’‘ÑLJdÐL‰† ¥R°TRlV)³ ;9ÉúŽ.AW!u­[ð•RI‡×›—ë x¡Zó #è¹¶<[ŽÕbÖhìFkSëx‡*3dmáƒ"":CŒè‚ÝUÂÃýËlâïtƒò7¦;#¾ÊÑ$·–qÿøÁí‘GïžUÜî½ñY?Wd®oiϽ¼Ä0\¾’ú”Þv0¯Ü:xö nŠüÁà·opíWÀ/Wöp7YâShí˜qÑ_fþjø¢)Y¯¨Ð‘4á€jž ÉÔóTß"F4‰œ&’¦FÒlD®g`Q#è° Èã\¿Ÿwö¡ƒâ¡øÔ¿xÅÙ(ž.Û×¹eu銧>º=—ÊÕV»¶]¨¨Rz Zo¬%ôŽÅºxoûf ”å ½fÕvjìj;™««º׬ɥt÷ÿÔGqì²ó?Ãv©Λ°N°>tF؃ü²z¥.3 ‰žpèNƒOß “eÈ/(_A~^.?ÄÐDD>£ Ó`:L™=>›°‘8`óØâi“„ºØ”ŸÚ¿¡<ßÔœKÕs]÷‰¦c_¼Sù•öÔÀò_Ïs·‚õyÜ?¯ºi #7K)ߎGvpk:°•;­¶€{áWðñ¥Ùñ3S•ÃQ…òmöÎ|H½Ã„ˆ¢+١˽£½µ¥¹ ;L°½€PÓ Ž3…¡õÀIàÿÆæä™âAALcUEY´¤ØéðŠ´~3KÊ7÷ ¯IŒÞªK”Êièyi‚âž8¬w}üª*’¢á9µjú+bã#ËCD8ŽÅm­•‰XiÀaGËÝáP >gý‡vñѶ¬MÌg!^Ø'Œ 9 æx„i0Fje9¦½Ÿ®“åj÷“ù•Ç}Z˘5c ÿù{¥^žãŠ:[£ƒQßâ-ÍG|a¥Q‘ï¯ô-Z¿F¼mëë[¶¸/©_/¡YJ¢eÉß‘«eÚO^«ÈG5ò[NËtä×.ýH§ÒªÊŠòËKK<6yt¸Ññ%5Ðè¨ Ù*J›"ÐWëö:@ùïÔ~‹Õ§AöóPVŽROB.ÂGôÕ„p‹’ñÙ!„tåéŸR/ðI8ÎŒ VN‘).§ž¼ôÍͰ(ïù탎-™ÀÑh£iX`£„^§”‹E„X>WœXH»’Oòe’rµ_¿¼ZÒ>Õ.ë>øÄš5çVzeí›Ú%y­[¨oÆFoè˜>O®~t_[Û¾G‡ÑçŽFc–ý‡€”úZ ?‰áð'½B(ÑË ¤\Š †îD$žÝˆa¨i6«/Ú ƒ¯3êi§øoýKß;¾~pú“ƒÐÖò ¥a ¥½ ø†R CÓ°ÚšK'`dYVÁÊ jZã/:èQQ‡N]4æsŸßJnNßp`ˆ›â,ÃàËÔàë$Wyé×ä{`9÷Àt.ïÛœýÔËäë|®dùÿo®d:ºð É©¦§ò¦òùúÁ¿þ•Ï™½[ÂDù„p$ YCž¹]VÂvCÌI –9Š S /Y`˜:ÿÕµÁåN¥Ì ºúnÛÖ£ûÂCÔ÷^øí‘ˆ\Ý*“‹€wâÙ㟠ÀÌVn7y‰¹>«2Ãƒà‡¯¦À陉tRpjÁzüc×lëÔ |\é4^êê¢ä„iê>؆v¾ ½g¶÷èÏ>ôÆ ¿; ›Ð"SÀ&Œ?{œþÂ,#åôÇäÞ«äfFsà,sJõÉvr/Î+ 6syä{"”·Ø˜Ôaœ="Ø\‰ç¥Ö¶8ù÷é`Dëÿþ<ÛI¤ëág1W„0Iäãw"Tš-$Ñ„Ö F£AIÔðÁ.úcÕôÖ}SÇàÃ/ièQÜføüuø>(/.t &qzçNßKMü’ıPÛY’;>µwz+¹÷Ó©?¡v¼Þ¶_„Æ5„¦ß¼5¬0Á‰›: ®#ßÛGÞªš¦ûDë/i¦¦Ð­øþÃ}!FÜš—åïF=Ò$Ì;Š×ÂÞ7µ(§Ir/õ'x/4œ7r§Ä(7C'±ó¥pÉOäå8%ˆ=[ÞÀ¢£·L‚_Ҕچ4òÌÎçíÒä.PEI­€hm©OVVÄË‚gžÉ ‘24Ñ :e*¿+‚a°ü^;žÍø„:ï0¡™U fyoøSéŒnø#ýÔÀý•[ž='«»á÷Ø'vlÔuut‰^‘×¶¥gãæñÂñÓ#AÖhï\]¾ó˜É‘Z;ýÚyãÏŒÉte}±øê%qÙø³c2y`ÉžÓ7S·:¹Û¼ú…{¤ÂÞÔÉÕ&‡ZJ;õ±¶€¾îð·u§,,/Ó 9’©%Æ9snˆPÃK²f~IdIsln§­H§Æy†\¼æ.d9sn‰´zòÞuñ¥Â’| \rm¨¿¾di¤~¸²€ ÈìS¯ß“!Õ-Jçnï½q8$•“ÒÕÍgM¥½U`óÀ–Æ?}NÈ»ú1ôѹt”ø˜vb’ÙóWtÚxŸEÚ]® N)6‡–\´ C#ÊdVƒ¦ˆH lvwlãÃSƒ7“뎜<².yò`ù²¶Š»Bnù†Þ?¹îÁ‰äº›NÝ´.¹amåHÒ¡PÉ( ÜÞáhíž•¾Ž‰šÑãu±ÒÆeÃc³nKdh{jª¦´´q´©¥/?Rk7Ù­:±EÍǃ­ÉÝLȇ>l(°©H”ŠƒO¹‹@©zÌiÙ-JSª B#»ÏîëüQè£gòð›‹( ¶H€ËûlDåÞÖP® WøzZkL ƒ«gé€o%­¨ª¿¹Ö ¶]ÚJ>:CPÿR´fp³Ýí©°Û+z¢&ûæµE—ZO‘Oòm<4ó!ÓËì ¢D âJ/ƹ6ç¤Ï•g˜7‚Çky½îÎ]V'¤RͰ,FçÐ…R™¿tYîjϪgþrúàkG[:îÿèñü®¾>W¬Õo K rKý–A{OO{îÊ;6µ›~ÿ…½ß8ÖördùÑþ±ÇwÕGFoî{bw=Y°ë»gûÚN~÷пç·'‘Ô5Ø­V}°%d_|ËïÚ´vªnÇã­Û{ŠÃ+N,Þ·´$>vÁ磦sñâbH´–&Ä"±2 ½špôÂ0!ª 暊GÅqaöBÏ6l×r'·>½å´3bÓÐQƒJ]L?üðô¥åÔpiäX5¾æ¨`ÞÈí¡³BMă/ÓúÓ-dš§&¥1…Ðˈê—WˆŠY%š%8²•èu Œºr!´ÉÓ ÷e#ÎÞ&¢Éap{–:ûu¸Xå}ëö¸g_Þ”¢F¬mÁ,Ë\6‰ õÃuïhÝÞØà“›Õ»_½­úTeçò˜™•yÝ·l‹N ì+‘Ê“w ­Ý¬Îí^îÜÞ–+ë¸Ø¡\åïm(a;¨7§ÏìÜÑ\CþäÒá‰;–ĨGÕÞ–h.‚-iBƒ-ƒ }ãñΰ9×ë²Ë~'4¿‰;éEeeö±¿ã•ðÁýt·ëm'78cA_IÌ™Iä(k/¬iYßd/î?Ø7t6‘#–) Ú5¦ W¾­lâüx¨¹ÌëöX‹Ú5à¶%›Ž ­% Å»r÷1}¾=__¿åŽžêò²:GE{Qh¸Õ_)´rQx"’8Ü>ñ•3Kq©Šòt"­0Ú}z­#sbKfþB…©w‰$æ·…úÞp¶Êyêpj=èt`u¨[H gJçkC‘PÞWéý{»êÖ6;Ã]c“c]áÉ «Z"Ú\yAý·wöÞ0 woï·×ºk @­ 7›jóýC‹‚Κ¾PýhcØëÖ×tæØr­meõ®¦•UõËCO¸.XZm°,† ^G:p®7s$ꈽ€FÂV|‚íáá#yBNy[dñ©~S.“Á-"º"8æ6§Þ¼Âa̰• bXž[kNQÄJRë//š] ™zÖp•‰P]¸®Èãvæçšu2 AiÖÁã,<Ø\íT4•…ç‡Ó‰¼·çøšòîÓ¯nÝpÏÊ’ÖÓoðÄœ¡|%åÓÉõ‘¥õ¡!Oë®­æêwHmuËk{ –¢>_³ñ\jëk·÷uÞôüØÁâ?‰&­Iå©,Ò‹û^=::uéÚ©³Kk&ÚŠÊÇNÜv^ÈÉô"µ–ƒÚv$Í‘/(f¼aFã3qHGg²Ú"žxÛÊ-P„'‹œ£ÀuQj!þhé“¥ˆ”öƒÓOžî. @£·Ô¤”ôPÿ ãR?ÝI—˜\¹Vu ±ÄLÀ=Ó$Ž0ÿFX …"e8é.5Œƒ<]Ð->_1Î2[ŠÌUw¤Q^Y2®ÇBŒ:îú¿úζ•Oß¹«¯¤þº‡¾þÎÖÛþþüš_çÆ£Rë^îìÆª‘‡Þ=?ݹ¡Štxÿ{Ð *[qÓÃ_^ËÞþ:ß¿ìóçö¦BÕS÷}åg;NüïË“ÕS÷~å稛®ŸùPœ‚6éb„³#œBBŽËÔK͈úò(“à’ÎÞ0'½zG[K“Cçv¸|N¯]fžÍžJ š?-{èË`‡iãan–lT‰~¾üÔʱۖù{îþÅÍÛžNö~sÛÚûÖ—çT ì¹wÅŠë!V+“çx*ƒÇG#gŽô®*¾¢øúþ¥g§’5ÃUÚ’¹L%¦ÿ5\Û{úk¾Â½ù£-ýýí§¾spü[Ïܱ¡ª6ܯ’­ÆáÇ>¸ýß¾ÛX¾Ë•V5v^øû“C·¬.ƒ$JØ?Ûf>m†þI5ñHR†2…Cã@† vÒ‹b–aÅÌ„¤Œ$d°›P†”´‹eyÆC9¢½RiãðöÕ*áâÙˆg_;­6ñ²¼u‰È³Ó$Ó€eûãoïºþ…´¯¡rU¶vŸÙÞ£#]šé_ƒÐÀîÖpwKÒ&V…¬}t¼ïÎ^¯0+p懯yçÞþ4Ø‘Ÿ‘h%k.LU©òùP^‘3¿…¶öï¡/­!¬ˆ­ÅÄû"â!ž]a]!>\Ìãà*BôÞFñ1yú÷§LVæŽ;o:Éýžµ›N‘?x‚{¬+e®Ü¤%åÓß!˦?R[`Ï€.K½Û]oAò~'4@ŽÁ¶X N‚^h>+,:k2Ùß$bèã «Å„ ŒY°sÜ˰5óÛ—nù1·½ öi'ø*¨»Öž6å0ö·<Ú¸‡›am°­rÐØ-–3Ê<9lâwPÉ©O~ÝhòyÐemðx¬ØOƒ.%óè« "‘ŒE0Á$œmRí Ó2×fy1EâvD¢–÷»4aƒÇ®ÑÈL~€±²>è‹÷?k¨8¨6O¥A>”ÿ ßmÔ:’n{¬ TDZÊŠMÏ‹a/ÿ²°ehkwäMMÿ}ÑD[P½á´1—ê‘Y‹¢¿P[ *JbÎwkº†\FM?J“jOC„Û­¶‚WëÇZÜÙ2ý åi«çj,p4RïQbõ¹ÝF£Û㳂6ô¾¡žƒcT urm²ª³Ä0€’IY1G‰P(Èn~¨ä?TuÉÒˆƒP5—¦!Py2¿ôÊ@ïZ ²ßVè‡Ç¡¤„SŸé×'±ô–rŽ6œLã.îÁã4uŽ9Á©W5«V6åR-sQM0Ä÷Y㨈GÝDÞ¥²ê•BOH§·‚uY¦‚ý Ù ßÞÛEÍÞ'YóÏÜ‚‡‚ów ð pT2iYÚ×Ûݾ¨3“»Έ;XFÍ••Æf¡§×"èžžˆGQ¸æä1Rb Ø NOÈþÒcwÉ •”˜“cÍîýÛ.j/T¬^T”_Ño\ÓR[dö®Wwž\ݼ¦k´z×s;òK 0ˆE´Þ†pÕºâÒfŸVŸàNYtŠ~b|÷ Ððµ®N¬ØtKAåÒ¸·gúî­k ,ÙT7u1à>º²çîëZ† ô…kpE ŠŠðÙ´fî%ö9&ŒÎ¦5#fœ÷Ñb=Agq,-„ky8d&‡‡à>ó¡%!~Zlwy½^HCx€/0 f'ÊP‚‰({E’"k }%ÕXR\âAfÈÖåj™j ¥çm”*ŽÉÚÐ4åI‡-¶,½û­ë M>OB¹üÜúòþsoî54û¼åÊ÷¬O¼ptŸ±Æi/SõŸZ·×/¯6Ö:1åÀéµq2p|{¿ÆîóÛUËïø·-÷rû€ÆQä³)Gïøú®/¼cÍ)´å¨Vë`ÿ©É&eNAa®jå}?â÷<±]&zè VדׯŽ²Ï´j”­'´pVj™‰ùf!#”r™r˜ëT¤B-W C«&4êá+pÛ·mœ_Û¹¢sÅèÈÀÒþ¾î®¶Ö¦»V0êôÿ¼Q½Ü¤n 8b¥ïwÀ±ù' ¿‰ó±#¶ÃóÍ>nTqãXrlí–viùpWcMuaE›ä\¾ƒ‘(½…AŸÛkô7hRñ™ÍÃuëæ˜†àÆñmE‘¢]ž=O×Pó‘ÃA;Í®PÄgO†óòX)γú5úh;†ˆ(Ï*E‹`éhäãyˆ°†™˜MÛ }ûnÈÄ”|é,Ÿbh!Ñ"è eŠó ó^Mˆzá°EЫh,¯2X¼ð‰R£eT¾aÇPmÿ—Á¥{Î>µâà+µè˜»veûš/\>Ðá+sZU÷s½¦€ëõì÷o}ãóÇVDÓ¶^ßçþãÔÚ ›«Ä1¯§NC;¥“zjmhåUBOZ'õ¯…¨mh2L­#’±RŸ·Ø-Òøm8uA‹ÐjLðŒ<Û´zÄk[Xð¥@W¸3¬•·Zõ¤G[×TÔ^íc‹·Áon–ÍÛ##ME “~?|(î]£TKCK }u ¥²âæ»^ öê¸oÝòƒÞõ׬“ðÛ÷jìÚàï¼í¹ É¥z}¾6)ÑÞüî«uÈå5Ï=W+—¡÷›ù}‰ú%QŽü„ÀÒ?!ðYýÕ?ç'¨2~‚ia?!F¸}—£0ßbâ½…ú å \¶IÓ~B5¸mô2>BŒ^©$jÛ8pl4òTÙû.ý‚.uðÂðÊ%I‘Z¡ÌñÕ—ÆVu%”à˜šÛc¯î Ûâ¡€YùKió¢Åµ[î1p‹©7¹e+ïß\Ó”èË,ùOûdã>¶iª£H¢Ë7È¡_ð{èH©·à|¶ c¼Ì/P]Ý/ Ò~caié F3³ysèžÐäël¾á0Yy÷5)er«Ì ÃÜpˆ{Di¦Þâœ?-ivTù€ªAÆ CS]*ñ¸œô®ø šb®`[ØúÏtoþ£öêªÝÜ=xꃿqPÿÍ ·–9݇õV*Îm!»Ðe÷”’ //èuj÷Š ô7’j•E§ XC®Ýp‘+õë€:ƒ¯lOt£WVeæ•ì÷¡ÒLž3¹½Þáp›¾És^|Ìm¦ªé·ˆ€Jjv@ktÚ• -#T@’9žU 9!“Or)üT =© ‚) í*%$b©dUl‰ì. “GV~¦›¬µØpÖÝìª'JB© &º õ”Ly­›@ º`Û–ñµ£#©®Î޶ÆúºÚxYi¤¤Ÿ³±P¥öó¡_Adhý°…,ľ ÛÜ¢¬]+ÙÜndtÇ=xÙhŠÕ¦OÄ–ê¬õ®XO<ÇéªZšXv$Й*ko2Ä KÅâ ^—"¥ùQwx,P=Þæ+¬©²5»CµöÍÛ6)6U,«w˜CmÑŠÞ*·B®”Än]öÄ*…Tª\a©.Ôç+éÍuò’Êfgas2®ÉÛÖT¿miXâ üE½å¶"_ŽÓ(•äÆºJC½ÕI^´ÙÛ¸d‹¹¸Ò™áGIjÄ¿—*÷ÊõZÕD®J}d‹k¸—ÄGyž :çs‚ú:cLLýœ ¦’R%´PÀPéLðù8­žˆÄS”¿éeÀ?ˆm»@ä+ðF˜(½ ¯ÇlŠ„<ÞŠÂ|“Ûì.¶Û%y~œ#cæ 8BІ¯$I_<œeÃ_!nZœ¶äqÔ*mÈ+É+ØñJ¬»=eìø« ëZÝëÏ™ù}Õʆ‰V÷S•µÆ¶b"¶¤ÔZ›4µúµrÛ¢ê"pÇ®oŸ[®r‡‚nyÓúS}{Þ87ªtú‹òÆÉ“KÆOX][½xû©u'-_ØãK­¿ ÛïfþÆ|̼L´Aû}ˆ%Uû¢ðN×ÍxÁh'”JY/TìÊdÎ+ä½PÂÉfMyÓ\S^@ÓüŸn«¿Êm“×ô7„;¦ûÁ¥íhoß±}Ó†‰±öí‚×ÑÕ‰¸—°×áuzØë ²ÙïBê0×épÏõ9è9>žsê¦he£·og»£v×ç7Ÿ*«¿w¸çÀÒ@hÝCÛ—Œ»ŠYTfÈ÷Ûê O½iM÷6ßõ–•ÝõSK#)[Øa‘Ê”"nµâÌÖPOgGï²rIÍxjqk“³©Ÿ}ÖÏ]ð›B‹Â4©¢Žè LŠÚ gzŽþú|M"QS±áî[ôÀÊ»[ɪtªêm®¿ñD{}}{Ù¦'¶ßxþÄ*Áù» ÎþÀQ o©[ìt“¤Óꉔœõ‘ü<‰”ÏÀ=MMÁuîG«zÍŬ |dÇ‘¡€øe‹‚(ijQ«”„Î;€GM ÊDŠ›-LÌ+›JJ¼v¨ vý˜7&(÷oSdË^U)Q+•yÅu‘Î뺋¶ç{sÕ¢$J±Î›—Ûqp$Úxð•í«NF(—TfÎ3×í|dõÉó¬„åú͹ ™ªØxߪý?9Ÿj¨Âïë€?“…pÕå'sHÌ÷ÐI C1<'ÛâàÏj®]EîÝ‹ÿ½ªÕÁék#\D\54ÁÀ‰ž"ñ¾$²ÈõHà大ðCº(Ìp !ëË­Ûíu¢íýÌÞ~]&%Cà;z[±Ig3+õ6ŸÑ[Ð _o)ÿ/g$)åô4ý;³¯P«²@÷»Ø¦KxÍþBm܃?ƒð’çÓ›©!Ñí8Ÿ+üL8D^ ýp€÷f&˜ïÂ|ø*ó=¢ÿ%©˜äS®¡·3#bb=Êå²Ûç½=Ú9ËϺ„׺Ð5œ„¦ZáX»Ýn—ÆgãÃ0B좇fÛ@\„2»ä²6,ž-<îœë‘ÆSßê:8?ëƒ0TºFöш›>[K‚"­Ú*1¤¤TŒÉ.ÁÌiîQÐ@$Œhåd– åˆ:8ÁÊHßùynx¸«À71 dAjR/¡]]ˆíçþw~ý€‘u7¬¬wÖ¹§Ü“W“| 1;ªµF¹jp1lãM°•Äák·Q€›_ÖÆtó2®ÔFÛemBÂÒ’a‚…&‹}Áw1Ãsž9¶fÅòÑeý‹Z[+¶¢çÊ®õÜ(õCIY' Ù–f”\àÂ*&X¨mX(ìàK3¢¨),lŠ¥­Bt´#Dv²åÀÜâ8¥î•˧’Š®Åz›Æ‡AêàõØUÚ¢{aÿô%»W×fKº€ˆ…$–°1‹Ÿ&Ec-…έ”Fc"3¸¿D$+ÂýÔÇ?{횣·×îÅä¸úÜ @‰õÑ‘\”b<µàK¢L¸´˜¢ÅcE³è((%BOgᨰsFep`ÅèÀØàX±?Oj»úóJécðy•Éò6ØgˆËšAC2‘~€Àq§çŽ{"‘hO´;x©^c¼Kh1¼ÿÒdï’$‰ˆ³–¡„…ý%“œtp=e?jÝDj°"±ejâºu×­\>8ž‡Sm b=Z‘­µ¡sá³k“U 8×ð2%ia¢M³Œu"-bç<×í,özð:r\íýà_?SH<ŽdÙŸuø¥© R*J§¹ R‘X$g dB$ÅÒNÓyâ' ÿyåqá.TKB3’Ô2+•Tm˜\³ZŸ–]p~AenoçÝ-µŸUR™±¤r#³¾T[C:y ‰ ;% •(-:" å=ôPîè`¡ãä"w€c]§¿ñ{ :¾ý­ço]ï:ýõÿá>9¶ƒÿ9üð×&¯ÿóOž¿¡~~èk“ûÿüö ‡Ñ/ܾã™n8óVÊr•*‹ØMêïÖÿ}ãLWí¶¾ñŸ»ïâ~›þòî ›ðÂÙõ‹Ž|é7€>9Ź鋿á¦Ás—ºÈ[4Nݨ„P¹–W» c"žÈŒ]²gÛ†”4-u¥bF,í*VFBñ+ÅâW.—¿›§&ÆÇÖΕÂòü«Ëø7Ì”fµá Iõ¡=›(y–Dæ·b„Ê9›iŽT›#‘Ï‘Ð20_Dç$+çWÄ•ºÒ•ðmd -K- àSÉÜŠÄþëwîØ¾írI¯¼ö»EgÖÚèIvtRò´Ô'ä,ÍÊéä¶°P@)šÝÃÃC½=ˆ¨(#Äÿ5žbú²úöm¾'õÛ7’ zŽTÇ €ó†­K繨&pÙ(à\˜'üeÐÜ¢s¤¿dÄf ê¯TWîJWFw“‹¤òT¶É …aËæuh†e+¥ãšc`¼Y}ðXR¶c)!å’Þ¥ª d"Z$Ã:FuŒN=Q–Ž“]…Á"–Qö²éuÍÊaÒÔš*•4 lݲ¾Ö•¥@FR¶ k#R§X@¢ª‡” É#¡)Z‚á ê\ÂÐ]pþ°Á?³³´_ctêœA(Ô¤æk 5¼}Ò=qzjfñ›#èìÿŒœÓi%ÃT”Ø÷€=Š|-wŸí§O~â-¼øïßñX7\}d9£è³K¿ÆC5z‹lºØl'¿nñjLêí«ÞY—- õEÆu,+•^K–0EYóäŤü¦¤\‚•vz¢”g‹‰pæÊáÌU¥•¸Î(Qæiñ¸‚æUÅõº2õÐd4#KÁ(.³RI'oÜxÃÞ=»w-l¨¯-"LCÖ;By×¼LŒŒ8¡dbV,Ëj Xƈ»Ò¦Þ‚E2?»Yü‚̘ r¼é¬yÛ@L7·$å“å$)^ø02cЮ"˜a¹ˆ¤¥@"F‚\,£à#eh ˨¹X²`yƒ)BF̯Jš›îß¹}lÍà@_wãTÓ”Ûë†+¥x¾â p qÒÙ]zê áXàÙÞTs7lGVQ~óNu}†r3ÿñRuâÖöòµíÅMS§µîqzå¿ðë‚4ÄkìÞäïަ¼çsr(ÅôKbR«2W]('ð¶êÔÉ¥ÚúÁqºXoX‹;Ê töb«¯#a¿ö"ê¿ëÇÛ…VÞ¶òö_œn^Üuðç÷ kçKöÚҪՃݾ·eÊëxBãQ‹·¹©µ¨¬¼*Vê›Æ›ìÜ©ä`™)°þ©`i“â)»@ÉŸaNaF±B™Îà6‘Œê¢Ôà“_ø{)õ’×Uìu¡£’<Ýèä )°ºé3ô‡ºôìÆÚÈÀΆƒ¯øm·o{x[ÍMž­M•½e–ÂÖýážJyO s,á[/,îX›ð¶%ì(vù'îaBË} ®\-Î}•^·N;¢,v)LJ#CáÍŠ¥ÜKb)þŒ)FfþÄe¶:ÂE Ûâq>|äMˆ) hjðB±µ)Ñ!+~»W‘9¾å¿Zq|ÊP"™åžŽ ”y‡ Õ"ª”‚Žw_×ër¸Ý^¯)à9aòì³ xç ‹°åŠùyK¾S±b_]byƒ;¶l_Kby£ûˆÁÍ+j-+X\åm‰’{e9¦=Ÿžåê®gX\xT(<Úè>lpGò‹ZÊ ÎH¾·5VHwŽÞ>Væmߨ8zv,æíØÔn ›7v®è-h˜m2í'/ÊU`ßò;ÖÂb—Ÿç‹5ãb¡†b}Aã´žã^Sï"Vœ 8ûP èFý«Aò ?W¯EXS#£öç@ *)± dñ¼¡x,É(ÔZé8t^Y&ÔÚŠ­–€]§³,Öb›–{‘”ú{úR‘Hª¯Ç?ý¿ôs–€M§³á«ðoñÌ qµ†|FäÇñzøPà.âºxu@Iq¢t”ßíþd†SE6’ 0_«QÉô1Vœ»ê9j„ü²p/øÞën|¯õIe) ‘FIQÂ1yàçU"rïå:IFTHŒåÎH ‚ÙË8þ_RŒ°Üy('Ëqü¶ã&øNJ‘ ·ã&üN爕°¶d>´ >55ŠðSD7ÎK’Úš5´`'P¥2{˜õh_~t€§Á!d%€{ÈãÄ/ðÝ$ÔÄ¡¤Úç"YPH™ÅLR$%0-y +cE² è­2’• ‰q¾)‚¤F$x¡þÄT^øELmW¬ M%5<ñªÆ€ÜÃ!Çñ¢¶u;ßÖb¾­{q[ï"ßÚ*†m=TU—“"‚6ŽñxMu²€Ï<„qdÐ@óKe3ÍtŠ©åúË gƒ–…\ÖP_—¬­‰•ù¼¹Ð^••ÊKY®Ð¯ ˆ•Ù‚ÚÚÀ·õ ¡_ßÄm½›ÐQ‡A8iêrEÉcÐU‡Ý+ÁÝËm.#DR‰T$A$P2©\†a#¤T2<Ûi„Bz•,™Ý×¼¥MWFW …(†¯|›Ùª(ÕDX Ÿéùl¯°lÖPW̯¯Ã°Ÿ¡øüžÃÞÕ¹¨µ¥9Y ÙSŽTz¨ì;±Œ{œü3”+¢e¿ÁyIê¼Õf ÇÒçôñ¦m:£×í(ÎE»”L&£ ] ¢b%“AƒFLÔ<ÁCþ¬pQÿŠXý†Åþ’¦¾öëÜÊ’¡¦% ‹¯¼0¶$á–™%/ë°<š•8ßω¸M®æ5µ‹§–$¢¥Ž³øëÊËêzžª"Cž=Ï@½Ÿ-¾ 8xgþ$þë4&š N›$®'¾ÌO‡Œ¢’±ÌWU €T›igu[ðªU°¢BõD½Ð²çõ[l¾~›W“X¸"ýïÙµuóÄØèˆ ë¼P×)¯¦ë ù3(;¬]W¿Lþ¤bùÞºòÑWldo3ÒrG VËMk7Os¬€Ü)Í…Êðv)T†ôtÅò=|ñe{›Ë—5ºÃbyÞ–Ò‚Î OK¬€ò$`¼€ ÄáýøÍeù¸½x9Ö•H "]¹±%­+C~}¶¶¼~ù¨àdãò;Çù‚‚¶\Þue×U¯Â>o±›yŽI€Ÿ rLÚ„XT”hÉI3{”+SIɬ‘MA#{ëÿ€ßsyâï 0é}”™üæ0žcUp޵%["8ŸÐbRŒ†ž•1ÂióÎðæ:Z%iôz›j«½UÞJ¯Í€ÆZfÄà¤ìÅŸg^›Ä8#&2PóàIÊcv_|dw}94Kb#»Ê¡-S™ë(‰¯l󣿱Umþ/jZJ¦+>kЈü´Tªóú]…6/]ܲYÌß¶6ÑrýhÜß¾¶*Ø[í,Y¼¾*ØWã,éZ_w l‹z‹“J±úfÕ”D÷çšr±¬^AòwX‡ÂÏð…ËäÒÒiù~;9Hí'„ëÀϰNÛ5ë³H¥ˆtL­ÐM­Zà±›¸óFÐ/áWàzÒÌ·¼©Ná’ ³\2“š6††U³—\ëµZ2g]¹´c¿mé²¹„ˆ”àñôÃÓ?á?˜‡ÞtÈ£6ƒÕS»b·žš u/–ëÒœfË©]{€…¤ñ1Ë]FÕl }Òøƒ4þãÿØ]WSYV[j¶ŠìE ­6¹­mk´<“ ô" 8/ž#ãŠi×øÜð„m­-ÍUñXm©¨à£ôË0ä„ÉL`ü9ø:*S$­Ì(< 6krLÝñ`¤™¬¹UL w1½¤–µ° B7¦Œ¬&ëx©›1 9ˆDL«"ÖP± A¥¹Ái†3F‹d’º-ÒujލÇ«BMö<«W¼a|Õ¶ÕÛúzÛ[k«£áb—Í¢¢&š#ºLáàß+…C¨‘J‡^3!£Û%9®‚(dœ‹¡XCQ©5—lqÒ:{œ¥â$Y…>'z¹‘Åa ’¯[}C7L4Ön8:°õ‘Ã]±Þ‰VŸ5Ý^\Ìéð¹öÞýƒ¥“Çß½ñÐW®ïkÛ÷ÀV;>OÇEŠa©gÿ§÷¬¹z]EûEÏì;°¦AŸþïc¢MOÚ‡÷Ý2tãïÝÐ{Õóçìý×»¦ ŸÜTaëJ8ïï ;ع¸’®?ÀˆPß þEÛ¼ šy—aýNÑ^qXšZR¨>+mW¢>à'8ÕVß“è)+õ×ê,&ì#>šâ™ Ø~'Ì¡%ÑyÈ1ƒŽð&=ŒŘSð0j‡4*Bó`’9T¥]yæ-:–*FˆfŽj«*Êã±h(R=«V¿¹@ÎÖCó±ËѳYÂL«’Û‡ô÷Ëî[J&–d*ì‡òÆ‹ \šéÿÞ¡­rõ]|I‘Mk|fË·íªHøÊ]êrwÿ%X}É —Kµë%ø.´Ë'ŠìZãôKX³9ý>¾7½£Ä‡W¦ zð½¾H·v» ñúb_z¾7äN?ºÌåJ ^\Œ[Ù^tÏñH,§{ÑMOÜN tSŽå¾b˜~ õ$iLO£ ñ¼rË’Å8šŽM1LÃORü²[>ä˜'? ó3£qyb…ŽåLR.Ž&'<½°25íüiÉ2Ñeªž>BÒ–é—ømkÚëÇa]úgƒO㋹?Ü^~wúý-é_aÛÓG`ŒOžR<õµcá­èìNwj© ÐIõGËT¶´“FˆÏd*3³K*ŽÅ¼{ÖÁÜ(HóZùæÍkeX(¯,ˆaÍx¶Æ|å@,.pýžbW‘Ó]à¦y­ì³y­v7–rÒZ±ô œ’ÖêèµUxÍÞÀtûÍ£W?³çÑÖÏí½ñÖ'‹Ò!_þÙu7=Êý_÷¥§UziëúÛ'“iwIzúú‹ÒÓxBä0…åæ™¿ œp-A㩵p¸jX&ê'ÎQfu X/©U4«« "½ZÃ"ÌG-JYìÎR)²•´k–.IQ¦€Åž…áR;I“îõJQÉôfÀʃ‡ËÆã’F“µ‰dpûS¾CÿàÈÿš¬±8M†hëx磬')‰xÓ¿$‚߉Ǚ^ÎW¯>Ü7°³·Òf³Ô†7Ÿwiçg/á?¯{„×~†ÛFã!ޝ®2›%W^¬7»ü]á:³{ë]“u±ª˜¥¶ª¿Þ=ø±oÙÒ½Tæ{Ã4_ÍhÈ…²Ym”QÚH©%€SÖåQ‰, ‰È$äLÏŽ€´ã ”C@² ôX)93¡L@$™€@ rßÖ….mÿ×Vh«Üý“ç»lZÓãÃ_²©Âá#+5eîþí¯lês¹ÔýŸµùØ3t3%0iÛ—†¿„C韗zö_ÁbòF’ïv»‹~SìMÿƒîÿ‚ïR²ñ.œ3.žåèAGRî¥n8ü œýF Ý\”\ú["{Í…æ¥,ší,18kšaöDÍž0ÕÕU`%&½Ó*3PTÂÈÍTàáìÊNéÝk”kãp&MÁ}Û﯉;}º`b°¡¿~ÇpMÝä][c[b#W–VªM½§¼­|yb×êzܺ÷žñØ6|kQYk`YËX“;}ÔMø:“CõEVíP[cqq8(á¬%‰ÞÚö 7µ ß~]³¤ª]gp»Åô޲º%ñÞ+¶·‹ß#Ežê¨Ï¾[&:ñ´»"4ⵂ¯ªàî™8w¿•ÀÊþ&e«0®`9ס‡EY"‚*–ùY—à¸xX5Ûxe‹¾8œ¡’4 Z6C½º§ÉÞJ,ÙHÅglÑvö-ž¡1ä…‚ =j.d;8ï¶Q”AÑl\&ËÊC{ãK.zæÜèD´{*R©µôNo™ÿòÚîÚ ¡Pï÷¯ZUžÜ62pyï9=A¦}òúUçõ¶Qéõ74­ñ½ÆìtˆÓýœËS`­ Õú-ÜkdãÞ›œä)UǞɼ}œý_ñK©/mªI81 Ç9'bæø3ÎSúÆp"p ÒLäìfÀ{áp8@)\6ÓeóñÌ'SIr)J‰Zvœex›äÅcMóÔÝGîôÖi:½ÕZIÆ]b2âEâs¤6Z¶´5“˜éÔͺ¶uZÛí÷ ßyî’‹vF%ÝöÙoºdÊ\|ñõƒ†ëvÖ¯¢¤ÅäÞ\f¦ùÍÎD¥ÈªÄÓ<º™ kØ@ó·Ä°F«ÅŒkÓhUZŠ&œÖjÔZjØ ¤«Ð˜8›Hy6•qC²®¦<¡ÊtaÍ&=KM3ß&óR‹ÎYpò_éÏñ*žøìé_p– ímøOæSßÖ7kU×h®1^ï}µ¨iÜzóÆý×»»No1;üÕ¡©}F¯&‰#sqá ƒF¶ª+øÍîR¶ò»"Ù•§ê÷ ˜÷oa­m¨x†æTC† ˜?É%cö ßí*,°9Šhž¥<ú¯¤µÄsÓZV8>g»O|Ðîw½þÒᲇ†vâsw_fK?à àMΚÁäar'î¸IÔ o(}ùäKk·½„ õ•KJ¬ŒÎÕÒ¸¾ÿŸ¥s¯m¡t®ïGÚJ÷Àä*=7ü•‰®:·bË·Q:7ø/v }‡†>ÎĤ¿½1üÃ\:$ms¹d:|¦èÜõ@ç@vpS9õáˆR»®bÕ¥Q=œ¡¦Yá¨èÿ*µcŒ©hD?ÐS#Ó#/#J+*ÍbAL³:SZaøÿ­(’ç?ów8cOnQtLü£ÿ†(¦6úˆ|tLT+ö¡¨ÚýÏâ1ÕS9qïc5¤êµLÄ;“àF3ie¤63ó_ÐGÜ·åöôtœý} lV˶»ÈíÜ+Ï×CÛyüoˆbÕÀªºÿ Vml Ù°¢á V5ëåûóÐÏûòxÐ~žÁùì0˜Ó »‹ .§KÆ`fŸí³,ðíY}àÔ‹¥ÖP¿Úy%œ=;]☕3:–ÈrFUE€ÉÑY¶ó…ú»Ñ&î~îWHˬJhfž†Äc‰ÐX-–ËS‹dqT²Éé?dYÚJŽVáSúÙÊ*ÓÏ™ðŠ*îWéK¦/ÁWÅWÈpr÷q7qˆÉžðŒ‚ügþ<³NÞïRÑã*YxæÞܘÛ (ñI\Äí᪙ÿÁÆÔ¸M€JVÆ[å'¼T ‚È3,æ‹f ‹Ž©¸H5º0‚$ÒÕ.M‹FcúEBîdq²ÀaÔkTÈ…],¬-SµÏ†¯‰ÊoÜß“W «K½*Mpª>>²rE¸¨ªÔ§2„¦ËFVöáÙwÃôýdTÞ‘è,õîx©'èÑÙœšUåð”~Mo•¿ ÑoµSUåÞ—îÕa´“»’û ²¢u2®òÁ´q5e{¹uªŒÅû5¶¡JF/K]¶p‰±”ÞjÉò1ªAIÖR):ÃÅP•ÉÓøÑ#‡œ0ýi·™Ò×_~äòwL¸x2}™ùéO/?~ôí·‡ñ݃îàÖr'‰ÇuŽÅ­íKÈ­§ Ý#òkz§µÔYæÇ[¬×›ñÞ{ßÔD­õ­{i8S¹w˜QQÊ©Wñ (¥@Õ›=ô,YŸ„f©Y4+À89aMï ”Úü&ÕDâpsI+æ>•þF“Óéƒxðý‰¿ã6nmú[鯌ã–ôWÇñœXŸþ®fÆ»ÑÀ™oÁ“øÖ^yÌE³IÄ(‚ÍÜ=©×ðB/ð+Ì>“eL6ÕË„Ïæò>akއlÁò¢²F{¨²°¼~¥-Xð¡,sŇ\•![K™«2hkž~XùP´6ɰ/Á¯òå kø…‡ K9u¶”P«•c7<g–hJ[ù2ÉwRÅ ÌÎ8“S6) ºU2D™y™µ.¥ým° üBýXq™`Qi®@A™§¡ÌZº©EOl†r[eÊ4š%£Íe-õ4U¸Ã]ÛÛ ð¦ìïu‘’Tç«Õ¡pg’SUø$µÁbßTDJâÃíQÜ¢µnV—•[U:½ðš¦ T®Y»´”,ÓÚð’`P÷ ¡o„ /IŠÛfî$F2{^C³^TYM°“Á€‹^cd.ÁdÁ‰£+ÃÌÄó[bÑh°P°Á̶ó)£[ÃòÄeõþM4UQéÙÙa"VC™3Òè ˆZµÁç]Z.²iŒ¢;¶¹¦i,-¯ˆuö®Ä`C_eËÎ n…ÖvžÊWW‰UúEÎ`Ôiã%A¾[gŽ)-îRm–Ïç4Œ¶…¼ŠêP]ª: ¬¬ÈnàèEïÍÜì}\Ks¢®º’†°¤÷q&] ä"ÛLÇÅÈêi QÔ+-ç9»%¤š¯U?UýTz“hx–—ÁÄ-fÁD½W$ÜvK»¼õ•.ovëSôE4Fº3¹¸I?¬±9?Ÿôl³zÜ…»Õis²<ëŠþ2 ÅXÑþÑ\ÜΚdPJֲ˿xsá3û÷¶J-–›.hôL4ÿ쪂W ö¯·¥šðß¾51®mÚ<õWüâ¾}û>‹kœ 3µ°'íh]jd'\UIþ§"8lR{[sScUÐÎ|ÕÇœ¬äM’#‘åÄ¡w}õAW—¹eh{S禢JA§Ñ˜ £¾eãªá°˜Kxü3ŽXÍÏòKÛƒ^»V…Š’]-cÍ^Ò-v`ž¤ð¿¶n[®+«T&«idÀ¥±™|°Ù–‘5V§•ªëb½Û[Ì>¥Åƒ37áµ\ÒâAZé¡™×KÉÚ€ÍüræJô[®PÑ]pø33ï±÷ÅFìÎß…Zeû ;x5ZhàW§/ÍËa2QlÖ'fü ý‰{—õϰïÿ†ÖÁ¾Ç©£ç¬Ñ¨7˜±ÝÆmDÿPêÀ3Ôù:Ú u('9[>’)-Ü..–±¹Ã3Œÿnô<ÌÝMS~"Y£§˜›i‚ÁX†ý¼0yRÉ×)LâO ‡¡Þ†”Öd4ÐøvrîR`~~—¼£‚¼)Ôx¯b›Ò#'mãåM›çýØ ²ßEÖnçÛÜò†Ò÷·Ù˜¡{¡ïÕ)]((†±*¹Y«™¨’zæñ¥dìbúWÖ¬'SdöíØó…ÁPi0kGögèóJŸf}ÞÇ`dEÖb­’ ²Ì,MI¦ËïÎ)¿äèÍ–á¡«H°”™¬Á~ܧ—û¡ÏPô“èvè§”„`ê_B"l |`¹Ž[†ê •Wàà6^¶¯ôÚùzÚ9ö¼bú–ÉäèšÇŠ;˜ŠeÊ‘ †©œ1ëˆãÉ”ËZÌå—b9Uó H,ó`ÖžŽ nêSSSMMêÕ-5-¥1U jÖ¤æ_ó—x,có¿Ÿ­syª”gùUè"ƒÇ,b bF§t-yÔRRÞ³sß W ·³5€g8 ß@{à,lI :‘ç@–ñ»œ †PY†F6[¢@–q;@–‘1¥åôD.f—8¯œ3v$Xöü^JþA¯aýÃ3ôÿÚýLÙ»üÀ—K[K °Zm<Ó/f·$Oø¥ 㘗mÎhdÈ…åX—AEJÁ|·bóÍžÉd«ðM6&x†âßD#0¦+RÆjf÷ º1ãùó®t²…c sYV$'—É42!C¦M¬/?°ì娨óTˆÈÚÐm¼rŸ·­ûÚ ã.>\¼´?ë$©pñ7 Ýí½¬œÛÛX{ßfí<¡U,êè R‰’yœÒÑMyÜ4Pÿ~CU®U»\©X¶¬´{Ñ¢c)ãÜËyÎ1=¡Œé6¦ï ½0¦[3ÑAÙ02Œsà û2˦ã3t0ÀF±xÉT,¯P.Š2¢eF"¥ÝÚ¼K!ëFë'•ýØÈÆú]´Æz$¥¡öoŒÛ–qDI†éžeµ÷Ær†NÀ… ËÍbÔ°ŒÉ‹1è³%ÇRæÖ–d}MU†G7+>˜ø¾«¬áÃl\¯££0®ýrôÝpæÜ,lÇŽr<¯ÜÞ™ç¥Aø#ѳ$ÁYJÙí6oqQ¡Óa+°È´rÿ„¦¸ä%e¦ÝýÐß tJ«ÄR]Å8`Ùš<1÷Þp–íÕÓp½C”eÑQ»A']5*­ZãBô ÌÖ…±§Ú[€¦7r6šÝdÎ-!ï’ñ:†sá™r¨Àôçù!|’ü?"ʺ6ú \*ú<+cÉ”™y&Bjð¯2|,ÿòÌ_f"è’y¹Ô¹¬d†…düóÔÅŠž™Û„q=“¾ Ì<ËÆõ,å¢f^‚qg\Ôg…•¢õ¾6ãEksê}}æ«Poÿ¢H¶RÏDðo°ŸÖQËz7ÛÉg¨W¢w£7F;fUoˆtåkÞ³¸žP³OÙ(×Ì»Ðwg*ufʵˆn–6Ié!%¤›¶/Ñö_›9 íw¤Úç¥LóÒ› Qbë³dÚ¶§_ÄÄ7Ò¶í´íoÎüÚ®MUÍ¡0 h•ÌüÒY’@÷á?®¼øOY¸úÖÌ;Ð^c*±(E ¼Ñ ¢ÝÁ¤;‡,Ðù@›=2¬Ð6¿=óhóyT!˜Kf5+4˜6?¬‘³£ºË墰PÉ@9æ!°pšHyÎùúƒ‰]²¥DI>ÕXÍŠO` oÃü9{ñÝ™_Á–¦–ÌRƒÓQ;R#Z3 ©FÉ«ôª¥ó uº/%Ðþ°<Çݺíï—#‡ÃÖRF1û¿…ÕìýuÆ Çsv¾ß›ùŒçÓs°º#«/¨Þ@*¤QiF[‹Ò»ilùhz@ꃊvD¥é^±ÃZ>Mž ¿v2œ ÏLoð(y=%cä’yP±ì±"£W~ˆa[#ÎõŽ(¿0Ÿ[ô,õÀ d˜ÿ3›Ë^ê` ‘ þoÞ0±~/æ¾C’|Œõ ÏL¾7C¿‹éîâ“ ^¦•ðÌt¥\«ÛνÅdëŒ@¹9‡•PDKƒøpˆKȱ4à™Êý·Á¾-J±¨ yý\©ÏLö_õ¤XP瀑ÕüìÎF€b]1S÷Ñ(´Ó@> ïÎgíÀ3Õ#ÌüÖ©Uæ -Ô‰ŸC£YÏ}Â䨌†¾Td,äÄŠ¢AÞƒ2×spžiÛôYްqJ=¯„‘C P•ub\CÛ—nŠn ½úÙËaú~U³(kM`þeÜ„µ}ÏÌ_ íU)Íl,yVæ2IVgp0²p6PK–â Ñr€< s¶@X'ŸÒŸƒ­Ó½3¿†þöœ¯…öéÊFaQäPšÍŠÞLÛå?=XË\‰tÞˆ-€×¦aÞ% n@lÞŸ˜yÆqÁ<ú›Ť€ç ,Üþ¥³xÁ)œþúLŠ\ͬÍ>ò9 '”Ži…_| ÎËÕhý\½N@Qµ(>Y²BH-µ;cQ|§iv Gat]8X—¬z‡Ú½A¿z¥_ë÷¾™C¿ýYÝN€%$ÈzÊbÃÂç»"Îí(’§“x ÖýeÝcëþITëÞ}BVïäDzßÍ@‰êxhž_ó3–Òä*(o0fQ`¬€ÁاØY<V÷Ca«8WUÃ4£Y7Há¬U:‹ktØœ‡`ÎeÊx†ØœïŸùŒgå Y¥#§@Öìd7Lqúá2K¼ ÒgìùPV†.åôdÿYçÂé™Îgpàÿ çL}¢I+¹Nøkž™Ng´xAÎÜi~:OþŸÓëì#m¤W0*g§éuš`\À%Ïåº3¹G(ê¥v²t¤²K#£|ÞuÁÞ]¥ì]ã!¾…z Ý­?f¸oyݹÌ7ÉhÆËÑ@ã¹ÎÌ£(ç²é´ÿ ¡ÿÇ”ór!ëÿÛ@Ûž@wæsêsytÙ’rÞhˆráF>ƒ¬sxôùJ.ÀÎ皟GG«a¼·+ã]ÍÆû´Æ{«Ì¥gt; û9’ü.^òŸÓío@®VövëwQ/ƒåÓt;±Ó˜ùÙh ”¡GC”¹7ª˜üBü^Áù5;lÏo‚qPÖð&6®×ÑŒë\ya¢ìDärõ³’ÌÞrÖNžyF.$ä–[T¯³hÃCÊ­d<Õ÷PŒå9@„…d]ЛQáüg]iß╌RFÉž8+oÈ9-qÿ´±˜n}–$ñ á»lþô™ê†Ê€6æê†R •ö _ceè3Õ ­üž£B[­S¸…ÊQðà÷k(Ž{}0ýJù3ÖÇLm¬ÄÁØnÕª1 ;‹à¢>/ý3ÂÃÂÇ‘U£^´,ÕY'ˆ/óâ:ÚÛœ@„S½5zOÚé¡@˜…"ÉòF´´ºª²"ôº iª,ÝrJÔ„¥'’8h–SY)æÊõæh};®µ×rmËJœ¤¶<8ŒdrøÒ m“=\ëÑ5¬O;¯þÆÕø™ÇªkÕ^“­joˆ‹û¼›lþ౫•‚‘¶q}*¯njøÜþø›ï_Óá×&νæ‘=wâ86ÿndº?ý|(޹ïÚìe£—`ù´|ïäµÉg~øû_¿12rp”V8ïÚGvÝ•þqúÿù½™@T?ó}á?…Ki äB^DQG•Ì^±µ³È”_O5-IPBàR×Ùê.⵨½­Åë)-ÈaÑHHÔðu5ñ05µ¥D /‰ªW)‚.5äÔ H;®ÒA¯&¼FàÇè“4xwíÀÀµÏMîxþÚAü• i?¾p´oª½¡ k¯¹½éœµ£[Ó¿…7éëFû¶§o¬¹¾)ý÷Ϭ¯½­iïÚÑmmm»nºeWkë®[††nÝÕFž€¦ ÉlÓ\ÓgÆknƒfÖnƒfTÒ×BékY3ô mNHðaÖuÍõÍéfÚ”ÜôîÖÖÝ·PCÑ3ïõgSuKR­-Åæ*Žtv¸Š€6´·y=¼€Q‚ùdmM¼4$¼¾Žîp8¤‚]Pö¸D¥!‚ö Ó}R<ØÛȼeçÙÓBº§óì§îôý´ÖrAìÄѰSˆ öp=–pÒ*qIš/G1|v†ë;NbøÖ*‘ ±„Ç“ ñ›ä¿[&¬íxãhz&‰Óµé±¥Ýø¾šô¤÷ùµÿ™Ø:akKz“d×à§¼ÓÄ¿Jo«ÅŸð€·äÉÜ–à/y7íb•}ϯ};A›N?È*“Zü”oúPy{RšnÇ›FÓé$ž©Ixe_‘k)«*Ü ”ПòH„–5•CCTªfôd©Ål‘ ±1¿ûïÅG±3üÄÓOìO+ý psúǸôÃýÜ3¸÷£·é™6ýi4ÙŸ~—5Éáé÷“ ÈyãÔ @Ê«’hˆÍyµæ7ZÏA«_xzZþŽ6:ÝH^ýp?nÁܿ洩B¾T1Áó¶hÎk‘¶÷Ê çB{_¥Í:Ì]ûá~þ #¿ævs׈͌ÖÀ34–€qÏc­àDK#E‰â.Ï_ý!<&Ë»ü5@kâ§Ú lëó=Í\Ûžf§ØÛ29Òœ‰HI1—Ò TÉB,œ"ßLÇ„OsÈm ŸfmoÅnN”ÆOk&¿ „gîJ?@TÜ÷á,¯¡9¶²R‘K u­â1Y‡X4…ïbµ rßRÖ•Ýä<ä.ä²&ÂÀð©ìqº®µRËä"ŒÆYƒÉWê¯?fúD«TÚ³s™§Òi%’Ña^ºTݺÏH|øóéÁ#iSÓD_\£W«@À9Âìá[MDx9Òþ©KU«@*Ì ŒùH(±‘ؘ•ãî"‹)×U"3&šÒxnÌů&žHÜmº«î‰ºCF|Ì®.´‡]FG¸²Ðq™2Ç!﹪Ãv³'jwW‡fOŒÂñ*ÛaôÖ.kèí„&áb~<N‘éŠí%Qæ%û~ìÙ(‘zÐøàÊ(·}¦ÿÅ?³ò¥¡GÈI²gúÎs¦÷‘[Ó­áos?€þŒçteTºš·Áõ‰ÏÔOê'’''I?“^¹%ÝŽ_–qF }%>ç¦m‘óÕ`¸è_!›¯®@ÀÊγi“;œ‰}å¥çñÂ'œ^bìš³Ž ÊÏÒÏI[Gƒ“¯ûÞSOÜÿb¡*Ö³³;Ö]jW9µºÊ@°&`„›?|ïû4}ÙLÓ×9ãmQf›¸9ýˆ°U8th9ÄŸ“µsÎJ,‰!¬SD£µaN£¥m;\2ï ¯U™×zx­Î¼Î{£Ëy3¦Ì¿˜|š­`Šf.f‘® 4ÁšuU¯3r 7bP޳wb%ËÔ&¬=SŸ2¤ ÜU;(¯:!$©x•ÄOe›Ók5Z½&Ûl&S|n«¬Vë;uFB›Fs>½M=ÖA£º³iÔˆçk“†µ-nmAh EßÒŽ–å­Ëa›š ¬±hØ4ÄýóäÝËÏC@-dgm/#Ì6– çeƒOµQn•tzª>[ÇÇÇ—Š/;z|ïº ãZ@„‰u—Wô”¸U½6Úlޤ7¬¾¶­9º³kà’ÑÊsð¤ qsn†¿‡Zº“wo¾òß?=Þšï<º¾Öléµë ;šN™ ôà·Ruí<}îÍWé±EößiN?Â}ÀýðÖº!eéÄÀ!bªãF¤‹,æT@¶cˆÄ‘)96šÙà½öN8m¼¸ –ÜLyŸªLYzÎ`—毃f«Œ¥Œímmía,XQP;âáìêò „yI´eÓgËöó§¥p䨥,’‡ªwµm¸sG2бé¢;Wê5Ý›~ï |ôÊÁ]}u…Vc™%hvœ¾]ÆÚMç=¼¿'f°DÛ ¬ÖÆ]wmè»ãšóWUÆ;†"í;W·[UfÕÕ7Ä*cæz³×ðlî&p£ñöuÅþP¬Ôi ûl2ÞZëü÷Ÿ0ùçR޵À¶QíFbÖ¨„ŽÌÑ%4ÆÀ·$P'Sª®$^ÈjÿœÔ'OYD5‘uˆúfj Ñ~ZU5ðTô]¤ 4ÛÂXÊIswu¶4Ñ Be¥_±Ëa hM³û‘ÈÊ›“éØCš±Ä2œg¢5{±8Ï‘OVo¯¼a²ù’]%!SÃ9îÜÿ`m…Ö¬×W·§6/ñùZ×6Ò¼ãN_ËšÄÒÍå°M%‹íô½²šŒá®m­›¯ö™Êë]‰‰ÁÊ%µ)“ÚérÆ}Å5!ûùÛVšâž°ÇÔf1~:?‘'FfþÎ'€FµRÿ:àcãJÃWQ %h”ƒ‹³¡5Ëã>ÕlСVÜ*Í×L2ï3joijA6™xÜŽýÔçŒÒÙéŸ\ó±MU«÷uxSž;4œ¿÷é55ºã‘ޱÚöëê +ÚÃ=^^XÄiîð¦¼Õ+¼C7<¯mÞuû誻¯u ˆӳkX½l×µ+–l딯»~}ÃÄ`jx—Ç9" ¶® ç·Ÿûâ5}ìœï"T ó,©¨&U)SMÄ‹˜ȆúËÆ×C(èwBig™ß,YX ¿"‹H§5ˆrrt`%]=^Rù™ ±yä‚!ëXÒcþÂ¥Æc*gaóîäÊú‘ÕçâkÒq?¶Õ7N ·˜¬þô-~¼Ìl¸Ð—ì-í膽Ø0óWn ÎH)ÕЖb,Ñ »§§|f T†L2Qb¡`Œ_~¦çYHÌGÖ\ËóOU÷\YâI®¬m\Q2Û4±eÿº¥ï²ñÚ¥—½p`ËÕ³iž·wíí1êïmÚsßÖK²<´næïä$÷"‹×ÜœÉÒœ1¼ë̘HÚ)ça½ÚË~ËnõBÀäŸ1—ÄÉeçÝÒ›IBÓ0±¢üÉÓÒÉ$îxóÖe™d1ðwpžÜ/›´Üç‘¥ž7d²/{ØMžþtJæN££Üo3NÓcÏ9Í&`bNº©£ù)¢¸Ïçgw"hÖênîTŒÊц”\ÏšÜR}ì1e U½³s§ÜØ3†ÉôJ‰C»XÒç]9oÇRz¯'ö”{ËËb†6OJæØƒ³1ÉnñÖ¢FOhY2P;vQOËú”ßâ¯ö¼j­óÜþh,u6%ý %­¤Âdõ¶¬kY~Õ¶¦’¾m«††Ê°ÇU˜þì;DOIÓ WmOù’^ÙŸnÌóA€ ê#؆ö>ï””& ¾Óâkóãkºæ1,Î+0–*ªªôy1jHT¶UµÅ"Þ _…É€<Ø“Í8³@™ÄlìS‡5ãWÈP6îjÞØzª²¢|å®¶ºÕ-þîî–†ª5‡½ƒ6_‰ÃU_æj:ÇW0×V·uë ÃZ8ÈÉo5¶´íì/ ¶ W­>ì+Ú”ê94\~íæo¹[o‰uV%ºðçŠÊ½Õmk:¥>«œvûeXŸ8Ni¢sZåÆ$ÎÜ2WÉ ™¦DO?[Î¥röké@&ðÙ¬&=Šã8Õ]ž]&¨7Î:o÷âYeZ‚y•þ à» —2– D 1Áˆ¢;æ0ˆ¼ÀÒe#£&æô6ÑÀ¦™¬Çv:×`¶œ &²Ë#¹ül©±”Ú¡‰ö”÷q,%r^ Ì–u¥¨ÕOîªë¿œ¨(oXÕXœØxIOɲæ*K‹µÈäòÇý¾æ²¢†TQÒ½ý2|/7о¦ ¦ÖÖܰ­Áâ+uö:ívWíòª†^½ÖƒF÷²æ|ö²‘fW©WnëmÊm}\qtÈ€» ÂÆÎ ‚´/]eöõXÊëFÂ!`¼7J™=%GÎ\N9Œ!+l¸2eÇvû'‰ádñº£åÃô.k h=¥*^_T ÔGlCëŒ[××—•ö8•Þfµ´lç^ô7 ”í¹¨°ìªŽkkÃë“>Ö¡+Wµúš›àÑ¡šº–:­ ¤ƒJgoƒ__ƒõpôž@Ê]n<ÏéBáÂŒÐvä^ã*~¸Ÿ§©Ûü4K–˜Kfm©Ûlnj¢WkZ#= þª5ô4¬mõYÉÀÓæzÏ-ØvE²ª¸.êH–ûê£v|—·¸yMcÏe›’‘¥›ëÆ×­¯KÿÍU€wáe]…U]eÍÅ5t>õl>Ï r )5oÐ+v¢íBÆáh=½HÈNÉάr^Ê3’¨pqÆÒ†@=j9zJkIϱšÆÜ7ºÓ'ŒN"â/Þÿ9¢Ò¿oÙ`°¾óŽÍ4J†ÓoZËœé+dÞˆy ÆhAí'ÌŠÅ€‡Y dŒŒÙÐ×FgÃ|˱h¾* ²„¢¼†sZrEò–š·¨NËŽÈ=³Ü_bÈÍk(ë¬îO_‡^­@²ÖXË’?d²ä©3Ÿ¶Œ—£W½)þÇ2ÑJ3@ZŸ”Hd±9)£V£VI¢\=F:Îâ Mº¡IMRWr§•¢Nª3ÖØwë½ì·7þ—2ÉvG×­·vÝq[÷Ç>Ö½êÔå ½Or7“ Îgãmò¤qz5¹àbæ'eãîÆ˜Ž=îÊ÷ß§7\z棇#ÜÝ\—òžQ€»Rý>æ¸+ñÐÌû4S×¢§™ (œíÆ“dGƒÝJðFdGbqkXèÏ…ø5}:YÂ~㜳ç¼rÏîsØZš"Ï¡zFx†½¦6)âq5Ë®€±–Oáÿ®èõ¤\\&J; ù‘Ui:B!ޱ I‘%ÏÆ4‚“°¤6VE›j*ªŠêW&bæªÂ1¾¬¸¼¥µ¶´¿)ÖêØÞc3ß…%\‡ QoJ}5–Š…Y{Q}'u¤Y]*ÞûÝXJGÕ¬Õ BRȪ+ÅQpH»²ü¥ _$éâöZM„·—Ø–¶”Äu…%üOH$* :mÈUÙæXQ´‹L0¶sù>²A¸èIUûͦ'¡«…D?gŠc9Ð>wÃÎ"ƒá¾W\…º x|'§®ì2gÀJÝ ­$š±_Þ,ÿ.+ß÷WW ý}€&þjn‹°†Á<Ãá;îFhŸlô_ÀP!µl,l»Lt$cAl)þ„í-È)$ƒ¦s€/ä;]ÙñŠ¥w·‚ 3ÁÄsØE\̾ ßΧ¸ t,ðŒ‚B;v²<Ü­T Z4¬Ø§XyЀÕLeA·H6C þs…Ð1á ¦ÕøyRÅùïm¾l@˜…Æ®"›Å L#D—ìsxŒ­G<£€Ðz€Ž,…' ëY¿'˜‰f‡ësòÌäþ-d Ì1“ µBùm1 ì¶;·¦¬3{æeΚÏ9ìµ<^fÑ¡+((ðx¢ŽPXÎ[ÀÖñ_Ä+f×Gp[Ç÷‹%²¿'M%ªvg7IÍ6SBd<« —\Y‹|ùû¥aûõ1¾‹Û)È>ðÌöKGMu„¶Ëº Å”žágΘ£[Fð&‘ŸÝ4ªÓ´qò~æ¿K(_³¢ï˜H)o•U£‚Í*´ZT~Ÿm—äR|uo‡±Í®AWδ±5¸,¥5PÉÑKåÑ…e;—9ë¡‚õ iš˜W 3 1û•3K¹=nX¼òhÄ]ï©Ï_>­ì7xÆØ©¬<Lµ'à ±_^ ¬Ñ0Dåb8ùØüºH`G‹YÜuŠ<5ÉšßYÌ%¿“Á†²m6`©±Ëïòû<6‹N‹‹¸"I¾×!ØË÷q¯>±¡yÞ*²´& “$ÒŒ»“ ËìÐ3&I~Ÿå •õ2þ£á`ÂAW!4g‹„üTY!#²œ˜0@ óäkY³‘"9\èŽÕÙü¥ÎPÐú ›¿Ìiõà G¬^ÆÄÐ+ˆû,eþƒ÷”ú,ÆÂ€­ Ìom<ÃÀ]!t#;z[vPµ³6›ÀµË¥Ì·áA9Œ€ü¾ƒ#¦È":.]tÁº¶Åëz«Kommr¨³9ï(õÁ²^‡trìF˜[…0×3Æþ£ü.ÌÍæø •,vT­?™9¸RSi¨/Äojk"ðTÀ_¸¦¼Ì[æ6Œ–•{ËÝX§ù1îRa5´µBæ?ltŸ© 9ÅcrfÄ\s9ªêP®Ñ++ûmÂÈl2è([ŽìØ.‹0ðÁ¤VC &*¹3Òµ¬wÍD›~åeûu\o¸Hйcü¾ˆÏ1UÑPqÕ…±dÆÁÀ| (8ž)m€ïÍEË•äpãò2èáS¤X9S=ò™j=‹3…†gÞËw(×bÄêÄ‚¦?\‡“üpë?¼"=ƒK¶¦ßJ¿ƒãÛÒož¼ãô¶çZœþÞ6>Œ7¦ûû%說xÿÒ¿§ŸÙˆ×ž¼äøÒS— ¤B×Í4HÏ ƒ³âB%(‰zÑÚâ>ùäì¡Á% Da³·ØÅ©Š 7õZN­1iÔ›v+gÑͼ™žN û~Ì#–Äñ¨;ÂxpQaAÑxPàT ‚”f^¨ &“Õ|œ^+LÈ…ŸíÙ ¿@JÈ…OžŸè¯4‹PÞ«.#­«ø¡ÝuJrøbõ©kwÓöY• ôGøkèÇl EGv4·´4ï8"¼uñd3ü7yñ´5ó]æoz…¯eUmskksíªßÅ;2OÜÔÜ’”gQ¡KÓǤÃb @iZÎG7£'Ñ×Ñ‘ʈ±áG?ì¤12Léí_#z«u_½žèÕW]BTúÇ%D{î"‘c˜“ÔËMz§J0×î’áüX_lw¥š—?ñQªk:Lz³ÚÔ^ÀÆ.êl‚i ¬hB“ȈtȨ›BZQi©]¼¤"Ò$ð+·Û`!:¤WéôãH­1Ã{µjœ¡žq;J°Py^£g×0â€ååÄ) vžmó”)Y–ÍKœJ¢fý¢Š'ì„:ëdð …ùˆ³>xÌïú_í(§Ô–ªyļЀP©õ*õ`ngjÕíO­êf44þÔg÷ï{ëÍ×¾ñÒ¿ÿüg¿þÔ×?vë—¾`ßÍûo>ÿ¼m[V¯êY–¬øŠ BV“r%Ght:¨NÌÀѨ™9ÁD¥h½ëL¶q4Þ&ç¡!¦hT0’hÌàôP®ÅƒÉ„GL;KD#N<µ*p”‡Ï™ Jp¼ Ž2ê-Y·QÖ ëê&¬œÉQAÕÛTéë„A… ÆÃŠ9’  ºwˆükfoIAe¹»®¯Ò¤Q-h<¾bµ&V™ž0{K *ÊÝõ}N„@µ öú<*xU®Ó6Kéj¯ÓoÂÚŠytÏiõªàJb)Ô„JW;“¨ÏˆNBtp;cÕ¢´¢6<Ð×érK• T®P¹Ëꋟü”oÀmR«]6ÏÀØ–jކM6ªt%Õõ…ç2˜h;Ú­éÒ+\_œß:§¢V0HzZútéùß;K¼–Ö²ØøøhØ©Ó zAã/Oxê?|S~Q²^Š0˜Ìþk†P4jŠè"îñíu˜y½J¯m(Tù¸“û#ÚHqÿø}¥6Û$íËÇáãM•‰¤3ÞsI¼JP»Ý…BQ©×üz1öYãñR«»Ü£Bj Pz±c·p¢o„¸1SMKk …q¯jù3µt|Ь†J<Ô¡]­’íSg\Ü—„3+ÞªT9s’먌]E…fAEƒ93å'PÿþY/ÃÚšê*š`7,¥(G¡÷£w•S ŸVb5ƒVNDNûçe©½HV­n>¹ÌÒmW™¤A•]gðÛŽY"ÆZüU\ßeê·œ8#¯õ˜9b¬;õ=ò5ÛtÛ%š;ñ—š¬*súú-ù[ݶrsЖþYem&±é7›,’Ûl!sùöúS˜üVì€o𦷑õ‡:ò_ä<~øÓâÙð0„¹¨*ü‚³”]¼*fõuÍX¾Ù b{iWU‘Z«ž%¸rÝÕk¸*{¨Ú¥·Ù¦Õ¯¶ïÈh4# 3…|J¬@>Gu@ËZÐLJS…µ*#–´™7•L¼q„p›V‹¥•hd{•,Ò:Õz"в}„>+YTd«Vµ(^.oáÙVJÅgË Ô9K¥ÅªõÙšù¥)æóüÔ‘ƒºSWV”Æüñ@Üçõ»‹œsÀ`Ž+ñ9™Y†rQª¦é3ö ®c¡;YAø!—­½pãµ£¥Áe;»_Ôyéóûð]Ê~ö <ìÝ»<´ÿ©÷o™l¢V-ÓŸy°åAӃ͟‘Åý ¡båÞζó6õ:\7îÙüÀÔ,Å¿vœ,-éÛÕ1¾']È]¿YJG…òž-§^Ä¿¹ÿ ÌMR/_¥ß|¯ì<´ºåޝTŠËÿÑ&ýyWâ… >Ó說C¯ÌöMµ´%¤†€²}S"¹}3åQ$Ü! ÇÙ¾9Ö7ôœtÞŒ‡>(û[úÍÑVÚõ¡X¤¯î¡]ÓÐéˆKôQ?™Í…Wgv~¨eŸ>ÕÌþþ5½ s« ?Ü‹6Éâf8¤?ëF#ÇêXÀ³LÖyê‚7·•³Ðh¶8u†ÒÄäÿÂrB˜ÃTúÓÒ£@ƒiþ7ÓÁ*^’òÕ%̽¬¬,ÂüZÚHho·áibœ¤ÍÃ×Ö´q8Åð…#Sƒ·¼ò;,ÞpàÇ?6‘¼åk¿OxýAùCÍú«>ó•Ý—üåÇǯè燾²ûÒ¿üè¹£½Äv.úïoÞ6ؾÿÁWÞ>|Oú×™ÿyÅy¯?w×®¦Þ«_zó7+¸æÅwÒÓì¾,;ö”#kÜ CÆÈ¨§I 8vÅš3àz6 þŠù†G.‰“¬ß‹r#’‡G×_Ÿþ´êÐ'µ“é¤HÔ¨ê” 0L¯EÙ?[u³°´$–¬u–PÝPH²Æñƒ˜èƒnž‹¿+_1ÑÐ{˹]e}ôïyËž 4®(-* X½ +ʊʶc N‰üû–Û·T-»å­;Øß[ß|øÊ§§âκÑö+žÞUî¬_Ë%ÎrÎ.T ô«%ÕXts„ÇqÌã%T» k0EuýÑQèÒîD2ôèÁŒ.k±];õF¥Úé(±MŸÏ}¾fg]QþœºÑ ªé÷c[Ñ×Êñé1’¨ñ(¡—ýr¿¬ÌŠæÈhn¨¯‹EÜÝÅÝòÜTóÏÍJ 9ŽdV÷)1ƒO2¸„̤œÆ—²|Ò"s>‘ܱ¶Ã²&e²öÄÊ®ª%áôß­ƒ–ë9—ëxqW𛱕©X mmú„­¼`ëb 2=èÚÑe3T7¶4Ô—ôt-‹¥cŠšwy Ú•­ÝÖ°8õ¸Æ¢Ú1gÿWxoJ­÷cž[;@Ôª, \‡†F÷’—K…Ô‚JÍr"K‚˜M”i¤âNîò¯]Ó³¬©œÊx‰{uñjyu‹.#°ñÊB^3k5QKš±šˆR›‰ Â9œG‹,é«Éí#m–Q³M¯œ=¥eõë·× Ù"õ`Y‘žW•z5¦±%½ —XWœ¾¸·º8<óÒš8¢6iëZêj†Fñ‘æf_kUqq¬TçÐê|ÉõáÚµ:®*+¼hOÙ@“?Ý!˜Õ4/ sÖ¹ Õ jFmèž”±¹)YÅ‹¤Íëá8 Q¼¢+Ô‰†ˆš)¤ï7"à99Oi©VFÊ¥ÇáUV™Wh@üZ´ÆX ¶©µ%Q_W[YQE¡ ¿Ø½è6á †³\ëgy àDµðsµIf`çĵ‹í^{ºS$N#îsßè3Y‰îÈdømìT‰7Ö©ŠÌÉ4º¼~ÑøëôÓ£&Û;ïX p,ýS|©³,Ý})¾42ÕLÿe9õÍYçÔ‡V¢Ï§}XMV®èôœäLP£ØŒ[˜%³š¨ˆz38ž¨-Hº"žÒÐuS#Ù5[äøq€q9&[gfÑ媪©\w,U„ÑòÞeK;R ‰ªŠxÉìâkX|?—{ÅB©Ìì© Ÿ$º)I!˜ä΀mðÓ;.ÛîN¥ŠÊš}þ¸ße*²¶Xªš—•ô\²1Qܸª¡¼"ñåúj!ýã7>` šý‹nÊ{é°G«ïm¨Z^ë²ؽÎRŸ¥aÛ #ukSAÍkÑPß5éO‰îR4gQ/FkR#ƒ„%“ç‰@x–O± §'‡ð€yè•ÈQ”£hóa —/on‚E^>ܹ¤©·¹7Y_÷/ºˆVLU\¾Ù3ýÊ™½ƒ£„Z1~qô²ëîöÔY_õTû-þÔú–ž‹ÆjÉe!OcQ¬¦µ ¤ÁŸlr–6~’/ŒÖyËÛT‹’°ûñªBWúí²¡¡Um;úJš¶]µ¼e]‹×jÚÔ»¤¼§Öµ¢©¤'á»Ö³¤-amØÓ™‡³+P5õ§®ÂKŠ.ð°€dŠ‘8£l )¦á™ƒ_Y醓_Y]Yí®pWDÃ~…z«X1êRBsðb ]˜L—’ÎEVÆd 6ÞnñèÓ×\¨Ðã* ¯~Érdq:`ršc…Ó­òCC‰ù1“)g6¡åhÚŒ&ÐÎÔä¶ u!ðj‡ŽÞˆHÀÑ‚t¥Á q2`€#~܈•û{g§Ï2-+úZš)šØ¾uËæMëÇW ÷­[±niGóò–ååqwSq“¼ ¦Dµ:¤Xµ9™qjR¹ñ‘Íþ¸ŸÏqs…ž¯Égý¹§çJA§†Ï(\8GÂÖ<ù }÷é—Ê"CN£š“¢£©1Q[ò¹ÕÔ﹌‚} ñ©ÈÏ"ñàI1O.]’jo‹ƒL$/¤´<ÃÂ%y‘;Nù›¤òíbÔ纈*l5†-4žÌëŸåEþÉlÔí!<©¢ËwÙÁ­g› jAPi›Ô5]mî; zAPïÔh+ìòjÅP>ìv£5 !lFÛR›×ƒT¼i°ÇÜÍø:*Vq* É„îR͉Â%A”.MÍÊY¹H±ðšÕ«FVôÍ·âÎRÜš™”:8â×ÏZ;yU>œ•ɦ?8lž¥d–>™±§ž¥‚w›¦yû±mG»Ð¹è)ÇnؽÖøy·c€è´TtãÁ%CvgTÀ¨ªø)б©íÛÒ" ÕÍ#Òéó°L÷ém µ(‰jŠnhK“¹-©li,¢Û¾kçäÄöm[·¬›OL4.ºýALœ ŒJœþü‘eÇ“s@"Í„ÉiÕÙÁÄG)óá#ÝÇ$Lü @Tyrf3Z‚º€sêšs0µoHňíuå’Dú©È©ç€|Óy„î¤ÿ&‘^äD=7¥‘J/“*̃À„' é@ÄÔÓPV Æ[Ñ×µ´³£­•ª®««æ ’Ƴ$çî7W®<Óž‘{N.O6Í¡'íóH›ÓÓgÜ¢\™suþÚÿät”“7BÌ“CGwÝ„¶cN>‘¾Í°äë°Žl[ÙË©U›6Žç‰¦b‡kñ"(¢ØÕvPëR"º)¤SÁ¿I¤.Ž¥+¨>븲  Û³P#zâ5ôÆ›65™×”f^¹Ö‡ÑØÚQ@ëý+æ“n ÿœt›'a÷ŒÐñÆYK¼~yÎIþoEžÞrF@9£üù<è¹[‘гG7fjÖ¢-h}1eÞ Û3±¤‰ÈÆF¢â3± iTG%‚º°Oÿ¨„«9¦ûÑciÆtX…Ô”-o_ . oN-R7VÚÂäœÄl €ÈÇÆ–'?¶elËðÊÞµË×öt/ëjËãèØmõBbxx˜O*?#ú¾maÙüTñœÝýý|ÂúôçÏÈf.,²_›!<óJðdÛr!OŽo¥\'šLmkÃZž9]Mƒ›OÃÉ• íR<òœ¬1ÖÃÙÝ?ÈàæØÞç~#Ê;O –i¨Éü.-Oˆ@v!•ÄI* ¨’$Œ¨YºrêQ1{wS;·¸(©€åâ@BžS ‰âìý§º £=»¶n\ÑÛÓÒT5^=>«ÃÑ¥ç,깺3Ü0pçêvNn_FÒíÖk› ùáëŸÛ:rÓDc¤sý¹/¡OĺçË·Žtì»gMϵ6w…;ο‡Žó!|¹‡»ƒÙLÌŽó´HÜJëœp‘É9á<ªØÑP¹²É_90ÙPµ²É·Çâ+/ò6•Yüe.Š´ÉÞ–Ý++ÂíkªZwÂßÕmîê°ÝUѤAä\•mŸJó× »„Ç…Ö<' $#P‹rCšrbû!æòsÜɶ‚žŠh"ä¿úÊw¾ÿÚ”÷eü*6 1{IkôüáMl?¯æ¯¦YnŸz=gÛŸõ:¥íÏYèLû>žÆ¬ Þ‡ísO7mzñÕ¿RxlÓðùÑÖ;³ašÖò—£„'u¦4fæ€#ûXPS4Rübg=,rl§•àUÔíç¹€…: [Y\jòÎŒX×î<÷pçõ×ko0ÿì{‡öî¬ãÿÍ*¶ŒŒÞô)SQ€ºpæwrï ·# R—*LTÐ>MûCRÃ#CßR ¶Y[W*±%’µÜûæ·ä†ÍrW×ion¿ù>caÀr-íiÕ¨âsû¬x,ãs‹ÔçÈÞg±GÐ1y®A9 eq×in¸ª<7\_NQÕü¹Ñü"yιª|çÜ"·›`wØò{Osú”}+{„7øN/û«o  z #58{îˆF­WÉ‘Èi,5¦÷"ƒ”ª <'Œ«™'Õ„ïÖi1ÞÜç.rÚÍ&m©®kàøj\Š¢È?̬éQ$äñlî¥ÙÔ¤éuZ ¤L,@€ä‰ÚŠrþbpÙ&î7þuß iQ%^õåÐqº§»'O‡ééÉ9ïî|3³3›ãl`v˲ìK’ DYå%Š ¨ €rŠäeð8TT¸3bÎzÞá=EP Ûý{¯êû:MÏìàé?ïØ™é®zUõê½WïU½P›ž¨ûXZ¹ê5ë7ååé[ÏÍQnÑh%0â†ù«Òü1(C‘—,…I¨½¡;¨ˆk?­«Ò÷íFÌÃ){Œ`è7¦÷5>àk@ÎH׫¿`ø‚ß¾ÞM~ øzòg„´X©ü• 0, 9¬…'ˆ“À±06ËìĶåŒ8¤~Çõ3xŸ&Aò-„÷SB6pO¿P6z ¯íÅP[†S•¿Æìh[±Ñ —ÃU™‚!óø$¤×÷*çÒ+‰Ó3ßT°òÚ‹´Qù#¹Óm ( c UûÉ:Ÿl²­Ëµ4JSm² EÑED+ÐáÁîÎæTq¤²ÉˆVdû<kÒ„ù.Y|V+À§:–eÛ%¯ô“ ¬1ûEÑÁ/çë} à¾ÿ!kɆqb•º#m cœ'|«ðû:§W¤¥.à­äÞªDÿl´ÿJ¿r.à¸öôb•Ç€õ²==šy|ÓÞÓ ®XY¾>yG.®“íheÞZ$XÌFôÜŠ-§fž€%%S Ùš _–ÿ]·àùj³Ä~~½YòùÜ4(PÙ'™ãnÓ0̳ÏÎý{·ÂsXHÒVñRqvC9 ³ÆØBUîúÉE?Ó#«èam§§Ÿ _ `/Ì1™s¤‚•ž©ÂŸ”å?±˜TN?‚{‚‚BBý˜Ø Vf%7¨úÙ&v¹lÀb9Ãå¶èå~–a–1©Ÿ%ÊêRÉÆþI†?ìjôu¬ë«®í[ÓÚ¾9S¿¦êêš¡ÔpEEëp´iª¯¶*¼Â´²ΰkÔß0ZßViå(˜kýx†-Môtlth «£±¡¦**ó±@ãz¡ÞtêÈ|B:Mø”"ñšŸÒi0ÎÌùûó_¹œ¬ä¥ŠÊ™÷5Ö×@ö˜•œ˜æ Íƒ³’_\HÓ?ËÜ&Ê÷sÚÔ; hÎtJJév¨9uæpC Ô0•'ÐGè™á‘ªµÒƒ¦?òÒW–æ¸dôc?Y&Þ!Ýç¢Ïrg3ØÄ±;sÈgIxÄ€§!¼ìÍÞTpD:¬xý[AÌiÝ.\"¬Ò0s°`¾¥%Ù‚€&Y6ã?¨Öú½êÒ×\ÃäI…üŸâ«*«?Œ¿¾Žþ÷åIrèó+yÈW <ÊŠÿ0Ù]&³PeT’gJµç B>ŽtV›Ïä+i vÆCÿg%§ÃÈ-€J15JB¼ÛÏîƒZ…uTþ*½ dáÇÙ^Ãïórí`'åœüU<)É3ò×Èöl.…ìÈíâ4ŽÀ‚®k}}õŒå§´©1ZWÎM€d×Õ¡Ë[×£äZ·×Õiµq!H)W6$áèb(À–U„¥î[…ˆ ¦F³¸O*Àէɇ³¸ê±:[£i)\ Y\q­Buôȯȿ…½>ÊàÂï÷Y€ììö4Fç·d#«ÉNG!+ËÖ£§¬]‘)sÄtê°a¨ƒt³èU6£ýiÇ`xûL^ÊX*·dÕ¨<Ï•þÿX†^Œ·‰1„½¢=T6Ÿû½Š6ü»­ õ`¬-“ÉÈòwèÛ•Û9Õ¿èȤñf­Õj’Id6û…Þ`¹Lú½fÐä¨Q0?Aæ½ò·éÊ·@#ÝÀ=5+ìøi‰(Kù!Ñè<ÿ+–z/g zk  ø©+¯ð^­÷nܸZ»Úw—ò­k.] ¿¿ëoC˜Hé^Cnäš]=QØ‹÷ÌqÌ‘§Ý¶H"ïûÅ ‘%5ÕU•¡ Ïëqépú ZÁ¼õz0ç7{‹cVg® Y®ýJ}—•¨ëim ®pP¢Ý£‰úžÖdúól#¥cÛÚÖõ×VǪw´­ÀŸt¥­£ãypºúîyçÁ“`“%Èéüâ¼&ªöÒìªeê’W†1‰x,Zxb°ÊcÝ }Cz@Ä»<Å KÊ( Ñæ,^ÐZd1`¥À,EãÿÑË…WÜÇ\AáÏ1—²åœOœ |²+ýNúRæÃè³pXÄÜ2 ÐÊ,ÿ¯‡7þù–©ü` ùiuú ú£‚v ¤b»ÚøòÍSßÿþžsW§}ôÞÌgí_ù,EËó%‹,×¹=í¥Ofžaßßžy¤DufÝkÿâ´`gå¶ô0}6ó1Œ#Úö1Ìa“ùßÅîUÉ,ŸSòGi¯ðïJ6cóXm¿y¥.y4•Š1M4Iïõ¤¿×xI£87ý9j! DÆói~‡¨Pº¬êbxŒy'íâ—|ü}°yaúà)‚:?/…­N¡:Ѓæâ‡/IÑwúDúPúsçð¾{¡ïx¶¯}Xöˆbïóhgê’¾ôÎs¨•ÞÊóÙ½ž¾_üqæib!®Mx…Ô“ñð“¶:ªV;ä8s”e÷—ƒ-á8ˆ{TøW-ßV.†ƒÛË©çͱ_þwNnœça…ôðqüv0¡½J–1ÅW°pLÝþ8Œ€™RpúY²½öŠt“Î`̓¾'à±LŠWJ!çÊj‹sUì+ÞΙÃþ0^)µs×Ü5®v¹MÑëyÍõGß]éûÔ`0 ^¼ûà)Œ¿Û¤6QTêÙÞÂï$.î¥7§¨ ?ß_‰'M2wÒlÏ;XÝ=Œdõ˜™#ptp èù’ ž¢ˆ¬¶MܪY‘ÏP¹PPHƒP)¼uœg`ÒF*œnèª 5TùB •¡d è¿é›VÒóäTu²j¡ª®†PYmjmúî˜$J aiVxYI±q›­FLß‚IXgYlè[bˆÂ$hÒ¡ò ©“ àFQºÊpt *„³C7Vû`*‡Ó=ú5¹ g'ÐÝúá¹|ïî–N5ù ‰r«ŒÝ vÓ2Nð@T^}ˆ~5tqÐ}ì3Ò ŸúÔÙÐ .¾¡ü\©š­°B˜>‘…ZY-5T†øi.Õ5.>‘NµßاÉÕcÊo’Û¶¦/§ÏìyõÐ_àóx8s¡Ò-œ@\­÷ѬÒ¼Q’æ#ªÒ|s;ýNå-µÂµÚë×\r!¥g¥/Ûú«Ìy‡^åwà¿“6‰ÊɤšÔ£ï >n˜!-3vh§ns>’ð#þ—ÈÿÂÌ xtÇT,÷ +ÜKlÉjg£ƒðV¢ÈÊTwZÊs‹5Åa›TÜf0¶æÐ-ŽÈûX©TÌöÏãØŠûº`L.pÆàÂê«ã‰T@ƒÃdÀCXÿ\ÆðËä%ãé;î­ÜØ' þ`6#cªCްT…éìø•ºk¢´ÁtÉN‚ÆŽÔ†XÕËûù]ù½´I88¯$µä)«0ê ¼-÷‰Ì0Žˆüû *Æ0·Iú×ÀÜûy–(pHoÀ¤v[0uÛ¤ © ¡À¹6XwzPt ™™×ÁjζXk0ÇDl,Îo èÙX ª ,¤2êüéÁ dŸ‡¿p7&edØ»"T93V€½Ê  ˆé*w››z pgÒô7s"â hu)#!RO>ϵW]¥KTh-ðbŠÿ)ð?mÚŒ£N¥H Ù Å’‹TáHrHvr¾„ÓTš[¤­Õ‚7ªTPærÍÉB­±†_E˜p}E=ˆ¦&y'ý:œÑåÙÇø<ã™6ë}Âé<£f¼YörúÚÉõô!žsÓŒVÉ O°¯îbF÷`âMñìH–Îö0:»Òβ§Õ¸,{ð‡èÎϲeº”0)Ûÿ×lA®™Ä@eÖ ¢—5ç7bŒåP†?éAƈÏO"î±é"e†=Å©DkC'åèâVQ*Ì)êatúÔï$¿x»òA¦sÁï Ó>õ§‡X–ÆZƲAžKËÈá#Qt˜³}CbXM¡ BQ`ðÜI˜(Õɶæ¢NÚÊ”AeESðmvX£ûlou 8CR抻dc» {iT‡ÃíêlN%bášpMe$—Ä-{·‚ëK”Xßrr:[ŸEÇ`}OðÜœ•è’=^…?ÕlnÎz"ÁŒ7©TÆzïT^h­f«)Ýìö×13o©©lK™µ%*6Uç5Å5¶· ´½·½·§«=«Xã!åC%ö°‘­q™…5>f¸‰°'YLµ7r·F%“.°‘ &²[¾pS€’¦h°‡¹ÖDÃÆZ©í«¯¯eØÑ”ª¨(ÞB«+ÞŸyYj—;QßïÇ!ã<7i#c܇z¿  êÂnŸSP"˜C/—W ´©±®& øu•¶ í*ËñwzúcâËòÕ¤•¼ÃòxðÄ ªROÑ]ŸçHf˺³b+¼Dî–ÂqOà­Rö–2ÎÊQI;n¯òä0Vk ¿!Ðé¡–ž@c;(¦=ÜÅ€çÏQ@*.fß·o.ôÛ|ÁO\Jø ¡&²rƒÛ'LT[Ö½¦®³2 hÞHÙºUƹé3÷‹cn1œ¼êc—䓼ÚIg¼ë¿”:o|ó_™^8Pº“ÂyüÒ»¬‰šj% 5ÅñªZU<£|£Íÿ¦lÁ>Áû„èc•Á#ÿ iœ¥äSë%¦©N”Õ_Ó4çW¼_P¶¸t#©ðRÉ]‚O¾‹ô.¹GÙÿa´à[-ô M’nèËÜåì®dI=‘À;1S7žÙ„ãô³Ú²]ì·EzŠ+gìô•ÖŠ¡ÞžÞ†˜{þ„;Ò"Ï#ó8ûÏaŠ\-.,-æU~Tm^{æêºî¸lͤq^úÂý÷ïOŸ¼ÿþ}§Ãh4Œå»¯XW?Té•Ú’-k†’ªN?é Þ}0÷»„ªŠ±uÀE˼ëæq‡8ÿq6Ú²í]3톾=,†{wŒ®ÏãªËìüåo^’×ÚùË߯îrϱïr¯•¿†Ö0ÞQºsOr,kX Ôô‘d2Éo,(ù‹ôú¢ün°Ûʘìpœ3J‡ôÅì3¹¢ª3Qî¯ÉZ„0×ÌUé WƒbÃûG8)ö³ËÎG2˜ÖôZˬ­dÑ`º4wÚ¨#²(ˆ2ÆtaMl…'âŸGHÕðµ, ² ˜ßƪÈûÚ¦ÖŠehöQ¼êª—Ó ö'Õjû™¯§ïïÉÍSx‘éMl"ä½–9< èj5€tfÙC@®ÃÎÎáY­`a@T-eV£v±FvKàpb­X•ÉNøVWà‹ýâŽ%»ÌXnk´)šŠ7¦ÊýåFܾ×93ó'é*9F:ÈeG:سçËV¢îûy¯ð–`À’·TÈ•ˆWlì˜>WìÚz¶uÁ‘wŽWTpí"B¤³dLÊc’~/\ƒ:ýЉ`ÈâôòËÕ(ªÂÄÐx™¼–v­"+ i Í4tªbáOL"Ë7€DÒ*ú✑¾Ô/¾èsÔ/þù}o~DÜû>Úzë­lî9UÁœâIJ–ûyfs[±dl‰{í‚£Z’Ä-ÖBdï–1?~œÄË¢‰¤?à÷ÇÌH mg‰¥Ôh- ÷¨qÀm(Ly1-ø%Yt@¸EyçEÿ쫼駇úÄÏx?SsÇ…É0­¼³=øgW}üïÇ.»£&=é6{ú¡ƒñÓFî½¥¾¼ùØ«Iú½pà’¹rÚ£R|æÝ‚ïM P  o‘»ÙåEÑÅÊ!nã‚5 ÉV¼Ým•ïGà_.2ÀòdØÔþQÿ¡ú-ºÞ%V¥¿êß•é/×Í­½rϨ.ÔúÈ®í×=}aú¿SŸñЊûž2ÅÈ}“‹+?zQ•çÖwÝyQ•ûécMÎô—·œññóFèÚ…Ý›¯>áøWhÅ{Áƒéûh/¹,Ïy½O«•[…?¥l½Ïs¹ãfi‰s9–kø"+;Ç8%tðúð(ÖúôÉÁ–žø@q¡Ïþ[&w¼/¤%'NKŒ'CZÄ4S5Cú•a}O_Ö÷}ý1›ãº#"OÇçÍ S¦KÛ•¯Ò™‹x…%A:Á®Gr´!ÏMi^ù¼CƒÛß]¡G‡jZ\F^!¼+Ÿß8ƒ…íüþ®é³–±9Ý'$–‚7ž){>Þ”e”Ä[s1ÞèUé¯l§W—#êÆã !-\u°¿7ÂþNüãçYr?:ëýCˆOr,Ö†«±f¸ß(œ$œÑ çJï=ΓÓá<‘¿È¤ëCô6ïçV¾öQ ÔwU‰‚ ÙK¯ŽA%‚,ÆÔ«Rú½9Ïàgâ1À¦0„¹Œ¥uep^ÏEì Ó¶/f¬rJ—/êl§m´›aWý³H*ã•å›pý)<÷gþ×ÿ˜ˆöÔO,£ ÓC€Õ‹j‚©”ºLe' Ó¦û%x&P'¥ ¹â¿ü`eg•Ñ~§'ªÀž ÖŸWX¼·5\ÔÕ Ö›·3,=œl `J6t'»i‚&q´!Ô€‹ÈPú:‰ÊWï"ÿa•`ªöZQõî‡% ðš[Úd'ˆ®ûõìzs½u¢ŸùÖ[Ôñ-®·!AHg{¢«¡ Ï'¾Þ„+ÔBEæºQ D`éÇv®6÷¨Û›5£çÖ¥5%'¯>5´[TcìÌž²á½ã§ÞÿéµÔrÁÍå%ïü¦ˆU˜µá‘âØ›?{.‘”:] ¯íÝrËY£îׯ—^‘ŒëßÕ{ðÍ=>ÍŒõ®ë-wÓJÂøî:Pú.søŽÌ2¾û’ð øYɃ§‘êJ„RÑ ä}rÁñu;/uÁçrÞt&ײ+{LU%CX8€ýš*”O·ÛïÆýÂÑVÌãvŸ±DP(®Æÿöf˾ÍX5ËF(]59²zÙj:L1µe_9ÚFD<á‹ó·Î[2”¿<‹vÅ-L[Ÿmå\û3À^dÉØCWµâMÀ-ç*,g'ìu ÇY3i´ Àå™x ¨›ø[ üm¸ ‘Gä3‘îDôC}˜ò¤·e^‘UG»Ò«y Ùáž»xq§ÐX9¯‘ýÖ©.î÷GüñxÖ_²*óWùb>V •·8½CøùóÏ0æmV`Ý” Q,\Fe¥.ï˜i5¨FLU3w¹xJNu–(D›dV§vUw^ŸˆÍ«UÇûõ wÊo>cU»Ì-›7mܰ~åÄ`?¦hj¬ª0Ç]ãñ¸»ú8kø°ðtÞþÉòõ *moó‰’Pƒ í´8é©tªCP]0AR0Áذr`v-ð¡ ÚÓB}ò[³ÊÒË—EüñDs<7ãx/(Â|g öw7›ïG„Kíùþ æ{¥eŽ-cq;Š äfªªdä 6ŸÌŒÅEfº@Ÿ¢™b,Y$˜Œ7§’l¦ ·å0׳ù\Ëù\/ds½Kxžë`®±\›Ö}Ä¿¡Åɶg÷½=Q…¥ä09lµKìù¶‘Ƽn…f¬Ä²‘Ö—¹}ÛôÖ-›W¯·F¦–Mõv· ·WW¢¿}<î­æë‚uüJ>×1Ä×ñ{NçDÁ:> xø·"ù$¾ïôÕ¢¡m§Ä”í5 l9ÜËQ+5¬é"šáÒfY¾D7ò'+ÿN¦u{7ø…|¿Ó›Àêh>ÝA*ìÝÁA5on ³¦Ýì$/c-/†àžÏ~ Oƒ¥3v™sûO>qfçô–U“Å,éÁûÒ ¸þÇu/Çõ/l\K ×w o®wZ|\Ï®L—#SFògãÆÙèÄíÒݳ~ê"¦á29ž=Ï>jdÚ« ù˜ò”ÄpI`ó ²Ç³¨ZÂ\„iÃv$Ër€xJáy “A”·^xÁÞÓVN¸ÌK/¾àŠ ¯8ëÀiçï=ÛÖ‰=+÷o@™#+aþ$Ÿ{PÉ÷àu¶B~Å=u؃>ËW§DED…áf؉>Fç’î¥ÝÎì|Î컂:^¨ì²ÌËæëÉ!7pûëê9oØC /x´˜E`â¬ü¾ÐZë‚xâ<(…=×±é­ãñØÌέ'MŸ´nÍø–‰-=]±±ø+ '§?Æl’à÷jùâ7Âñû~æd~)^øýˆp”üˆÜkùœãF—{©©:G ¢¬‹h‰ª d ÂŒT“'pVrç&zÖÓÔ&t:£-X©xæA)ÝuÆòÏÎLoÙ¸>•´$Wܦ—zXÏùzêùzî´×ó;¶ž»„?Ázž¶üÎz4yÍ`˜Î‚Và\8¸Lͨ5‰¡›Æ¼¹UÄö„‡êºó.jYÁbJCZ°ûŒÝsêÖ-Ý]sûN=kÏY»Nز{ëîq«ks÷f{¡>îëKê`;9_ÔñužÌÖy'‰Ø²é¶of9üÑQ'hzîžVP E0PºbÀ±¤MÅzèþiJ?ÓTŽ(ÙYV—Ó ¿ÇÈ_qv^ÿÂ>p¬­^58 ›æÆõ«¶®Þj­˜œJ%Ãý‘~3d†À`Š{mÿ_²!}XöÈ‘2An²Ë\TTDAàýüK@ušR)kDsÖè­ìޏ|BBlæBÕSè& ‰èãP¢q^^å+H‰µ¢¿·µ9V.WeÒA;4o‹l¾Ggv#•í²è¯¶yBxáàâjóëÜ¢¿zºuÕú-ã³7ïí;ð™Ì.ùü»WãOw…«,^íÕ”UøLAïš=4}æ£W®êš½zúÌÇ®\õ‚k4T=10zvcèŒ;¾ð¶Gió.™½û§W?šþþ.¦ÔU—l«¨mPuUüÕþO^dtÿËïŸûçwX'ÞÿGFk3¯ W˧"}¬E«åÇÌæ;’9Ÿ°È34GªQö°?^¯…× ßæÔήTð%žEù9cé©Tª©!îg®çxGÚAˆ‘c¤œŒXƒ¦!P•æÛ¥-Œ¯¹Ò Ö‘›°},·²’#+üÎÿ\‘–*Ú#¢‡ :ÂX"` >ð™¬ø…ÿ?¼Tð§ÓQš);¶¹wÆ7ã½Ô'¶Ð¿¤õk.»&ýû÷Óûà'óü8Ìe#Ì%JV[“078›AÇ…sPS5vÂ) dT" *«Žé‡O$šæÂ+æxé–V–%Êb~ø'ªGZ(»èbîÌlfNù¦žîp ‡Þ\éW¯VZ&fûculŠëkÃø™Ú<¾«?V?ùˆðIÓ:y"¡§/x¿p†i <Þ  ]Œþ[Ò‡ÅÇÅ_Nb‘Û8m»A”³ àeÎã€Á: °/á ÚÏ$óX )Ç¥Zð@¤»‹’áÁ.«Ûjk‰G+ÂÀ´S/àƒc„üHéœRé,©½ß#–ÕÍtnØVѶ<1Ø|ý3çn»aßü<§²"˜ŠS±PMÐZwÉŽŽ ÎZw)üËɨ~YG°lrï•/ûá='œqÇ©—þðÞSDFýõHýtÝúƒ÷íºîºuWÜ7{Ý{8-îÈH¾Ì‹Ä²‡a ÷²:†7p¿rr^úú/™g‘_Î{¨þþŒõú;¯¶H4(@Ìý_dñ–¤3ý(ýbæ Ø§ó œ©z}c¶.ðþð~ˆ<|(=KW^gùCÌ~3Ý÷ÚK%gêªÙ8;ÒTzèÆÙ¼Œ•ª1®-óÚÉÍ–ÑÅ2ë&©<ÊŸÙ£^ƒ:}QɈš½²+N¿ì\›Å°!an ¶äÐX#Y>£DCv¦÷u§¢©hƒRÇôæc/AŸm½9Fìù¾úÚsä& kß Q´9WR0‹Æ‡¨:Ê/< îßÂ:3“(Ój&œZ·~•Õ.)lÉUÚt\žß#¥«ºz²·»!^ÕVÝV)˜:­* ¾-éo[¹?{Ëǰ^gÆýúZ²•û)†æGæWæ‹O.Y;s6d^–†7Àç¶` v#úMÁÐk¨dHÀîÅŸÊð©íåÖ¡‚}I`͘>Ka™ïQxy&\ L0–£º$mà Äåx™Ý³”²ìæÝë5cÕcáÁN«ËjkmiN6Äê1:ˆ…xÀÉèÎ…x`Ež CbUbÙµ&“£´G ˜ó@-åï¢Išuï—£]V¢¼¬1yÂî3ú¯z_lÅ g_9µåP‹W®¨=éÚNsN®ëX+ Î÷j×i³›jêÏÜtp¦s‡t QÒÏ.;u²áɇ7ßyóå»z'–VVHsæê–µýõÂoêé¯ ï¿qÛk½Í»nzdÇ£ŒÝ¸ÿ5à¾tŒ»­@ ,¶0åÕ… 4$ɾxo'Š„¶ ¦I¢˜ÆêD^ÒL×mrÃÛq LÃQŽº×Á`ôi²jÒß‹Á·‰æðº‰ÌÒYªNœpc^9:ǃ"ÉÞòs©WX.G÷ÎV™rŠM±øbê¶ŒËÃFÀèÜ}ÂúªOПèß²bøäñø¹žëzÛðIãñH纞¡m#.—O«_qß¹ÓWlkuož>©cúœÑ¦Ö¢Àäªv7¥e CɾچÚÚÞUMÖæó"­#‰ÊŽDy¸*¬û¬ž±ÔØö¶ú©±_å¹—íÛØïM6;¡Ì¸§'ƒŽw.ìé8ùœíWÁóiª.*(˜Q(ø‹?¤’³“]p «2ËŸNû¯˜ðÓê4Êb·Q*±-Á÷1¼)ècd7°TgØ9§++±bY_/OY[.gñÝãtÜ,ŒïvAÛ-,›Í­‡•òE•±`‡þ;®õl®[yåSî¹¾»W÷y<5­cÝ.ÚÒtáþ¡} þ ‘Zùø)ñDv/¾i‰»‚«÷]¹áŠïÝ93±l¹aFj"cïøÄi×ß [ã[±vKfñòägé£Â¬üoL.ý ßpèÔ5™6u !!ˆÅcÅPÅË‹A¿oÓ7Ä„‹X?øŒˆAè½ ¦†}2×¥W‹žÌmÌgê6°Ç×ç»@Ë[Ÿ™$ƒ¤Ãje ºá˜‘xÀµ” ¸–A•5XŽJT*SœXá•âcò€ï¿“¸x¹ å«x}å«x•ˆQXCV¿Š¿"³VûW°dæÖ³*TŽa¬5U嵡ڀƒYA}mVwâ|›äïÊÛ•íløfý†pçb+F¬þž¼Ÿãˆá¶säÐ^ú%øk–‹]Le=uYñnAaAÔpR¢8¬²*ØçKÉLÁw3VEdoM ¬¼íÑKÇö~ù=gͬ©]¦•yÌPM²m×i\Ó¹óâUÛß¶ºÙë—G'8;ÞXtHU¶¹§?øÂåк¯iK­Ñ]þÊšJÿöþçõ{?zîHSw“±~ãoh¼1w,oÿeÖ0ê[]¶Îæzó ôóPŽŠ7(‚º? NV0c.Wò×¼ ó›Qv³³2ÕOÅX©?#côÂÙc$Зôa>¹žQ|ÙÄâÃÀ–Ra˜9Lt©à¶¨¬± <¦ œSjL%âÉxœ±ô{¤'ÇNVíu:v„lµ6!éšÀ|ö€!|ˆ‡AX&¤, h4÷Ü-P¼tv]•i‡ÐÁ„fql¬kÜÓ€1·X b$غ4ºÀŠ‚%F—÷õL8ršÜK¤oÊë™Ì…ßAÏþ|æßÕ`¶’–/ÅÈîtbdƒà¼áÚ²¶ #)н _úÍÆ`ý&³xýîô§žK$… «N/§Q ®:iÛÕ3YCh¤çЦŸò±ú¥äVØÄ—Ažv“1²þ•ßÊUê vLú Q2ÖšÔL-Éžäí²¿¹î-‚DZXw½ƒ“hB$Í”ð9äžl’Y¯K0 àc«À3s¹¶çâÖÜ’6GÜÐϽ¤~Á·:Pèo(É<~ÐÁÄÀßl8›LeÁîÖÈñz²ÈÖ{†s½y@MÍHû5«Æ7LlÀìR°ßÝñòh°<Ñ÷û"-=xÿ@€ÅÅòôµì_/ÅwªŠñ³wm¯@,/Ò~že¾V¡˜Ý–Ûô'oíŽèu=;jë÷ú§çÎÚ»á‹g¿ûm!eÐ|çì½Ý¶›ËìÙÕ:ôàØàE+OQÝʈd©bEwjý-C-;g÷ôKÇ6 ík ¦BãöÓnz÷æeô¾šÚ£ï¼êºw]÷®Ï>ÖÖ¶ëì÷ÇÓ³ô:_U< mŠz_¬,m çm¬:ÿì²x•Žbпf5HßsÒG¥> ñN ñïXx{@["B£D­s—Æìô~9ÑÅ^:)s—%ˆS®aI’2mj‚¢øgO;—Ò$†ŸÝ¨1­®7Û…½Ua?G#Y¸Û {ì x 7Ö=KéŒ'üñDª1êãSÛ¹‚{Mûn"—ׯKiáÝæ9=¡¦Nð5Ξ~ÎÐ?½/6ºëœ«V­?ØZé+«ëiiÌÊ!–ù&â3éºO?yKMm꜡-WíêšI•kjj¿m±6õÄ£›îºéò]½Ã}ß«£Õ<#N´Ú½Kºkxî¦í¯ö5ïºéá^Ìí Ç@ð¬€ýhÄú¸~Ÿ Y³:cö%ʼãÁÂ’1ìÉZ¡N`E#iŒÕWWUF~Õ4‹ÂÔ^zXM"1æÜ(M¤äšðØNX¤7uâ¾sëÞùÙÑ@¼®Öë,-œŠ×y[iKóêJ¡º¦ö§Û.›ò…?Õyû×ôʈŸ_†+ŒôMÜ^~’6ÒŸÓ¿ò\Lð»H§?ôæ=¤Ëj_ì~½Ôå:ÃÃ2^’½@•§Y»—ÃÙÓh ©¦²(¤ °Õ@}±ïÂ%V7Vs1w_} ËTîœÈ ‰Qªž÷w´5¥êk+#åvÑ1WþQ¤f_¿z ^Éú>á¯Q!€";‘D¡ÜAW| ,ËŠH;›«kƒªGMv^>zêÛ(š5jw2yž§LÐ>²VúNj¢†7ýjÒ[UYaKѲ¦Æ®j·ØÙá)óø·5·ÏíWÊ}IqdW%í ¦Êi«·,ýè•éo¡2´¹@i[Åo‘²ÉZ‰&Ê©ÂÞFmÔÀ2†?©-[ö6Ê ëj9Ô°LnnieOw lôÃ?IÐNXêuÎÙ󋉸í´ÀßKCáè@TŒ‹~óט×Uö‹)õÝS-Mk¢¿¢ßô;Eü.Z-벫:R&R†Ö¶Vö„§Ë„ÛÄkqOú»¦Ë,¯ul¨«,Kÿàº5®„—žK3-ïØ8Xç2~}êÊ?Ï$é6}ýœéæ·“Ê7³,¶.<ÿ"†Ò€ßÔÙ%Œ?“¼˜I Ë(¿Û~‘Á¹›”Љ]ª`§j95!:S©Ì%h‘`71sVU•M¢ò¼ü»¼påÅ»ëÞuœî˜ÚBÂ:8sùdÖE•$¯bJÉp~5$¹_4¦Ø†n'Êg°Ï´<‰ "аÙÝ¢Æ7YZjD&¦H™SAÖˆ¬íÃâ™ÐáœtãñºPmÚn¶ÊÎíÑ–áh3üSÒp ŒÂÅ^w8PK³Ù‘z(1”ª¾…¶÷îñ„†êÓ—ƒfW­¸N’Õ«ÿ³\î9)ýŸ'{•­`hɆüY3xÒ÷øÊ„妿5—®®je oüI9è9™5ž†SQÖ•Ïé¡åý>Oúòå-az­Û[“¾$Ò*oÐòx]¦÷xk½Þ:ozwä hwÙ ~íªÓ³vaúnÛÝí­õxk}éSÃŒö^IOÑg3p8ó·_–nCÿðþTgýeû×ÎdÒ·Ò7èÂ;ìóõ °åצÉâwºKò¬´Wpɼϳ˜“Ü Eÿ &Gž&8߯A1'±¼ŸøHÇÍι‹éb)†³©…œ›yÜß\Ùá˜y T2Ê’œªqj¼Ã È'‹ªXv¥2Ïûõò~]9GHࢧ¹n §ïº3wÔJ+¬ Ç‘PÎ…þL‚—åMèºÏ7Ú‰.ßeFØ|ǯîJß«šW–•4LX÷Šô§„>ØŸ˜ñÓ°;ä©ÌÊ×§0³[`©î%_ ¹Ä²s—S“þ†°’ûÔ0߃tæä{@I2#Á\çÓË’aßÀs‡Ñ·Ó«Äß3¤óŸƒÜ±6ˆ$ÌÊn3¸ ˜”*‹'àÿ1-·£+NâGžvÿ7J¼o²)9:Ð]Uݤ\¨Äû§šWô÷TW5)ç Ûð4¯ŽW·¶à ¿4·²}K¤?*lÉ|è¤ó¨®œPÐ7Ú•¥ †•„FB0ó§“#¤É•róü_ݹWS•ɺtHÑŽá~¬J?!,Ï<…û±ê)Ì‘šY÷úJä6–±àÏÚEÅ» |s½ü]qºÊÎ+ú]§oà]¾£À:^ÎãÙ™ù|ää(üûÁɼ(M ³ò8‡#MÁj|ï:ÌkË2†ØÙJ‘F1ä3—ÔÏs•æa…ìÏðCóÕar†¥±tò+ ™;&þ s“³šåÅÌN–Çx¨“¶Ô²y1™Ÿü·E’¼o¼Ñ+ɾ7ÞH_qýÄ• _Lëâ­™‡™\|Ò73_ûúS‘+_ûæ{eó=°Ç=6ªuqCöcg@¿}÷Y‘¦Â¿eá9t3ÎTB^»“ òC/Šl;§`x)t2 sSû£päËM‘¹`–Ì尞ͩy4Ó@ Z¶9Êü¥úAοôÖôKB3/¤‘¾'ÇHŠÜc{ÑVšQÀ>g©ïœ¿¨à¼¡4AUTA³ˆ¨‚¤îB·P ÃW”y¯ÉN[#{‹]¢Sñ+r5î_Öïo ú=½˜Áu¢Õ _[ÙG‚(Õ\&ª¼ì,q;»@@cÀMÁ>ÆICC_Töqœo‹è¥ºúy–ðÕÏö]ϼ Åog ßž‡þ;=vËóuZæô‘ùÅFØ­ýuoìÆ×‚ÌÓ Ëzœ½§ÙWûZùù‹°íg*eó†ˆLçÿ4y:ó°zƒL:« Áˈ"*`oð–uËL p×ùŠHmu$ZõûÑZ]ËÂp$/Ü~«Ç«ÈRå’p#‘H4RßÂÁÖçÏ÷ú¸yø2µ–Ê*BÛS•Õ ]²?•âPñdžybÌÇ/;LÖZ«øL™Î&“ùáˆèé“=~ØqÅ‘ÑÛÝÞÆ}p"lW]nœ}E89JîÌÆ,|$ sXp7˜O&S­ ¢ÏÇG¨æ“äóÝj±g/cqje¦$*" žïÛ†w¨ÞgÈ¿Á¾ZËlxªST‹¦¨¢¡!«4V4²Œ|ºøbü§)ùÞv„ƒçvSf­©2lÁìñQ«Äìm(™më´±}5Ù#y®õÒ€¢Á—ÈG´=ï;Ô{‹æý y=ó(Ú¹6^tC6ttÙ+œI £l¢ôœÇ­å#Ù]ñ,­pÿ\F0IXQ{œRûˆ}åÉk‹æúRæ`æSd?àXa8f 3EIT¤¹,HYá,Æt~Ë|ŒY`ÖÚHmS–Yÿ¯kH:××áIúbµŠyì‚€(fwl~ye²µ]ßRˆúmº/s˜žÁÓy"`JôR‰qÜN0fªä>-+Ù6X¢mq³\ʳæ¬RáÉ@0ªV,Vr$ÎÙe7ØÉ½ j[ÆBaó¡‹ûާÞÀÁ£¯Êbðßhö aÆ“‘æœÈ²÷B^¡šE{ûÝÊGÈkª½^Ðl!nP]Õ|õÑdmyÞÔÔÔÝÔð7s±Ît%¾çª „H#éBiÞZ'èjÀ†®©@g™¢d}1詎öTWS— ¾Þ ·DùU¤ÈªiôШ}›À+CÁïÜÖ ô4 M©zh•μ6R)o<öÕ;µXè:áÇÓ;é÷®–ݲ§Ò¤¼Ü¬uO‹’ྠ,j¾ã•;Ò+ê*\ÓU‘ÞŠ§CA:šþ¼¿B޽ùœPM$¬½Z^'i’ÿÚ N•w•¯«®k¾nÓù3ê–"Üþ°æaúõl@C› ¦QØ ²W•\@üF-ü¦lÁ>Áú”jž£ü!ŒÂ3L|¨Ö U߸7ô“ù† ä¤ÃԌš‡¨öœ+î RV]¸k‹,,1¶¼o‰ƒY:¿üÁó».6úÿe`ÆDþ±Ñeý=q‡ÀóÎõ£ù{ àÃäšÌC ï'÷’?“ÝÖIùZœOª<çŒë¡š«hÚ¦©O»©®—©Æ‰±åËû{ºÚZ§RSŽþäeeu¡NÆæqwæ1”Ù *ÖÔY¢n†öŽ{T_Oƒ3‘';¤;ÕCEôýÓÈÃä–¬öd~­²¦k2n” T6]Í;ŒÅ^>÷Û²»Ô_i¨.Ðs~0Eâ±H¤£½µ9Õë‰÷äVwÞ8o¾?¯˜¼¯h¾ Œª”œ¯,çÏw:¾²†zù=ç÷™±ªb`8uv´µ4&£½±^PÜë+êù„sºƒzEÑ|Qó0ù³åi¦¦>E]&?Í8;.#`ë˜.8 E5ÝDué†HR$KQZ¢}7°<†b ›¼ Öȼþ:1]:FŸ»§¨wUUDVŽ.èooµ•Ö¦ ®ÈxòprwNþ«úa²×:•o¢ênj*lHoÑ.—1íá`vt‡€Zš+"S+­Ñ‘¡þÞÎŽæ5-kŠf௳õÆc‡ŠxŒéå¶^´ÁZëhçæâ‹Æ÷kǘ®ˆô÷f—íèê®|]ýú"=òQòO` Yýµ Ûd9Œ"±P5Äçà,–ó ®Ÿ©6­‡ÙBÜ~!ϓֈ£WÃÆêÆÌâ¶fÉÚGf¾}¤î,çYzàó ´úÌž®Ù D(¥£s†AG7é•ràȲ–s¸ òEP«Ð+ 3«ˆ¤’±–xKž!mæÙŒEö˜|Ÿ°à_• x¸5 vPCÛŒ,¢Á%ívâÜ¢ g¬@E$dQ]i¨h˜oÃ]SDÏÒ9˜SÎæ\‡©<Ýô´'3×Éòí$°H ½ êª0Õ4IrI€MĬád»‹Ø¹|\2• T ¼XÒ‡Qµ=VÞŒ¸ŠÆ/Ýã ¤5Ú/>QþN9sbæyò9ˈfÀ4dÙÓX…ÝUR‹->V™ê‚yª¾4vÐ[WÙA$©º´ïxݬ®y=4‚>Œ³ £ ¬ƒ‘HwWKSCò´eš䪀 fÕxÃ[Æ]<着JÓ˜C½0lÿ^¸¬n¢‹Š¨£ÁSº+™ßÓjÏv‚‰Ël’×Ù.(Ì[–»» ö¬,Žö…)´/âÉþÈÙ •8صæÀŠbÃ_3®…+˜ëóM´+–TM™èè¬ìßÜ/ ~ÇÂÊ^®Š=þœ™±Ê©7±¼§yÃp¼ÁtåÙÒ“E2õpæ6ò¿Hã•_ ÿlýÔzr›‚s}ˆ¸MÉtKsÙ¨6»MsË8ê]Óphæ¡s`±¾.bº1ÑLIN¨ãurtùкÚ$ѺªÊHwE7W^ëòÏ"yS¯®ø·ça&òj4((²“\?‰…d0Ä OWYf—;ùäãÕ°u•hޏÝR…¯U^ènÊ_iiµ³«²‹D‹¬ùü;Èç+2«€Ïÿ…ñ„˜‚õ¨Îzš4›Ñ Wd²àG½ˆÑÑý®Y+-Jõ°ZóëÐBÑgïH´Ãr£ƒ9wqß1»FäñArµåBOUD1›ã²µ€ÙÐKšÖjŒËõB^Mݰõæü~¼m—Zh35›UÍÐqYu~}ưÅÌZË™uC³nbÕ?ê±úG+«þÑÜâªh*à×£·åøõÎÂb íñ@¼Ò—‡7eŠ4’N²–ü·eÖµ5b–® èp²¸¬?°VA“åÖ¡„Ö¡Š/Ø­ª+›Ñ4Ÿ,’ŒÁYuî­_¤/Ë< ÂÈLewתÉ1ktùð`oO×ÚîµþH·Æ[tKS€}»êJÈ‹Y’­K,Ú&WËh*8¹¬ ·¡FƒÍ©©‹Ò ù›¶fÁJ„Ù}ÓªÛsûƒ{¦ëhyÞõÎÿ×*Ìão«HÍœ’yõq~±{üÝâ›Úa ²uõsüµF)¾o…aøUk°löµæÿÇ·‰ô óîœEy#ݤ7—Õµ#ö ‚Œž+ua‹v–T">¯¡Œ×;‹ôµx<’¯¯±yêkq>[‡ü–e4Q ÎWCì)aö=`T KL }-Ñâ –˜$gß¾ûç͸ ²nϼ~:Ñ =ï)¦ÄŠg,ïÐ`wg£³n3¾Ðº™žš]½­¯ÒËè¤꫆³þ—[P É@Ê×@«Ôô" †–Uª—©”b¾|Œb=Ö|=t€qkùÈà@O·£®GÝñ|¼–Ö‹óù·ò ´Î2VRЯÏã yñy$HÏeºM—;§ëú™®ë§'{<ÞiâõfQnšîélJdT¹W¿&èØî¥ #Þ¦–ÊK<>¯§$ÈãÇ+"Û¦7¬[5¹bÖ)¡——ÕÐé¦ä èç´Ú2Qv]Ëé1:$}wI9ˆQ,Rº´ Œ0=†õ_@@ŒgJ_@ž$,†CXH_qGšéÆqd)ØÄ©äÐ@Wgr45êHTW¬eA2› O¦2Û€ö[&ÊÔ~° —#¦-w—”©x«É¤”ªˆÎeæârùx0P£2—$˜çbwÃ%úº ¥âZš8­íhºbp mªcªX¤zë[æãXÙ•§7üÞr¡fóΗ·Yf=XíÉ<Ëc­my¯ÇçñúrF€IÊ+ß4ñùü>‡èÖ9öÄßd’ Ó·ÒâÒ·s!kgj) Š­ž¾C¨º1³´Áª«*+²™ùëÊó‰qȰ5`Ã_Ü,LWäTÉaîB˜ ô[n »1<¬6L†é±ð9†'Ìt²ÎºéÊŒŒ¹xBõ¡ºŠ0-§Á˜fçˆùÇÀ%bæÏ6ÜhGÝ3æ¢:ƒhR]”ôY8D8á50‘nç9؉!‰îDWKSª1ÙVU(ô…cÏ×RE£…¹œâ˜ó“f³?‰¬^xÔw¿õ z÷ŸÚŸœ›¼ä²‰w>~^z*u½žôýuç¯K¾ýÒŸ­òÃÿõ0UÖž»®áâKè×ÄíMÎ^9|æÌdyå5s»n?{9]} U§öŒœrұϾ÷šg’«N_qÂ>ÊÇ8þ€¦;C  §‹G'eƒ¤—ħ–Ãç²áîÎ"œÙ½J:·WöX}ñ×*Yg­vvl‘²uü kxp±€{we\l¬ ©#ͤ3¢ºŠFÀ%©ÛK/¥¼œvLþ—ŠÕ—ו×FB „ :ÃoIjÌm`TxÛ>7èìÑ_œ­{„ùÍʱâ NHwóMºŒoÛëÿÉ|iY®.sòr}g)ô—¦„•¨*-죳S Cƒ}=… ?ì\‘|Üë‹Æ}˜|ÆÝeíÄ‹Šxæ`šé¹ýGß|zù=ô —|þÚ5GºNºf[ú÷©ÿ¬Ç¯\õf†/Âÿ~2ý?Ï%Ǧßÿ•{î:gä×§?òÇÄsú¼y~že¥Š<“ù9íH¶…Ú‰ƒâphJÍÔHÁ¬» !1§~²@îdKÏm'΄ö"ŸÃ8›ÃK¸Ÿ­Òo°nµÕ„éñàp1p0šÇ£©RUÕ\Õ\î¯@`:£·g^Ѻ8¬·3XuëzYμB>È‹Dóf¥(LõÁåÞç-¥’l»‰l83³ššlí”dåTY&Ëð—û#8kîp$Q éÿ€yûH”åœbå6U"Î8yÊÙÕ!cF·°2ê÷7øSQÈÛ€£”p*‰7Ø$¢r2ÙðáµôÈÛÿåꩽ½rszݺ;Å—:N8¸9ýÆ í0Ó/ Qeÿ—­¾,œ°õ_¿ü“éß>w¶pÚ{öÜyöHººeúÎ_p:qæxÅJ6ÊÕ|o–6O?ÏUŠŸø—…L4íXž;€oG¬A´À€ýÑû  Bœ´¹ wÀïyB¶‘Áü€ý y`ƒý‚>ã–…ó,Ûív‡Üå t4k¿ìë ` äá™ÇÐ(€n?Y¥çxðól#2ö£lMa/fèõ‰æo6Ì{_ì£çÆÈ)P á,“Ýb@ÛZ’,O1üÏñqµá†Šà>y³¦Qµ–ufñ©Úÿ˧‹CE{÷ò!Ø» Ë™¢‡½ÓÕŽ3]»²"\(sW{ªmÃ6Ÿ>®)ãY:œa§P6«ßbÀDh±<üæÙŸ˜ϟ㯜Ÿ?„õÏÅÁð9½”¾6ó)r-‚0ÍËsY¨èpUڮ݀ÁH€ó»”j=cyØî@,Gì‡5ýãyü@ÏéOçÖìÀ¥/‚X뵺–o“&KÑçÉá~›eÓC<ÒÀp{7Q|°CìOTþIÙ¼6Á¢6ù_ç"zˆ®©šŽ^ʲ¦ž ©éж8Zì ´a°Œ¦î&ŠNÙëÉñ:•±˜ƒüñä}K0ÈÒγå¹%Žø· ÆbÜ ÉH<»õö¥y\e==¾á0ÞI˜˜ßPSõYU ªÉÇÃãnjŒÇj«+ }Z=­| w}NfÊS”Gcßýí#/t]j:½®èK9Ojkkk“Ñzs4ïÂС?ÕU4Þ÷ùx±rÁ`ã階‚Æ3£p¼]%Ç««mHÔ6Ö5Žº-žc7‚Òc’j2@ÿlG¬ÕPS`ù†–‹XãŸj¹OËJ¶ –h[Ü,GõXpÊtis°S3ö±é“qI¦dâ8Üç#ÓMKsÁr¢™c®†‹šÚR€àzV–š²ï-O(ÈÞcò'”di3úûL†qQ!}=í©Æh&Pó¸`ÇMF ž` UmÅSŠCÍxíìò â¥è šÍå™l_+;v¤ŸþÈŸNëýÇè=Â/.3ëÜ[yÀY½yÑ+wlªYݼlÝÔ`Ù¸«§§ºÒ ú]“äØéÕïI/»QŽšrv{¨ryïzË)jsU V,©Y}«‘Ç$9|ÿ‚.Ç"’ÞAþLöY§9:©˜š2ç`ÉËb( Ð:QŠ[jª±Æ_[K*‹VÔŒd54—?xîê~8—SXÄDDÅxÀ·¢]æ‡MàÝev4³@¼¡H&dv?†/@"žÍpŽïpŽ„=p}¤>ÿvÕ/¨ËÉ÷ 18ÿrÎhÇ£O™ìýqd~‡ÝAž[Z+èqG@ ôûÜ•žÊœ½v¥ag}Š|¨mŠ6m8ÉR;æuâz¢í\›Õ©©}^w£§1§Ñ±y>’/ç”äÙ¬_·%K• dIÒ‘,Î_9É2ljË<@\^jˆè ñ‹÷õdÅÍ’€ÈdK³@ú{»;;š‡Z†±Ê!¤Ê´Ihòñw˜Ì÷˜]¸®ÐV•—§¿’yž¼óˆïƳ‘¢àZË™ŸxOn௥H´5ÛG•‰º)¿g©­1C‰†¼«œß—à\ÿ|v~q˜ß¿1o¬ÑÌÚ ‹#Jfú£#a{¦ÈN½†)™ÊKF®³I-iesg¿a¤#ƒ¨¨“(¤ûÞkh^g]!ú¦¿t_ÐÀÊÝîÆ$feÍ3Þ]x÷™yd,Op ÈÖŸ'¯ìUK L›† iê4— é¢<ØŽäÙÅ÷ÉÆT'ÞÜ€¹há¨ÆÌqúYN¢ê¥É3–Øž¾…ªH$Í‰æ¦Æ,d}(þ‡Ñð¦"^‘þ`ÇÏ'k vB°;̺öÊÅÿ˜áŠx7wwÙíÌHWœ•àû½$bü˜)¤´¥uíâF¹ÝU“‰¶©@)ìµçÚë¨/)Ç5Ž­2¯»¾ä}Ò›´ dÇ÷)FS›JðUã«éOÂÏV{È]5öÂ`5×äc¯O·ù-‹?Ö9tJ’±?J2z“õëó˜u‰½qf½y½ ……¼¶PßÜŒ±›‹jÄÈS=¼ˆCŒÂ©|ÚPWl µåqªÛöA;º^W8¼úçiÙqi2ÆÂ‰œ“- \† ª‚T×óˆoÞXó™KjæB¸@'æ ÞØçm[c[ks!Qºí[?e|lññhúœÌsèÿó®LçÝ€2ø/”„o¥?ð‡A8Í[ –˰Ca ßxÉ·„ó‹l°ž³Û7Ý ¨1#g ÷M±úêÊ`™;éIæY88Ævã'ܯg;Žá¶ýz’4‚o6Ù*ö9ð (Z»}?ÊßZOμ,ý’Û¨'#,÷ý !Ðs¶Z›–¾—r5)r®¾â"x:›s/DÇ Ç_@ +'Æ­Áþ¶–úZX‚KWiŸÐçø|ð1ùºÆ<ÁÚîŒ'Œ;/@eRÙ¼1¼1ú ƒ®X6Ò?90Y8²«2·ÖkŠÆ}–bN¸5ÖTn\|-ÔRj‹œZ¹l¸p ¬ŸÇÉ雥_r¿ƒ¼±¾DoØïµŒÜX\çmI.ª"ú\/2æD¶Vl®¹ªž±ø4á5ãVÿÄÀÄü Ye~GŒ4%GÎk•¤ýhà¡¢1¤§Ã˜›­ 4ÀŒ›ÍXzý°ÈþÑþ#CóFÌŽ*ïIZŸ9\´÷ Q%|ß:ÎÞ ô/°÷`qHßãøÜ‰cymª¯ÂÚz­®Â;Ÿ¼œ†<ßd6cEÎpr`r»i'ÞbLRoÕ”îZøýroPÞ¼7¨ÓøTÄ›(‰ùáÿ(é @V•hÅ@[²%IVòÍéo{i<>ÊrC2<濞ŠÚÒñÈe@Ìgé]óç¥å+Uü>¸3ýéܺlX}qZeRx>„ÅÞ“ø¼öÍë(ùv†åÑç¯_ άèÕ+ ïÓEðž"¥“øN—÷æ*j õ¸kkØkGÌË¿ÏÈ|—íçºBš+äË`ö‡’Í*ÔT.)Z š™}î´„ÐZg‘isù1œ÷æÙD^ò5[ÝmÛ…/À¬> v¡&:va3Ú… ÀæÓ2ÖÏå¦]aèW'tš6 &b°á¾ãö³ºçuÑ1þ£ Ed“-lüq½åßa­-Xë¿Ûöß3\§|øyòÛü·•À;7ü ¬ÀÜbÛxp)lÀ&Û”T­ô^d­Xœ§v/Û,Ù‰™‹¼½ÊzÏ3üòƒÞJ}€“ßN†óbÞ¼{YÌ[úò¿,Zs:óò‰#-ÔÅpÒÁ|ê<.ÉåÁº¿öM‘—ݹ•â…—‹E´åíêàbÝÄåq»¢ÜZ{d¸·»½µÔ­“ϱÇ~ËxkSo­Hÿ¨ø~fˆ`(Nè ,`uwá˜:µktgƒHñæÅt_¢=愱›¢•©.$$4.a-_‡½¹+ÏòÝÙ¬Hÿ+¬êfIŠ5ìÉ›¯ªY·´h]`¡e \æ„hÑ`éR]¬¶¼Öhö©ÆqØrkÖùÖ¹Âᡞ'Oî]Œ½Q>n·è ̻•6šl76Ùž”—®¦ˆÓš™â<çòM³¼<‹XeÇ峌ÏþdÈh]Ôírvr˜¸_Ü»‹ØÅ3×|ÔåòL§8­Ã‡0ã–³¾,ÜÛ=Üׂ„kNÏõt§ÆšÇJq­ŸŸ‡?eçõ² Úì†Ùç…ßãd,Xó`µz\ž%A#òV-–xý>oi˜9a’ð¸×¬·– ÷tµ·6¥JE[çú-“­›­L¤õ–Äž †îœÓCÄ1‡ÙSKÑ=–ž4§—K“z„ô ÀBú8b¬êé ˆ¬bºo^Ošæñ„4˜[5Uí­É†ªÞšÞ,]º]~ÉŽt×<9ÍtX:l(¨[@°9]æ²íîÒ²ÚKQ4ºÖ,ñ‰ë8ÿx@Pav-Mâσõÿ¨{ðHŽ"Q8³Î®êK}©%µ®V«uµî{t–¤Ñ\šK£¹dÍxÆ×XÖŒmlÀ6ØÆs˜û4Ç.^ƒŒíÁ^›ësíîãx û`—åzàeööÛÓz™UÕÕÕÕáí÷~0’º3####"#2##´ƒÐR nRM747¢èêlo/QÕÜ~.CkKO;ìh:TÌ»H%ÛåBŒSY%tÝú£NnÈxƒ³Íóa Hˆq ¨Z?¶–œÉsè‚#‡ßûVè( N(²,‡ä øV½I„ñ?‹uàrf½pi12~*°÷ׂ´†µy‹`±X¹¡çÇ<&Ûç“ç,úJ*Öd”DyÅG… ¥•¢¹a¢žÊx$ì×åfÐÓœsü&??uÀ}b}à6u2¾·êYàZØ¡)?kÆþñûGÿ'×÷C¬b$`¡*qÙ9-»öý¿ÏrôýÔú-@›.£Ç K ÅóÁ‚v0—¸DÔZ³¾%ÂkPîÄšv® JVGÀ«‰¥3±%ˆ¹ñ1n½@³ÝûèE•Í=Õ‰L*\ÙÜU•Ȧ*ä¦?Öt7%"uÙDMO~¶p¼é—¹/x=ràM¢F¸hŽX é›bmÙ=Bª0Ç_æï[ÿ8Yà÷ÕÀJ?]å/J `RÎïØÇf6î³ùeg¶?ÏZnz^¾9Äßd‹~»ú,m6_Ȱèi*K…W üQ*¼)jwµq~]ˆ‡O£œH²¸ Œ)‰cU[¹XZÌáR¦ ‰'‰ SVø¶¸ ŽßT‹žò†xÕr`tÕÚfñ‚®ì¥†ÂmVr"ÿ µ^'ì5 Ïä¿ »=àouž3ë‰beD¼?1kˆ±5øI‰nzt}ÜÒM’@g±Î¨%À¾¬nj8ïåñÓm<Ö×'y^qURD˜5êç„*Šœˆ…«*ª,ÅësðÖ§ÜzA¾ŸŠ˜ÏS¾–1¬(RJÏ:ókºêõý²Q¡È°oø5ŸU¢\ƒÀ¸‹ùï /çô\,ÐãìúÀ³GÈ ~ßÜh+Úu1„'ya/}ë\ºXŽWäÚšÊD, øT¹AiàÚØ’)õ Îyc¼ëú§É—mÙºœ¿ÀÎA”ª«Xââ³îc˜ò)Z¬=}æ&% k…†EØÄd¹§ “%bæÎ¥amèõ_1]P8kŠ˜wU?+~ +´R뮤v/"H e¦°HÙ߃>–Š5ÂòìbBæùˆþæÿ&ǹCÄö®6菉£ðñ0åUšÑÍoTTA”`‰­ññe>¦—=úXI‘D–\RÈ©2­¶’†2@çÏæ‹”3VÉrC}Ue´ÂÔÃ>¦‡×¿^f“ù§¹=~ö«ä³E4Ìh‡¶O|NUI²þ ÎsZFD,¹Šm@­¸´Mˆ+HrA1ÛRœÒrw´Ø­DÖŽr$ˆµª­­mªmâ×HVîYÌU#tq»XaÅxýï ™¿;å+Ñ .ñi']Ì«[ü(¤“$ÎÛ›>Þ¯„ë7èhôlÔ¯ëXFÊ¢U«oji1³ºå%hÊ æýy1\{Átþ®õ¯`¶Ž²æ«Ë>]ÿ™'#ÿÀŠXy,¬Ô* Ü£úÉ߸p{yÓm÷­ÿFº‚·Ý‡\£<ç\äû„¬0Ùx\ÅÒų)ø‰6šñ³üf,‹5ªÈõX@ˆ²jž¸²qXŠœ•]þëH$‘Îf~'÷ûõ߈78jÅxN‡ÿxŒÜÀÑk©¤àðÌÂ6ÿì¨ÛZq†6.Ø‹lˆ‰LÆùµ‰ŒáÀó ¤'žq^çȽ8‘E7H«ÊŽö8ŽdÖ¬¦l¬ga¬Zr-ç`KáOÙ¡³®(軳÷À–çÅŠqÃYJ`rIòª»™•$‰…âs-©ÅáñНƒŠ˜ãŽç…âïB1WÔ4͌зo?³ý©È“ñÔßÞß±§5$Db‘þ޶m#mzü¾ á‡ÂÖsŸ¹óÃßûE te(nØ~ýÒÇî´òØœ–3v›8§$yðÖpRÄÎc㣊  Ip¯É”K${!cFP6ZaeiKÍëÆ†\{Cgcg$’mͦ5Ó_9ÌrÕgìœR€ ú OP¼¿âìïÈ—ç9Av?†• ²Ô§ $ÁŽl£¾5o è»0¼2N¸ŸpÑèaòu™ãgeJÁLL^PÚZ›MÈ<«…ZL{>óo¡ËÎÚí†ð0ÚM ‡× ^Icl[ ‚™ºÏE>Ö½Ó ô°Â÷TQëi’±í¦9*f°Ú¶š¦'‡ëZ"ômñŽÚwç;›³É¦]¼'Òó? 65Ïí:sï#ßà™“~IÐÿ¬8÷7Z}õ¹·Æ#‚?;w¿œyöè5œžb•ƒž NÏõ+x¬ ¥˜g"0õG𠑪ªV¨a¼zWLøW¬räaJ˜üû¯À¿‡9ãÖ£o ûËÀä7g%£ª+4à¹—ŠÆ\6‚>5Qã¾8Ž‹6 ¡|2ƈwí`iS4Úl1.„›3Ý4c¹°_zgFÙñÈvZÉ?3B×£çöïš¶5?þNaÙ”ÏKæ<ò9•íˆ%óaÚ#"ò0HËÿf0øžhÁ€]ãÉõ¥õ³&,ðÂÁÈ—ÀZvÃtTÞµãÛ9¯1¼d°±ÓhÝâëÖCnÚ±cr9žËâ|9U³©þÕ#?c¼òÎwÊMÏ.Û9µ(«’ºFEnæú•àRá9H1–hÀ¬yLªbùGÖ#ÿ³#&#Á̘ˆšý˜¥½1?+joZÐÞq´pAgKèyvK°ÐùǸqZÄqªkÍTÔ<þÿ½ùÈ{ä[Hö¤÷2zŸïZôQDò¦Çu¶1qc®£ –0Ä ¬LØ ?{}Èv£nT­Ç'G6î£àA¥¹Žö¶ÖLº:I£4:Ø¢¡ß@N¢¼…Óå4jÈ*®g«$ØõXi{Ü,Wqž9söéÊáH»ÅO–þ\þOãºT™9€#\Ÿšp¬"òD^óÙÜÜÜÕÜÙÊ ³º$&Ü;]pŸÞp{.~øœƒ(òJж6¯áéüƒ…yZðè3t„0 ]~¾ERcCds?{ɳ|îÇfµŸoŽFoe ep¼e „‰.·°$r]U…µH›#Ét2Âëq›0ïvÁ|”<Ãab=G„Ëê9Yµáb%G.ʃÙä€yÌó“äŸÀŽÞj̘xâ£ê}~V)¯˜pu'¾ÕUƒýÝ]í­xS5R= ` Dr lg9}c|™þ%Ðwåq>Wáµ*•E u‘ï„lL)ûkøä*g´6ÂÕUà±a­C6¿†Âü¬±¡Œg¢|Ðb¬›Å»œ.O¹p~€ü–n%}F7ïgiX¿•(kÅ ª«ÌB&˜wº`>M¿Ìó2š´¶ÙÕ °OŠóë1°'Üôý~‰Š»jéÌ\ºÝ„ì| ì„“ 7a>>˹x˜*Îá)Á§ÏñX@×2Aj8Gû+“`GŒ€ºg&Ö_ʤ0ë"]NVú…ý‚(Üø{—µþsgÊËÞÿWWšù|õøó©DkD|âÙVDʼn4…žcÛ›Û¥¼¢¤h xÅ”¬DŒ<¦µ*3 r:ÌÒÅãë¿Qoä0;ÌÿÎöé?`Y®1½™ ª``.ó !þ¡ÚÞL¥vž‚ˆÊ¢ÀÈ’‚­YÁ8luÕj Z¦£xBÆû¶a >æþ»òP«Ì=ã.æ]Uç£SòõçÈM8¾êKoYõ%ÛÌIZ§b!V-ìXÞƒ|yY:Ð*òÑz °ÄªªÆF{»«Ú«Ú[r~Æ[­ÿVN:ò“¥ø)­¾W¨$ƒÍ&SÙÎC·“8‚Õ™ëëÉ v¶µµµªõVn'O˜Ó~®¯aÑ$Ù¼Yð‚™Ëås­ dCÇ»]ðÎoò\u},)§å`–ƒikf­w‡ æ1ÌO̳dƘ²K­êTÏFX)3,f6162Ô×›ÛÕ¹ ‡ðÓá”kŒ'ȯa ´.­“yoÐ--mø_«™ÁªtÁz’ü/€…ùè@Ša©–½§G8f>:ç5.8Ÿ¢)^#ØÒp>ÊÓD{Ïytdhæ;Ö9†€µâù¾Éûóä5¶¦úÄ g<4”Ë M MŽÚ  78aó÷,_ŠëuO8ØÑºë °~ì6PÀé—ùw®œ¼žßÑ5C‚òâÖ']’¨ œ™ý4ShÆŽ!½FfGÀÅÝ­0ù0y_®/k2ùŸ6Ç‹òú™s„ýáZ¶?xôv¦ìeLVÄo+^?úmú$øæ-ªž¶ØÎ‚ò­ÛYþ‰¬ng‹ÚÄ]mœ_ngs(m 潓Á<¼s€ªådÎ8 zÚžÄ^EÞ q”= wÂÇüÝgïzØ`Qn<Â…g1žz¦¥ªÅn+Gÿoåש~hÑNÆÈV̹mô ª‚…>ðå#áéu$u#7>>¾u|.é@U×¢ƒ÷I<Øå7&7…„Dó(M ¢;+YVUà<ƒ’“=Go¹çìe¯xú®W=yî¯xúu;Ÿè¿ø®C§ÏÞ¾~¼~ÝyÉè¿ýGãP$öÒHC½ð¿^ò7¼îäÀá÷}ûU¥Õ_»îÈûþáöËî½vêȽ?½û²]7yøC?[œYÊnÿFXSûúx¢@“¤°i²Í˜›î|Œ&Ž >’Ï“&9M&ÆggÆ·NlµˆRùBˆÒ_Ù@Õí¡›"Jø}p‰%í¿ñw÷䧪K©ªÁêÏ\8Q^ˆ¿¾¶F¬û†ƒ&‹&MÑÏš™Ç©¦¢ºÖÜRÈ,È?U ŸF=ÛÆ=Úº›dCWMG£SMž‚Ð|ãhÅ\ÿšŽ9ˆ6ÝA¢hØuÕÝU§š²A_VâÊktéÔf‡gy7ІwöÝpü?ih&öá¥Å½»+3&+bú¯ fÅòY7ÅšþMæÜ›fÏ—aÐÚSùSìÌä ùÞú'˜½õòŸä˜±lÙpš¤ñ\錢A|ÉWD×}Kx÷õÞxÇÇÜ¿oaç¶­ÆôØòø²ií…Ò9k,¶ÿºþúý`“«dÊߌ‰¶U>|+>Š_ÚÓN}æjÚ©e2û—µS1hþ)f÷u4àûX¢¬'ÀaÇ[†.P‰ˆ”Jv­v˜‹OT}Ë…º ],Ë0'ixò…CFðà«W£DÂÞ––íd%2à‘i%[çfg&ÇúÚ[“‰`@•I'íÔ13ŸYØbhpx¤[´9š%bWCb"Îî4¨‹ÁѾ£õ‡»wíŸyÉc·ÌZÜ{ñGöºh¦6’mˆ×FýôÍnf>÷;úŸ¼öwéÊÚ¹ÑÑK[ÔcïýÚË-v}å_æ÷·7 ͹ª†¬ª©âãn~òòòv+]'ÿ¼Î²Žƒ¨‹èm\Î êÌošž*¸Cz}Y{[¾_À÷ó/2 ”3G,0ºÉFvF«Ù®nÜrÙˆvæ†ÁDïéÎvŽ–Úèj‡K˜OÂý òUÌmæÅ[>r`1n¹)æ;zoøÌ¯²áï1vøeÝ¿lûYóùY{÷ì^(õµ‚žÇ.Ù{š>å]ÃmKÛÀêþOÓO£®`øÿ®(«ûFp™ßey6ì³.؃Lþ5Ó…ŸZ?CÞÉ#y†‰$«²¤¢§ƒ,zš*¾,˜-ç LïZ•Ö<ú–á—ºap½÷-ìÚ¶ufz|lèàðAÛ |põeÖø‡ü#9d°×3d¯'>Òò/©ß_éÎÜé«.»deyq_éú†‹ùö×øÒíä6–—¤bý˜#¾žù ;4$`¸«axKY‚µùÌ#~²Ç:”—ªÑ‘á¡‚ˆ£TÙ¸Ýå^Wz1y£í£_Ç£¸r°®(¸Ü6XLÀ/&Û16Ä-’c¸Ù«¦ñsÍõ3Ÿ²Ï—2ãùþÇúWɯ力€g±VÜEŸUñ„ †=ÜàÛ`ÍÌPÞ£Œµµ ¸-×h¶Ú‚Ã%Èû¼›1‹K«ln+l§@ÿôø‘#F/eÇVí7c ??o{|M0ýy^{E•oât·´³<–;Wˆozã €ÞUÚGZRY•RnÅPùôƒ >AFY|â©Mô4úÜÀYÔ}¤|ŸeöŸËåæ·NMŒÛ"ºÉ»_)³V“ùO󸱇¾J?Á¦i2r ~úx<®[ g UfôÔ|‚¢Ø•®]O ã©ÔÙ¯Ìòm~Æ6‹u°8k*CS³ú­kÁÓð6eeEÚh•ÛÀ‹Á³ÃPbûÝsjM|à,¿™.h˜‡uÀ§Ê¾2íp:]¹© б[»Øñ›¯ÞÊ ÉøtŸ‡Nqä„$0ô!˜Ù¸cf]ª)œ'u5Sа߽%Y¨[µ%z“½š=zY•ÖÍn‡Z…àç >.Éešãb'ûA’ç&'úlYö›²ü7eh4•ÿk.Ëý*y¦xíq²}šc5¾2\HtÐ5²·Ä° RͱŒ¬­²¤R3#¡‹6Mð6Áb·Z9.ýIî(ëÉýFÕü|Wn~a~a×'ËøÑ'Xÿ9ãÃÅÿÓù›Ö¿BŽsôÏ{&½©ãhïsæõïyŽoä?\¿É1,^²y¢ÐXÛXÏã·–žsÿ™îEÖ‘ù]ßž™×C££“ˆ`QXÁçV.þü¤‚ÒŠšŠêX„†i(5߀üYáð»¯5œ[½SöWà‡ÌÓšü€Ž$ÖÌEÐ>7èT §šS™Æzçº'ÊǸÓ5ÆÓ4Ík á Å|–e@ð¹ötƒ8 xˆ°÷¢$÷`˜:WPé æ 7KÃ*Xb|QÖI Ïñ²f±!±t¬±†åˆ¾ÑH¤-íå¨] –Å›eF²¬˜¯Y6CÃôúÓqeÿükž~å'Î\ñ\ýJê’ÈÉê/î»é@îÖ;ÄÏÓKûݾïʇ_µ-Çïü§ºó˽o˜_»Ù¦Ã7Šé OÑ$݆9#ñÞ¸QØGÀu–•“ẋÇõ@ 'V²¬tòáê&Z/µulɬ§ÄNºú9]-þŠFÌÉÚØ‹J’€&ñ«« ô` ±¹ÿ¬9Ò¢±×‰Ÿ#,—%æÙ±2=àˆ#C=]X·½hÔ@¯ OÖÖlnX¾w¸Pñ|ÌðäR‚UWP›«3ÕMµ5Ð?Ù™ŽèQ‡z j%Æ+·ŠÝ4ã§ö¦!GÇæ@Ç]zúƒ§úú.º}±!NÓìN<ïOgz&ÜwÓRÇí·å?ă–[Î}Onê\zéŽ}¯ÑžXeÓ¹ÿl¡ßIÆ„ëóÎ}W—_õìO0É¿þ[ñC\Wù‘rM¦®z?ϯۀJg °f°œ„ªÈøzŸá­bzmØj >±ž™e‚ÂÖ™™™]3;·ƒ›konªKe2;³gafïì^Ò&lœB€3Þ9y`$)ÀUœp¹®qÐâiZÃ÷à àÛÕ8=ÆÁcÓÂXÖ[jþÁºƒ&x«Jv;/h&N ´oçXë¿•¦”_9ÇbçŸ[Öïê=}k ¹Ì8é¶|ͧaêø EÉ…„ÏŸwz ž»…Ê_ÄãêK‹piBg”'÷‚^׼ίµ§Ð ÔtEà ž²æ;Y‚OvÇÞZ—âe&h™vAñ˜ÜÆ`ŒÉóBÐ1µæM|ÏxòâÃÝéwòE1²û àA“?އ^':÷ ë8¿0¾å»¼öÛQ®ƒ^‚ã7s”{ oÂE7¤¢²†{…¸¨òl,›Rp1b~‚-‰D}Ö>þ°ÇFùkÃE=ó Àø¤ý†¼J*É«¬bƒ³¢)ì&ðÉUîfxÓ5J1­R¯, `ÍËfÔÌsvsRÑÌ~ò ïG(Ì÷Ö¯™ÜnVWj‘V`ެ®dýU¸®gu‡1ï%¾[³žWX "ðÌ|-SÚÐZäˆ`¾_k&Í‘ÊL¶¿_SÓb¦¸Û€£Nìç#Òh4ÿÊ·ŒÕ]Ó 5Úoο2"7½ã¹k‹*¯Ù…Ù¤·¼ƒë{×_/‘ßK’¤ýÜê€/éñAÑi@)4çS€m©¼¨R³î «'¤ª¦ª:%£ÍÑhV‹ç(«±¦´5¨UwMVGÄ/™•Õn îÊ¿ùð™áüÖ\NMzBSã×ä&VZíý/Î÷œ›¥ŸÊ‹uI  øŽw¬¯Û¼}§‹Ÿ¦Ðïè=™ JÙŠÒêd8h®¯Êùì97?ƒ|>ÍÞôxñ¯Ð’pÂßó%`£!,•øIÎhã߇JÑ ÄñÞ*Qñ¹K”¯+Pëìñ<ƒë´Õ ë†Õ‚uÁW> ßJñUR׫X!$lFV° ÒU·P³G”s—hÜ×®ÿF:vú¢¯eº¼ÅŒPƬ ÃÆ€ZˆPÙ8ä^á ׆µ)¦R͵͉x8ÎCPã«ÙîÃ4kÁµ£”p-- pùùŒ7Ü»Kàž!Ÿæ•×Óî(]u@h{[k¶.å ûX ìOì³gß¾úíÛW]w@GšàkgGk6ݘ«+ŒpŒsªdœ'Ȭ¬SÖ ï+B~| :)é†X×Íàp+Kà>I¾Ï³ö[QÌ¥h}]­ƒ ªU;‚Ã|M ÌOQЬ6+¢YãÍ„<äÎ-:j; CèZ|©þgè àü.ó‚WI]±ŽÞJÉbÞ5Û-ñ@šUŠôh»€¸Ñ tËȆ´~“ o|þ†Ç°.¡®jbù5Ìf‘Áùi‹›Íý&|ÐOçÌ3"|úÌqß ¸WЩ¡€½[»ÖòmnZÓoÓ®Ÿ¥‡Íä,ãHQ2ÿÄ\Ô&îjãüºÕØæì¹’|Í]áÇž-£<$Ë3<¸,“dJ½xdó`Ù]0 /TÜPWÄ,è¬GÉW-åŠ\s謃›ÐYjG1lqô3"èûä«ì¤¹$â¨Tu tvƘ.Õ/~Ï«_(ëaº“gRbúë•<“R‡aÃK…7ó³XŒÒaS˜DÞŽª)´/m¹lÔ£äÌÏMŽãa[vN¼¿ˆ>»vÌøÎǽ"ó Ñƒ`+{Ðd*¢QaèñX•ÏG£ZÜÛs`#ý…Qy»Î5˜¢‰’ÊŒu¥Þ’Å1ø1¥{ÿyöù~ám0Æ5\z3VtžfÅÜy c³š·A³e£±j½¦¡>ÕVÛV´Ù˜8å¿ï¡W±âëž=B^Æ=™V{£w“w×GYòQóe³Ë$”#eW¤¿·3Çßs¦†j‡¼xl<Ü” ƒÑ8iŒY¶ x`ç¹ ËÎóTÞ<ë!¿ÿaFÈ}lý y_(‰ ÙQnA‡ ŠÖ©7èWèXÚeÙhia»!ÆÅ öõä:²[[¶º%;\,_úîç„—2jÑìÎ/—0•H5YüsÎôê’ãœ$ÇõêçUäÜ’™+V€"7­ç1>Ÿ¬;‹>¯ Ç¥üa‘‹«LànÖÒŒ9¢ƒ•ƒ^·Ï$—ø;Ðï¦Å¾•{Ká•Å>0xØPöƒ3ócÉñqÔ»uÜ«uIÂ{Ö„3‘Lå;øÅD C-RÈÒI"b°BifÔAßÉ\œ§9EpàŒyÂÛ(ŠŒI|9#Ø4®t­ß“Sg1S¹¥/D‚ç±ËE‚1¹À†q§KG3J#5‘êÊ8­ ‰H¥}¯ÈáÞí‚ûh–EZ&Ý÷• ºê†NiøÔ±ˆ7ì;‹`£þVyžGj”)M7$&H~gÄàqºaØ&Â3³ÉÙW‰Îþf_~9KˆÏŒ ™E\´_¢´vJ?ç'Òwxf„ûÁ³·1ÄýŸfÌ9¥ÂÐDU#ûŠ04më@@@ãÝËÃz42ë8&²lÄjSc[° |™yvÍãl!Ï»¨\ËÊ¥*¹bÙby)ÀïÅíÑý®É°7f–õMÕGÿò}šY}h+"ÓѹL£{ÃÆŒ þ"‚,uÈ{³ÆÄø–Ì9—›ïœ·i,¢Í%.Ú<¾BÈ_¹hScêÊü˜Ô¹ºšÇäÒüxFÀ€ú2 ¬W6­xmµ©}{æçÆ·ôõØ3@“I4ñ‘jÒ¸_gø«çFÙ!5i<~Y$ÅKè’˜¬Wi‰¤jº:sÍéšöT{Gk£/šKgšZ¦iH4kãH˜ï­J‰uSþ`ÃÄýÇ2UB´åÈ%/3®ÜÙÚsèåoàØìmç¾2xùòžº¶doòÄÇ^µó†Î­gªÿ»±º«­{ùŽC‡?ü¶›ŽöÅjžÿÖøÚ[Ó²¸øúO^öïÒ[Ì¡Þ2úÔ*!g]ëWÏRó`T>èŠç,^Â\Ìžcö)×Úè(Û°°˜íŬF¶œgÏ•NÃ^P;þšÏéA—^ü4hnB>é!±˜»A0ºÊçcnÒùx³LŸ*ûVìŽå84W¾e>ݹ¬[Ÿšsúgå÷Î9‘Œ¢ä_3ßOo#?õÐùƒ°ñõ5¢ÉÚÚFÓtë4êG7Ù·Dy¤Œag×IP¬uŒº……ÚÔÂÁ…¥ý{KHä6'[_ñGœÀ­H‹‹<æž,Œ$/KÊ…ƒ\$¥ÁÚ”=¤bé¹ü‡ cZrBŸyŒ Î(9í©)œ¹¨3Êi g;е©-#"øl~ø¢üºb§ß¢Óðó§üè&†èœ÷Æí ó.ìj°£±ÅR¦¹ÌPç¼¥Z¥Ï5ïü^Ï…-9nâ!ê×û6dKêͯU¯£÷ŽoÔ»lÇ‚'~]Õýê*R_×NÚÄ÷ÝïÓW\‹ðЃ%]ýTWÏß7ÊËK†/ZüÍÏ“—•ôÝšYÙW †ŠéÎNgéš| !éNHe€3ÚŸqiÀ­€SkS˜OmÛüô¤‹Sû³r›‹O¿òùœ>@ýÁ"¿¯‘[þ ô «¶—f^JPV‘`ÐI›mÆHPÅZ¿«GPýÂ)/0e SÔ™Q%T¬³2h‘^´|ôðWª$›2¶ £Qloÿà›ÖÄß•ñŒ{ÊyƺӾ(¼0 Óééþ–ë[[YoYw˜'….x 6ëÌ´‹üNÿP¹ÔŠߘ¹ì&äƒÁ Õ4ÁÐSÞÞ†ßCK÷ñ¾ÐTSPËãÆpûűÖÈùû¬Ô‚¶NÏÌàšóç0¾H¨x¯rùÇô‡gÊÎ_S æ¯æl“óǾÐT‡¹€¨Ûs9¶ÑüÏßÇsþ³lþÀí»fwþïuÍÿÇgˆðÓJ¡~m…üV*ÿJñ{X©Þ½âåzyv(ìO“$àWü°Ï€ò~ý$ш? ùA-‡¼0èZ€/ o.›šD¹î8¥)'Ì"ÿÓhü³ ¬8mAAÆò+ú)`-Ø|N•+Ƙ»pŒMƒElʶ«42êò‘ƒv/ÌÏy°j¸àO•ìY?9ÿzÈé–ÕC=°¡ZТs…7…š€F~Õ¯¨«A*Ã0.°;£M¸X„Û¹_9røÀ~ظ¶Íœœ=éAŸ(§ç¾õÔªµoýÓ¢ÞÍèï9¢ë,u~D³,ë8h#õ]ÔÏ,ÛÅ¥½œÝ¬#N|]é1ñ`ñy’ûŒì‘eB>nT öÁx¥Ù°‘ °a£¡'—d»˜¡ ÷vØpa_+×ÒhñlT8È>x¾ƒlÙäñó¬s’ì#_ò8ïÄ#+Å'+Ëxxí[ $D瘓¿aó¡.«®|ç9Œ6ªS5{woì/:A löUæäxùñJ¹¨yåfWÏ bèÜ9ú‡xº#Ù6ÈÿÍ<žîLÆÒU!ø»²mèü‡¯òg¯ùEU®1:ÒZÕ™Žmi¯êhŒ†«›âì/"™ë´ß\§òZUj·Hâ`ŒmpúеQÛÒc#‘ª™špÑY¿0:ê¼_ØÉ5ý`<ÓU]Ý™ŽQ««»›â› ç×®ùNuö誮éjŠÇ›ºœrë:##_ XÏÐûÔÎ~´ÁI¿G¯“þÚTÑÑ%¿(×°>!¤÷62@&È F$Keq,‡9°¢(bá‘ÝÁá´•8ifJqàÂÛPgÌjg᜚ØÒ<ÖÜÚèK8²UÅW˜#ÝÔZϬXÇêÕÅ“´¥ —5Æ–˜ËÈàµá¶øøc]}|9“]ÉS¡lrâm±Lô\]ýšTqQ?>xùE»ëšòýis“ŠW?ßu÷I¾”¢ðÖxõ×e¿ïÜg/ÞHa9ó±µMœ».J0ÏÑü=ÒËÄ‘>ƒÇŒXõ)£uñ@ý™‘8vkë’İÈ@±ò&F-ˆy$Ÿ}z/HXÆö. F<êÝSb§÷‚Œù(K[²ªž¬¹]Ö| bKÿÌÀLGûP6©Ð+sÙU©èÀLö˜¦¹[˜¦9(¾±$‰¯ªÖÒ[êr=!(ÛšguTpÎ:5M¥ç—¦Û¾ºw9¨Ü‰ô-]=1ôU‡Î'FN™¹Òeõ –Þ¡Ô¢ã3ØèŽmæJïƒdeÍîSVau”mèuiÂ©±ÑþÞÜlçl¹û ÿŸÞQþMë¿‘Ìïù›ÿ~Ï/ÔÂÏq#03ÛgºZÀè< …I8DVícgQVYBb**t%JÃáÐ\$ ¨*YÒ*€‘qk·J?'$+[åžŠØæˆß_jÝ4<À4P°ûĉ¥=±zâÊË/=zäÀÅKïY˜ß:=9:<Ð×ÝÕšÅ(“Š_§sÂ\ _ŽH&mcd–ì'ÇÉ%4gŒ1 gc•ƒžÓå‘%€ë" …› åÎ S–Ž7ëÂiØq`‘’KNž¸øÈáÅãŽïÞµunjbd¨¿·«³¥¹¾¶2ꙥ³Q,Mf¦ â9æA50¥ÖÒÊþE `š[…z(+¡œ¢ÔÎJ?Bá;uJlÁxéƒÆ‹öwM¼ãŸ{ýˆ}méŠX¨kèÔÕ7Ì]û‰Žôŧ¯|ãæßÿÇÇÕðáKfŽÏ÷WFª•ê•O_sýk’ÓOå_yÇ×Ý5zåQþzvmÿpàMŸzqçásoc¶…øã¬q¤xõÀ Xû¾{bµéÚXdh`goòºk§TßpÃÜÍ'Æå-×ßÐØÚ íÝsâÐÁ‹ÏýJÊlÙÓ»m®yûéíôG¹ý×nS›ú&3óô‘k,ÙÂe´ ʦæ%“Cå,l.y¨,çLo¶sY~ÙµÎ%!—„ÎÃ%-(gç“° ëmÖ"憑ߌ4ZDýÕkÚaÛàë·rMêOK‚,URAfY4Ì¿D™eÑhæ‘ã`úࣶМ0‰AdÂ’ËÄñ·Þ­|ü ¶µ^—%kS”NŒõ÷f3©\mŽÖÐ궬‡¿¾®_ðrAÈ{7‰m+)§3¯šãêlmbk–lq]bxW“ÒMÞnâÚ(v Wó/®Í°G°5ےɶd[|É܈uÜdúÕ•I5Ä ÁsA+LLDiÜ@îÞ:¿p×§_Òµ«ñFt«o¬Q¢z *’?R»4:»<™‹ÇBÇŽ_6¶ýºÎ÷ /N÷f¸3œ‰o}ÙEƒ·®~ðÆï}ìòpè£ñêõšì¬OÏÝßÒÞÒÝÝ™‹¶e¿¯¶×ô$„¬$oåÿWh‹0ó‹ïqÈt?—i‚5aÆd%Ñ¢²¤ª<ˆºú ,ðú†ÊVf­ ¢é‚® «DñeÕá`‰¢¾„PB¸51KÌQ?èÛÂe眻»Ÿ‚òU =˜²}ÿi4îÚ%Ð]K»ìÛ³uvl ˜Aø®»ªR•éNag°†ËÇàúo%ÂmŠA¤Ãˆã,êVß1 Â'[gQhïð÷N!{sM²}|KŸ`:—­ž­|”»—æÍ΀X<ÕË¡«¶«ºŠe|23 ~ù—à6Ï¿r—v­¤<^nûfcÜŠí†%žñzaÉlœÁu‰áYM:Á.»ÝðWn’7Óž9D6&]›w3OÚ¥j(éíéî{¦3ÕY•ŒGu©¦Õz8—¶ŽÒZ¬ƒØh³ýèù0I–ÀÓÁ÷ÂïÙáÙE—]=ŠFÍò‹nݶpKnî¡3ùÑÞËŽí««o¹zdèØîñöÊ#˜Qc˜=ùȾ¿xëÍ nRÏ^÷ª±Õ7üÃ`®cû%/ºqËõ7å [»ûJyKI‘wÅÞe„íÕdÆŒuÊï0 hþ:'“&XLjlaƒ=¬´µf%öjš=–Z¶’㘎–<ØbÓùû¥0³ÅfÉ3ÜÄŠ%aèa@V’KY}vyëþT•,›¬c¤iþ>M€ZøÛ—|Þ=V!éúYáy,NssÓÒ<*»8ÙŽÚ\6¢Ì ›í›ƒ,kkmô'¯Nn)µÆÜ tõÕÞÜs_âíÖX¾×ÍR^Ìô­Æ"‹ì)k¹uì#XÇÛCÔ¹¸Bå<¤ö»Æâïsqœ2V‰üâæ3?Ivý·êå,ãïß î¼[>å‘N¤Ì•¶?ò±ºÌ4ë#û/ÅÇòQq@W|˜#,i”aëgœ¡R]tžý9oÂ]ºX[1°hø"0TØî 3±cîÊúÊvãÞ.ïäw’sÙL-3 848P¢Ny¾¹qØïßÇ÷æq¤ë¸ù¾é>Xç=Æ®Øôã”%'PdQÆ24T"%æ»%ÕzêÈdÁrÿ&Æs3ÓÆØ1–ëhÉäìx8Þ?”Œ÷Í•Œƒ)²s<4Y½Ç‡ñ°ú®1å9Þû¸íl6þ«¹ÓÍ‚±F´LçN¼cUÇÈÌ2õyÌ2ñh)Q^+ êÕÔA®c×ÎÛ&ÇX–ÅñÀñµFx댠Ê%H¨" iãȯ˜]´‘´îEµ’7BÒÆñ׺=Nn‡Î·zaÝøz‡5Œó ]+CÃ6ÇC{V ¦ 㹎Ç--윙¶ðÃGÜ¢‰_Œ´‘-d½nÀ(Æ9ÉZGû}Ž7u´£¨´oéØ`;|1¬K%U‰›•–¥nÝ×Ô Yvr) v³ZšâûÆîÉÿïoÓÛïØ·¶0X w_¹vÃÜñw^9Ò4{â¦w:pãžæüçæ_º<]ñ’\¿£-mÎÿžL ÏM_uh:öäohëi‹ ìêInY{×ñ…w¼öÚƒ=¹ÙÅ™þ#7ÎêÒÍmÉh¶1N½¦ ïˆä)r7LìîâuAÆé*0µ”¶Ã2´2œÓeÆT¸{ie¸§ EŸ‹ß?±ßZ#¿ƒ‡^çÂõ ´™û‡Ù·†³ïY½W,×ád_‘Ìówã(I0¾D.…/—jATœòù_«#ÿŸ•5Ä/ÿá}-}EŸù÷2«è5çó–\ÆŽ:W±¯ÐoÑøùE°oZ>D}¶ ·ƒÍo”âo¢eûÄËôñj^.<èñYKa»0ßu”YŽc ÐÍÉ,çïeÑ}ÎaÅS0nü…û‚‡d±wÀFV¨¸SeÛk|صïÝEÈ?sy-»;±õ~Zë/7éF–°ÎìÌß½ŠÅ/lËtR™½\ÏfEžÒÏsÂF- öý»vnß:;59~hâ5í ™Ïæ-}—¿]qÈÏ?ücÉ¢û€Ûf¨ì+pœnXòd”SEFŠ^f"ƒpöŠ^FÔìÚÆÀÒ®{vïܱ}¾`ëW2ïXóÐT°¯4y¸žà\ÐXlÚæ!àojóøÇá2.é• T÷]Aýú,LÊšý¤ÆüV-ý6ºaßø}Ëu+hŸÙÒç4Å„t¾* l¤‰ÆËBp>n l¤“æ<Þ¶¼lâü@¯ˆM¡ÓøçBÇØZö±Œ¨{À ”UÀ¹ŽË.¹øØáCûös`йçÿœÇ;xð{?#äóFèRê:]œã4>—ƒþ‚"/÷l&Tf~3Ž7/¢ê/ÌÈP¨Œh5¡¶[»jõÔñ•#‡÷ïÞÅ,¿‹&.²&Xaé<´ŠíT|óò*Ërø ·76ìíéž&…óË€‡_v¾^ËFe®cùÈâ¾â5ò×›gUãù‡™Ý³•"Oðãçø.ðz©( ü"‘g€rh7”~L%ëĺƒH*^`b_µpÄ̯æØádáè´Ïj죘fŸPï^àò;P·ÍS2hÛ¡éIðL:Û[«“ŠD¶Ò­z8';jCt‹–“ðrtð&ÐQU‚:|y»zD÷þWí)87nÿ§ïøÉSc•δ¶ØýùµUg⥚qå›ÙÞN‹Ëêžï­‘¶8 TH\¬‘´þÓõ )jÞãl'û°ºR,((da×ôÄ(˜»Û[jEAì"‚$H楎¨Š(à…†|šGW;ò°h‡Á©Á©fà…l»/‘sÇJŠœPªXä&š÷9Œl£úÄe4NÒÁVuJ|hdŒnøHæf¼áùL&Ýtèí/ù§©Ë‹ÜÇÛV^‹w; ŒpG_7ôD|ð]—ŒÐVúkñê¦\µ¶<ôÞK8áb‘3ñêü¦÷4÷Ú5‘̽Ýö%‡>¯–ZdZe€R=¿DõË@à« !¡ùï 7¤3D2÷ôÛM?z;V¤)xÒ:µ-ˆR§ÚÛœžêh;hûôv§sí¿0çÏc=ÉBˆ^›ýœ@ƒçòßmHi§NÆ«üg_€¿ýØ5ôûñÊŸûÃ4éôoåéÄÍYÝ4¦€NSÛ§¶ÍÍü‰t¢<œwм :ct^üëP*~Å_†šê_ ¡þ ü<‘0÷¶ü}%þ¢u7±ÏØméüTõß_ð‡ça+yË=Ç"»Ô:O˳tp”Ñûx³Ã¦ê;^r2Öè<cÇ¢Î\Î'îâñÉ%½Ê¦°“±ý{±HÀø‰EÆà:º^!|W•×QÄu’Ÿ‘3ô²Ã˜¯¤¢€&€0kgP1ËGñ‹ ºˆ5¢Ã<#û6|V“6ÓæÄP¶2«5`íªùûÅËä[HY0v¤©ŒKõSŠEpñ]P ª@OY¹ÙB,ª‰§\ ãzD0Ûè°UÙ,«B%ß_%c­Ý>0‚áFI5$°Ð0MàÃKE×UzKÍíWÅ‚úž€>{ù»'G*„p²>Úµg¼M P½Bœ97]¸¥ RcH¨Œo?~ðŽåžØŠîhÕð‘©£·ò5;óËüÜhé8gæÖÆLð‹ÆÞÌ8`Þ/XÓƒ)‰‹š"°é±«?Ì0g}àó±yJû;;h–f‡³•Í‘¨nÅ7Œ€¾ùƒ{LÐ7gAßü+žXcr§?@}:4Ÿ°âB"è@ŒU[BáââPSMBO¢ 踔ïȰ7?(bŒÙýá v&pds€ØÉF}k ¥“ã#Ãý˜¹¾Åh5 Ä3bP2²–€Hí**‰†ÍúuAÁ¬ø)—“E™ #s10ÿfÖI:s‘H$ÆFTaEk‘DZ9¸«Òd/¸ ¸0ÃJ] ?Ë䯾´Ï×Þ2Ç•‰JA½Gõï¼ù¡«³‡j€»ª›*?þñ˜"lüTÅS,†…FÚøü¾ÒÜ"õÅx|×àEïZ×ô£À]ÂÞÚ—?ys~…~ë^cîÛiù\ë+p­Ì8üwã=;¿Ôàe,“Â?|æv\¨*4^]ù—²›=²(RBÂsÀxöÓ³%>¦ Ùeæ=[c†ÛzJë[ë[2iZGë€nÙLÒoñ( +½˜óhoú-ï ~ËÏ‘ÃõaA££À™ %KÙŸèæ'æÑC'‹4j]R)b¤³T”üwv~k.VQè(ø`ê›ï { ¯é —J;“ ûbÚ_J±pD{[Sc(F®s¯]ñ[÷Z‚®@Ob›1WXÆ (_¬ :;š› ãèö8ß”>]Â3géÜ)ŒÃ¥¼ŸI©†Rî5nëí»SA´Ë÷F~©o¨g%¥»y1²úцÑê\œAž‘onÜ›ÉvckˆâS*I0½9fç—äeÇ(šf¯ˆ»T¶òj…™¦þp+AŠlé¥\z©Kx@Ìß~j5ž´÷TÄ?r{es….ÔUmݬ¶õ}¾âÿzph!¼Ð”?}¥8ƒâ:û‡>,ÉÇþãW½:íÍùÝô“7ÓøóŸ5×Xé•ï¶i¿›¯±ˆw7÷¡V˜N„‚΋ÏZØIsA'Ú“ókb¿Ž"Î/ÐE¦±B¡à #Avä@ æñw(çóöZ6R”îÞµu–N²‹ú-£#‘Hb0›Î&+2–ìæ¿©¼Ü=rVxvU½ç`DiˆT„C+çŸLÌD+NƒÁðR„†Ã‘°µãLYsò€·(Æx hßÃGΉ%iž¢ôÐÒ¾½üdiêèôQJ%RŒW•7¶’!Ø«¤ Š%4+ B‚V¨O[ö"K8èD#Ru=¨;WÖ«™^ËF¬ÞV²uëÈL™([8WF’àØÇ˜™d}¢ØvS¦H<2ðÿ·œºDÄ÷n]Ÿ<ùª…†-5!!TÙËímQ5úQտ㦟Î,ÔD¤\ËÒ-‡ú5U˜Ñæ7UÜ=øÀà­á[›ó/½ò+ó',!Z<øêån];ðK49xxz÷ÈEï>=á×ÄhfÇéý7çÀwAšyþ³âˆ—¹¯¨ËÝvï‡Ò#¸ Üù÷¦]áPU ØWà‡f*¼” ¸OA\]Yªi"‹RÆà@ëp@deFý>A–Ãs’ 8“9°þvoa£Î°¾f?lHÔ ¢® ”é…yÈÇÆ(Å4eÓ“t …ÕìïÊáÚ¢¶‹FXì6òäYǾ{ï»Òßó}ÌŸjÇRBX9Ø+èá€u+Yc~§•|Wüq…ù±u30MBª$á0S3¡¹ ŠÕ¨MNþk¤,1+™Ë`Ôó…éqÁÑ–‚Ì[D@ÖïVðçFppq·ƒúÐÃúª=èµ98|û4Éæo3P`ût(lŸCb j´ŽƒŠZÜ·°k;Y;8~Ðc1ûç`- ªš”iBzŒ; S)?‚ö§6åÇ`|Ø ÷Àêë 5,TÞï·~T¨¢,k‹á€€‹VY[ô)«ædå™-@0• @Îßß&:ñkº¥Ć—ùmØÇ·€OÛÝÞÒX_]±þƒš–캙 öìb{VUÒÍÏU< ›m$±7Û6Þµ+â÷Þ–lŠhmõÌØ»!qνï–Û¯VðÍì¾j øâ¿Vž'ÒEÞoè5À#*Øí‚i €)ãÈÁ·Æ"K±¾:dªJ~œÄ<{·ô;I¿ûÕƒ·ŠŸÎÿ2¤P-¡¯¯¯ÿkþ^I– 5ÉŽ[ïÁè°½œjþqpyZ¨ 7R•Å3`\ʾýžhÊD 2Øÿ”¨> ºÒïg¬X®u&èó±û¥±ŽV'lDQRÊ*£YN~Í¿€D<`-ňÏfúúüïˆ BáñÅS“WŸ^[=q|ùÈÅm['wOíìïhk¨OÄ‚_\†¦O½”ÐY#䤧hÒsŒøC€‹_ÃJ’ÅH˜opÎGÐIkç…´E'¡‰__;?¨2TÛtOÊ6_||jòÅ/ºêÊË.=~ÝÅ×yÑ7ì ïëŠè‹1•iøë_¼)<ä/ÁAÓLU»!q‘.£ÖŒ6âM’¡ò]M”B`§þµS“/:}Ù%^D°ê£štx‘‹_!ÿ?›.„‹ç§CËyé°HÎÇ_Ã6gèDumÙ¿IÆ8GOŽ"ž™uAaòÚx”×ñSǯ¸ôdyºɤKŒŒ“²BN ¾Zd Ë)˜¾øú%ð'髪¯J!p}59’vìèáÅýós “ Cƒ}í­õuñh ŒúR"^/% IÒÄþ$†” ðìPüÁp2 VQ˜VRþŸç„ª¤ƒ-­-ì7Œ`¯®LfÚ›«â͉ÙW>vÍò;Vdžßò¦wíÐÑdۉɉ«öt ]ùÞË®Y¢µª‹¾&žÙè‘ñ†ãGŒÕ§í~ßÿöŽ©Ûn˜¿ngþ!þዽ»g·l™È¤zº{'³[o¹x4c\4—Уmɺ™–êTvÇÕ;¶Ý¹:Ý7©“}ùOuí8Ösh¹cÛJï‘cùÞêöXBïÞÑ1¿3=º /]ƒþÞ–ü}âG¸žÞ‚²p±©§Ÿª?ÇõA¸‹ªÒPJPTðîvŸÈŽO–yÃAè*€Ñºj½†bNš´¤ËfKðSsUØUV‰þ.é‹yÂÙ ?¨n««F|’æ[Þ\GÌRÕßG)žné›îŸ¦½´§­=ÓÚàõÜ8mNÑìÏWòßø„cÛ©O/¢{i~*;>5)4ŠÕÂ$Ý'­Ú83qETýœJ2³ÜBtŸ¾ý% öáý®‰z÷·ç¹oïž…Y÷LƒV U>××¹øà ìîå3 ,m–ŠY`éXÀ\Èý{·m-]È2ëˆ1wB÷o\àܶ.kaÇ[0|‚Á2œ4#.Ü®YËÌ2=cà<1¬sÛ¼qhæk¶a¶˜Ùóýwñ'¤›L’›óë€ù TÁl4k~ð‰ìøÄœ_‡¢ããÒÊL¨ÛjìsÍ@+3ƒDo!#C=“½“€]`Íêqp8Á ‰/mí¨*+YÃÅœ(V6¾Jñ߯õï¹õ¡«nüË­2iÜ71õ¾5”Ù¾çPßî—-ææ_õèéc¯È%åêHþÐ×LŽ4¯Îí~ÅÁ®«Ä¿þŠ£ï<=1?8Ö¦Sí;/»6;’_ñŽã¯úî_,oÜ<ÿôžô‹ÓC×ß¿ö†;¸œX´^ÚÚÀ‚-ç€ÆƒÕ@QÝ¢1–aq|ò_Cã¾^JG‡{§ú¦híF·è ø&o=ŸôÀ}Œì$KôZ~œ¡º:ˆ¾‡Jb<à)ÏÛ]ïøR+þ’}î+ù¼kØ{nQ–dQBå¦.}‚ª&çBø|]>àWY²oº„·—jÙcžÝ@ºäE³ÙB;Þ3!©ÏžcpÈ@1Æ7ÀN­î´¤÷2lBïÍÍŒïœØ ”ÞÒÖÏ´f³‘pÒ•à 'ÃdJX~ØÅñrFQ1IÉðˆtzõG ŒÚ™mžÍGîÚ?²²k<Ù§%"‘Æîù¡‘•­ÝþɽR£±u¡sþô®–™î»ôàZK¥žÍß´íºáÑÆå±¹Ó í ‰}uKúø– DâŒÖÓ–m‹¦;k¸h¤'–úohinŽ]öÖ•[ÿù/WÀ„Ê<ÿôîmô+ÓCS7|üê;o}Ç­‡%ßѽå÷ÌG`Ïü:_åøÎm‚î+Ù4í½vMØú$Ø7íMSãñî]ó Ç®i•ç”ôw̓ºkšs5kQ:ö•‡h @ø› ÜWF‰OÛ¥0ÇÝì¾9áÝu³{gÁvc§±c~Îc¢¢¹¦1ÒC¦È¬m t¢¶«yø­™u¢¶+|Rb hšoööÀZv·M[}½„à-/ׂ€7jÁÖlÀÚiŠ%ï¼rÇŠý}äZÿ®、XÐ&/Ý–Sƒç³Ïuhn:ÙÂmçÅ‹–t¯{ºŸñ Ê÷×ö#¯àµw¿GÈŒ1Þ™,QÌkh=,'êK WH䓸1;¥­-ÙLªšVѪL"5ct|žÏÁ†öuØ@èX¼X&K -¬Ú€Û[ê,°>3ŽrþÃ%py›1cÄ„êÏù PÎAæøJšrÖ…ïämÊçù›Z‚i^‹ÇÐlœ±Œ›oþÒõÿÌ?Ìð$0ï^*¨ ´ø"ôQaüîü¤pë•á§ðŠbr©*³ù°ô-…÷0huáõȪwó¢|FU”ôte›êjªãÑp(P$2Hõp.éx?’ÁtrŒ«U1ã9>á7µèÙH³ýTd ùÁ3û÷~þrãÒÉJÿÎ3;CcûºNÝW;Õ˜r>±„Ü*¿ùÜU×ߨwñŽž{›pÍõ/î9|ó¹çæo@xŒ’ÅKäe†ž„宂uì,ü‘@ôù’ˆ®¥³üt¬Ó†K@1IW4‘Åâ2£¤ÁÞÓ dð¿#åZc‰ÊšÖVJ1qukGkG{[&]¤Qlñ×"N€Ïqœ.b¸ìÐ]‚`ÛñëAŒ\gIj$*Eå×ë;ò¶úï@ý{)¡È?ÉéT€£ØpŠ«‹£J·¡ª)–W1ÿMás̆Iή×>»‹¼Š_àç4ŒëWð½¸=‚^_zðŒ ì‰C›Ý¾€O%åˆW7fÕTSÚÛÝÑÞšmj¬MUÔ ØØRܯ¾(pð«&]äúÇ#T鬕ñJÀpö¬ÈJh#…•àåƒVêªN»)ðÁ' "‘Eù¨GOÄKokϦ3ÑH…SUñÌTîìõ#1W}J?zãëÚ]éê¿”hêL&;°2C.™ìlŠ‹ßzþC¡¢Ìôòtu'Vl൰rƒÉ/p[÷"\›ËyÎuðçI7&^PË­ÊìÊÚÉ;ŠL©_“CJˆÑR1ù€¿vÁÈCÛ94WeêæD É!9¨û,h«`à>GK·O¬>‡PA¢ææS#R«Îʌ쀜ÂÒ´‚´†©˜…5¹ˆ9@ÝÄcÑ U.Lç0 ãžâù¦ ãž]Ç»¾wð³ò +rIÀ†˜*úÐ1²™å ¾&wºB§¢†F|È=®;[à™&¼¶¦2‹„>UnPÖz*gÑý¸¼ËIw’¹ý‘ü5”ßõ?Ê_'{x™*Â"›È;ñ¨„%–³zE)¢T„ÒL:U‹›¿ÕǬ4àCˆ`›¶0IX$H çêV 1"Wä0‽ {åßøJnBëùnñ[t曇ŽðuwðäSë§šé|¤ˆ¤b0·°Ây©x)œ†¸f·)žK)Y™ˆ„ýº\£Ô°ùøþoÏç¿|mÌqCüì· ‹ô[„NñF_h ²$X9ÌODþI´¤MÜÕÆùu!gB3ò²„ÉE*‰[)>]ËÄGl 0+‰©˜r̼͢t‚¤§ÊÂŒoæ&Á±ÐC o]ªª²"¬©Ž= ùä:Σó}„é¦ÃO„ü(#K9þAõ´Êã~\Ê 5—¬J¥ó+'k/¿e綾;öÒS|/MÛʤ Bæ4–éUUí¸z—¤#EúfCucí¯äx0;yºÊHEÖë!¦ð=¸ôëÐÞR¥Èežœ`Híæ ˆüÄÏBÂÔx.Eì ° ><øšŠ;¸Ÿ=–ßk¿º y8L_ÖIUZéÎdUwSÂZÅøø©Ý9M¿8¶m/ýåÍÂO«°¶TSWu5«-ÕMþëi ãÝžÿð"G|Æ•¦ÝqÁ ½z-eÝ‘ë­AÂÿ2Km £q#°ðH4«EspMÂÓ¯µÒ6Fµãk_ÅIJ£:®º·ï½½ÜÞ»ÕÚG­Þ$µv–‘4ÒÕ6£Ñ,fÓh<žÛcy¼ŒYŒÂΘ˜˜-,!!qpÌ/˜83~NBÀ1Ë÷’0I!/ ‰á‘„&8ÌH¯NÕÝ—î–á}¿äý¾?ìQw×9uêÔ©³ÔrN~㋃g¿nÉQLäh™ÖG™­ÜR +«[é´JØr³Øëò"Ñ3ú“Äx½‹É;ù³Ý÷ &ï¿„PY阶ãÇvµÁú†±K0Èäºó^ü8“ë~×çÿ¡#Le4Âã<Ø–Û0ÐÍiz¯Áø‘ço1÷G|<ðvâQ!!&ô.!7â£Ü|–HUŸå§Kîkì-G9òÖ(Ÿç.^{ÿ=×NpŸeñAÔqìÓÆ>ÈSê^…v/„p­}óAn€Ò×D£kس‚È­€¬ÉêÝpcA ”PØrHI*©IKµÎþÏE¾+ò®û¹ÿó?I·—3¥©¶®L爼çžK/žö=ø“»ë»Û’»:X Mâ›<¾åß%t@Ì”SÚedZ›9l ˜`T üäÃ>²ù{êþÉËßy‰ØÞ;7?ªñé¼›¶n%Ÿ•¾ ú‘Æul–Ê)ð­—ñ¨% Ç Ç$úæñBì&2Zæ‘àý#‚÷ ÞFtúj¨FCþ[wþʬp#j$Î>ÑN¬ðPžÝK9šãLÛì|Kü%j-‰Bûàt>õ?š ß,´ Låïó}M,wõiv¿[<Ô³4ÒŠ_¼‡­¡[èž%åï-¦=Kß+âý†îSé~0¸KÝ„”¨¿ûolÇò¨åî_‘ú=†wåÕšˆ6ulNz·§÷úê`‹Ó¾½h¦ös !á¬nÆXµ6‚û”"! KjeA ¾N¤‰ÑiÙ‚[x:ÏMJ#õˆDý„?cüHc.Ü» é\2O,^–Å:IŸ/²=­ƒ`OoSs¬T¯wé;O øÑ»Bd¡$hPÒAšá€«Ít º2}}C]ƒ¸÷Ò O¶ÐwÙFßÓÜaV{ÕA_›qË‹¶¼éŠWº’Oï2ÑÒhÚ|Px‘Í“A~ýÔ‹&7J¬©æò^”XsË%!y†“&"w·l>$¾’Ð4‹þR­F%`?’EŽ÷·h™Ì߈z5ª"d†Ð©#’+­È¡/Iaš"IÛýVï<ö ¸A‚¸á ¥-Ð`|@kO@Ú½` „%i<‹fwOŒ ’Ù 4†·ÎD‡GYù¿4ý¿Ä;<‡iœKŽôrêA¢ñç-woÝÁ…üÁÆm7üÜío¿³nâ1BúÂ’Ä7 vô®Ý×¼ûÑ}Î v YÝÛ2Áµ¥õê/¼ñÞ7Ýû¦ÏüÎfÜâQLp—vkê:òšåk/°¿û×Þ°²‚¹9¸5)¼èûšDO1&ÆÒ˜V„E]²È³É¯°ù+2;ª©ƒ¹3Hòù¤ëý´Ü¯ÊQ€4Tϸ˜äS_CòI·z´#RûÄ´ šõg•gò¦5ƒm!òë$š,扌µŸ6äàý4uߤ:*—Å(Ǔșf¬Õù~ÐÄ÷ÂѾ÷B–º·f³„ÿK2Vç÷¶,²ûÑ–Íçhý¡'3n<Ÿ=—ýJªáÓâg~¿€¿ ßÉ_ûá‹›w¤¸Û¿`æ8=ë!.Ü_2½F÷BnWï¹ôCí‰+=Þd|Ë"A$‰ViUW£úA'ÝR1ñ£´6ªÌÛD×”Ts3ÆÍ…æ|vnÂM¹4Q´ì®ÐóYw›èyœæãþ€*´s’ aV汕R% déhYùMTi—"Ôio5_ŸpieT…Çô~EÔå~”Á¸ÜÕYjmv¥û¸…n=Ab¯oê\l¦÷ÝS*›H „ÃÙAËu†¬y”ÝÛ™ž²$I£€.­•.[C ­é_Sš[Èœ ˆ–œ;Z›GZF4„U¸þ³Lß›æîóž~~@çB+ä+p"'Ð7V1šÓs¡ÑÅj4Å[\e.omâ!rDæ€þŽbsgK§ëÜÙ×À'ÐÇ\ÖYd¨Ùæ=ÖYdhm<×ÐÓÞÖ\h)¸ÒsÙFÏÓ¸‹àºÁBO‹©:º-æ ë^tÄ`3¤½M£Á¯Í僎¹dw{ì¸M QÍ'ëÚ¯¹ ¥ «ÃJm·|”†FÔŠ®»"šzmd7.޹ºUûÑsĵ¶47ƒMú˲šÖv^ÊX¼ëD Ñ̇¿t›r~w®D'÷ßxÝá¦Ñ \@ù~öÍoÕ/fÜ>xÝ›V6“Ë>~Ô¹&Ô|Œ/W‚ÀÊv²ð8]ÀÔTŒ¥åaÓÚ¸i)­UUÄ{.ZUØÙs›ùÖM}èiu„ðVjÐNsÔ‚¾’£u÷1PÒp…]Ï™ÓK:°ZïE–pW„TE ôôS‹Æ{õÆ´™$™Ê&º¬±Û@}¨/Ι9vÈ£Às¿¶WœWËšjùp †‘(éíš™'lÛ5 ¾¤±iäÔntYgÜ€àº}¯ÛL¢/¨h¤5Sš™èë2Å[2‹·Æ¶~ ù4_â%U?> î§2­£±»*ÚŽ=ð‚Ë„Ü*vin¾XHÄ9\î,ô{›ãùD>èÇ1.hÔr2þ€ÿGàaµÒ@S3«ÛŠn}h…ÞBOW‡WÉ| S¡·!¸«ÓMœ}í¸® €ç/ ª;õþ$ÚŸ«‰)Û‘è/’Ƽï¢G£5¥¨Ú94Ø_*بÒi:n¡‰ø +}š9 ½ôVl.§hxì[…›é²‰¦ í.„ÍYÙ»ez)†÷‰ë&x/¸±¨^¦1Á¹7&ÓÎ&sr|dÊ/v” ÓÅióÐÂ&¹9gã÷t™ðû6öœ ¡œ°*Ù¦ÕO{ häe!ÿ†˜ëmÖ”F iß^EËfb ”!¯Ò’DQ²>Kp?.,­ +'-ÒaT*òM ±\<PGýÑrÁòD0c:û‚›˜tÏú«‡^¿6ðÚ—Áÿ_sÉ÷Ü2¾rÿ3/#ÿN¹ÿ™'èÝ€{¼þ¡S÷¾cÿëûÔ½÷Ý;vñCçïùÚgÆnùÕs¯ýÚo]/d/U×§Ðûÿ^͵ˆÄ aŒè[¯ÂÂ"°x·ZsO>¶²Éè+wÆ‹ãf^†4™õelk•CŸÙ…Ð’²—MhH òèéëck•mg8W¬l¬ þ¬ÒÃ%¶÷là¤IFV†ð™qˆ¦õõ€‡ßA_ߎ٬Q*êE€ »Oð]ô«iî]1T•P¹P&¾¦M ý¼ÆD|3‡ÆüÜwè½kWÊÌ×óçЀuøM¼ºlå~'ÿ¾^½-RÀ¢oK¢q[„}#ˆÆmK›”­ùgã¶Hh|(“tFÛæðhÖy—V¹NÂ5@$X‰pSGPW¸:\j›=m¿V¸ff÷ÄØH_gÉ®ÁTÙñýOVÆ4ïö÷2[î"-«gÀ †0ÔÂ&”øDÉ·`Àž]°Ù³¥"¨Þ£Ñ:d’Or˜6Kû5¥mçNjÜvïœ賚8Ù$óºÿâ9•n¿HL¯ßD7=úôºýÛ ›—ÒCYŸ¿*Ý#ÃÔ‚M OL9M³A·oÕª1Ñýµo".ä62ŒÒ2ºÉ xmE¨ßŠÆ· ×¶)—¶öfÆ ™@Á€’(—p&à?«3FBP1eÝÆ °lY-; A«Ã&hŽS÷–‰©­ÿMˆæ€­€¶Ÿ™e·.ú¢ÿ}Mî»Hf$îð¬²LÈÌÁý‹{”é]£N©ÑrPPyÿ«/c’›¯ÿ#BŸ¼re¬ŠúXˆ|àå ”'Qݯ(u¿dŸxɲy8aJNDÎ8’Å (7 ^ òÜ0¸¯)ÌÄ]=rxïÂÌôÄ8¤í%Æ~¾8oOLØ«B¼(ê7«vç>‹÷Üçå=›V¡9—/\œvó£=š“ÈŸ›™Øåœ„PE_ÿqêëßuµ·Ì‰úÅáéœ8¼ü†` `¨ÔpöaX„>2´‹*9û¬žöØæUê‡v£I¤ÀëâºÃ'¯’ðć`9ìC<Æ>N{qÔ _!7§Îb5XzÎOhÜL¹ÁÓ#‡|UàZ½·*ͽ=R³íÙÙ3Ù;ÙYʤò$ nÜ‚:6ì2z²fÐB6º»K¢m5;oq‹áU·á­Ç{—ï;zßcš<ý²_».ÕÕ^WØ‘jN„^mò•³Ô7þ’<]×<7¶ë†"7~ËÎjŽñ­OøÖ´ÜÖÑ×´£ $¼hv¡}¼„ÕÎÌP- }è ìF-ßþÀšmT†:µÖnQ‡5RZ!JØ·¸wÏü,”Qïê0G rµhÁz·¿N°Âãj¡D+\¤Kfë#©lW]Ç0>AþÍtìdÿ߬ãøÈµk• ß õ]m‰hC{ŠžãwÔ—Û£%ú70Å ?ÜæW¢§1¼/æUˆë«=ªóÓåSçü°×M@.qò­ A¶ló« 7¯ ænl¤·Ûåk‰òû»ŒêQ¾/•ëihè†ìFåéÏ\ú‹¸ ™íihìaÏ5´9Ù?lÑÇíè‰ ¶cr Ñ·ÔÌFQ †.¢ ¼èØFˆØ¶dÊš°Å莚D*ï(XÁ•a¤n›".› 8bd»…™'öhÙ{“!fÈ–x‡èízÔ$>O³Ì¿L|ýãÉX‡dBÁuKb6–D(mQ KF øXeŽX¡• Î˜ C,`ÀÆŽ3§=t`ϼ7Câj®'•'o¤k¢ @Ý\õ“ØÑ-IWÈÚ½–E! c ¿¯·«#ßSè1/PU•f”Žª¦¿Jz¨Z©Êb¸ŸÕƒÚ|ï%mŒÿn£à#ã6ºÚóúY’ÌmÑ(ûTUGãgôâN› 5’s¢£4ü˜·Xu§°w#ºÛÒAæÏjŒ›&µlF‚ g¯ÓktËÚÊ Óø·Ô>äó³Wú0s )G·?ÄiµãaI6ʦ±Ýlà $NüÄEáE¿W†TF@´¹‹†·FzÄig2ÓSc#ƒýÎmÙâ/ÚǾ]£cWðw!Ç#ÀÆó&øWÂà§õÐ c3GÎMM7fŒp„÷kS§‚c¿¿4‰åÌ€Hƒ«ÎŽÉIùÎNÎÎL;EA6Ç0›-LQ¹»W ’±‹½ôÅ ;Ù!ÀÙ}¤YyÓ¼K;0¨ÞN'èôÁ¦Haª4åØÑhý_VZ©Ü¾ƒÑ sˆ¾U¨Dß0Ý„b4wKªðž•©ö÷W^8»é‘t¯H0ÚS‰ í¹…çå{»U¡Ý=:ҽлàÛp…y"cŸÒåö_ÈçOªµrŒIk¢$öA­•ÓÏØJôæ† é­µž£ë 3H!ªˆŸ’Ÿžfó9½0½0?ë6«–³ƒÿ`û¶ÂkmëAAß"ôL+“~l©íä}8Eð;§þ#•ZÂ[ìkfûûa åNõ,nŠÐò—J2EÂӱюRs@$ü§©ÉµÜPj¹sHNMo$dæ´Gu pÙ (¡•æÒzn¨>£J:·VƒEô²œÁsÏ]0ºÃ@þÐññþ>N­!Ñ·«W¹³©!ó‹8ÇåBÚzÊ’øüÛì¼> ñ¼÷¹S‰Â]Å¬Ä ¾Ì Z‰“‰ÞWTO¹õЊ·Ñómv÷AÂj¨q¶YS’ÍMrMåæ2nÄ¥vKY£ƒÍAæàålÐÁù>zÊ„-įD*ÿ®Ôù5ê€íX±6[´Î’­~Ö¦în ¶{¸{ç@Ÿ…æ‰æË6šŸÆÀÚË4— ^…Þ²ÉÚW§5”NŽ“o¦3ØïW´9nA½èMpÝSDy¨óW9½|Mð ¼ïT€V©U©R‹&¦é@z£Sk§Ò$ŠMö¶kJzG+BÅÖÞ½¤ï–x²£” Ò›æô ¤b'î¥î/À½5d£Þhö7N¶ìàb¥õ›îØõ¶÷´OŸºã{übwc›•ÀÏ Þtf¥¥µãŽ]+o<5°¶yUhiiýªrËRÇ“ŸZþÈý¯;µs|øƒ÷|Ð÷‘ñû½0ÜuêþOå«åÇC”½hßÍ¢œ$ܹ€¿ fÌñ4o\»ËOOcÚ¿UsƲBˆÞUy«"M±Éˆq8zm_EßéѤ4®‚ €Ú--¯[E@M/Äõ{r;/§ÙGóq¨ùàÒ.°Ê˜F'¨ñrJLJŸÛ|̆ÏÂ>ãíW[«±;’(†R(&•]ŒŒês ŠË¸@B„lSC*™ˆØXVŠé×èÇxØ¿·> [|•‰yoþÂôpŠniÖ›_‡Á}9•C}ksFH^²½Ã[/n>DyP@w1}/ð9ܤÞ>4>÷›XÖŽCGè«|˽Cpࡵ•ùæà“ÙB¡%Ó4ÏŽy¤Î•Xþœm¬ÅG]. Úû´å~ ÓòÖóüLÈ0߯RõÁï³ú¸ ;`1ÎOLD¸ôIcÃHÔDh¤¥´ê·x¶ÄƒŸœœ\˜œŸ™Þ5ÚQÌe[šr¹@sÙè3Àt…Þ'‡#¿[‰ÌaѯõÊ«Êbž2 ~è¤Á %VÖwA R5JŒîÁw„k«ÊlsƒXSû—öíš´R¬Ò[rÐ ºí~%º0ËùE;Áý¤oìÎ-=f%ÕÈíµƒ6d‰hÝJíÑŠÔ2þnÞËØ6ÍéçÑ[p8hÌ)SŠƒ6îøõGêN6éOÙ<àe»;S»'÷LîÛ½ÏK&4š w Íh·2n’¿¦ÐÀ+Zeí²O#È£±_YôÆoÞ¶söt` ’²j½ùU9]gç;~nëlµ¾Üù¬=‡…áξȸ}æ¾PŽÄ¿•ͧÛ^‡®WN›W¯ûú © ø×ÕðÊm¤““ÇX²¯å™¿óâk,t´‹2ì<|}ý}G‰Ìcë ÒDæ„(l£(ˆb0°®ïê8E_=ä×Ìö1(“ä×\²Ø@~BYÜîHÜ×ÞšRwÝúêÊýK‹æby9˜XeŽÌ •9]Ö”ã/MöÌá¼Å*‰ÁfÕ/ÙEâ¥"‹+èÇÌ@ErXôI¤—½Ø ·xL_éµFZËY9â¿lPäŒka°é©]÷‡xÅôà‘á5%¢ . ‰'¦Ë<À•YÈ&í‚& jºꀧ!;¶þš’xhË,-ÌMíÎúrùöX¨ŽØQækõ/–‹sxg¯OK³+±äuìü¤UL§À ³ZîÜ›[vgoø@W¼%–Ü?>tdW«rîeç”ç~éø™w•ý±p$;°0ØwptGaêБCS…|û)·[ûþÙáÙ¦ælSsÿt~pª§cxyj÷+NM‡Bu uõ]c;Ú‡ºr¹ÒÀt×È]vcQÏVŒû="WEô:±D°ËcÁ'Ò²‡Mô ÞøbM}VF|mïèêFK±!µ†qOx ¶n¦FkOæÓÙb]ޏ'ku6zÈ4˜QYZ–D ‡ûüåâü›oKexÿ‡åÈMËñ¡Ö¦æXÛhCŒÏÄùºl9SÎfå6Zd=§“{®ůwdšÃ¡À‰ˆˆ3-ñ¦‘ëX'âßJˆêùÁ^§Ú³‡ ÏÍ•’zÖSǤ<-ßä&ñ‹’l˜<ÇÓBˆ>/P7ƒém*~ýN¿ÿO8xùïíÙ5Ú3Þ;žÏuÄã¥R0Kë%žÚ|”Ò¹}œ‘Þ5,‡|H$úãhúŒM·gˆÝ$’UG9] s ÓÄ?ôêwÔž¸wÐÖ<@³9b˜=ï¬'²ù\{¶­5“&–c/ÞkKà 5g¦¸Ñ$YÓ\…Âù(¯ßŸ¢—l Z5%ôý–žÐ·oáQúÂäË" ÎÿkKB*v]'F7_˜ˆËßßÕTjMI©ØyÃpk†ß»£7Óùá"þV*¹'»£‰ÍO½a“,ˆæ0ßßIDâGsei—5ã/áûÖó„ï—ÑÚ¥Œ”1ëɲ Z•§YèØ/ˆ ‰2TßúP…’öÍ—;`SR]§hFÇhÕè‚1…/jG¬\Ã`±Ñmpý¯›>w§‰ SÍÚ= !Ô©ÈaNWïÆÍ¦!¿ØÞ­]¢ rÿ Cî9üÜ›hÎm&öšìVsíRZ±¨Êkê{r„o>Š¿ ™Õ•‰¢£„ü’ÿx…uO0tÅ@}y´Á!¿Â–á5 åc3}ºöÙj2A˜ø­KôàG5~éü›†ZT€_C4s±ì’*ñ N‰ ¢!L›ïh×–y޽McòvŽÄp—•ðÞ=ƒA ìÀ}ïËÎ|°­ÄoÐZPpXè_ Ò’)‹iT°cÔ´§a‡¶Gƒ ÍH{¢.Ö)¬{û5%º|pÿ¾Ž•Þt>T_Örœ•øj2Ì%ÍE¥à²€4 3Æ^§q%Z;ëÒ…–$±+5ˆ÷Œ<–«+µ&­¥ººÎÖæÈŒ®wáƒïyòS<Ú˜KmþUe¹ÇþÖRœ ÈtªÎŒÄKÑD®ü y[æ¶&ù—“y%ëUVÆH`4€Š›ð¦UNŸTn€ÔÞJ§6ðñÆäªii~ttt×è®B>×™ÈÅKÀ="²á¢á¥–œ¬,ñôU¼4ú0ÆœEŽ¿ôjxÿߤܿ·¶­Ýø:¼‰çŽ6ïòÇfþDªaóÆ7_vÈÊ£Ç6ÿßTƒÏ÷oP;xûÖó¾· où¾äûô|ï‹Ü"Ñ a>ŸÄd´jRà>“”çŒD  ¥pùÙVÄ*\Sëž×A0C…ÒD£Ã=e\ÄÅR®ËÅ“ÈB¥÷WÙÞ‚A/úo­?ì¤wH_˜*!Vfhñ¦y´V(Ý™ŽÆ=åRG¿‰vY§]8æ ýî/Ú‰ç úÍT¨% +Ó^#”“vú ²¿Ô×ÝåJ»ï_Ù~…‰öÇð¿Ó³ÑH/‰Æ4úµ ‹¡¶‘¢U¬Ô( 9é׆ia0|EP¨[^”}èkJc(ØY²\_#›,‡­6v¸e‡ßÀrØr¿*| }Õ9@YVa2R*1²ÛèËô½@M€¶Q˜–Ôä"JŸᦱË^cÏ`¼0ic,ë-œ¥öÆþ‚DÔ ùãdŒ ÓË:©p· +Éf_µÖN=€PgG©Hº/0º$£y󳼨êP5gxÑ¢nÁïòF/<|ófiÑßßÝPjN> ý¯Wn¿%²tèØòÕÅŽwa…%.þÃ{ øõµß™žˆÆ£~ÿJWï^í¥sIÜIl~»¤Éƒ ²·«¦µð~šü;`•zóRu(X Ó‰„’–âj.Li,ˆ¸‰Û”Pá£ØOà$M« ] íf Hk Bë6TžðÊä¶@µ¦ì8596:o)Õ5 ê­gÜ%2ÑoÐj˜>‹²‹ž|ªÂÁmHigçtx{œ®™³§c넳Gkä#]Kwl=/þ#ÓÑw€Žy+‹Ðý°"w$9Þ×™‚å¡®ª?V‹Á„q(D¤.­¢X ªúp$†–WâN–£ô ¯ •.½¨1^+ ‚Ó}€fOPè&ÆŒYZÄsxnVÙ=9±k°¿§œË%ñD"WÖÆ²Éâ0c,è>(‡ú¤ÛXÆQ,$‡bò†NZƒgVÏÔv€ÝÆ”[˜ÇxiqþàÂAq%õqIO9ÆõˆóW”µŒ®q\Ûv×<×üÁùûöV—ZÅ4®Ç|§É¸~ªÄöãhÈ›æǰ>ÛÉK¸ŽMãN<¿èDS Âm»pgMÉb¼zdeyϼ7sTÞœg±©ÆˆMùçXlJâÖ/¡s›û1Iq ë ¦0¥“—ôà©“µÃ;‡g^å„Àʱ躕7É ¼Éa|þì©“G{.õTŽå‰þ¬‘øuíѲ~ÉÓ]\ ¨”9 7o´¦ èƒ8'ÄÁÀQ<Üj2ŒÉs²_ä!e1\`‹ÎAä…VCRœWogÔ„ i• çØm¥£= ósch°¯Î]!O[cC}¦.EâÍâÙD´,ص¿fr±í;únëw^Œîƒ=Fé#‡øºø±.ü‡øúر.î"øÂÇoÚìYôô4u´¦ý~¿ÐÙÿ†™;oîÛ\Zôö4•à[0-oà…FÍQ[Œ<ÛÁé©I³i1›¡]}çós‘ÅÒ¦µý„¯@þ]·Ë-DÓÓÆrN$b«)*pI‡Vë¢aÞ,Aiñ… zFGS‡£\ŽÆUù3¡­†¨ÝBA‰x,±nÇX2·m nkbM)ƒX_;vôÈáƒû÷î™?³pÆcqd ½ºîЫOÁýHŒí¼:aR¥)²fãÑu7Ÿ¬À® Š]t"ª…2½-hWÕQ.;zøÐÒ¾ùµ…5¥seC>ïvðèqj{®)ñƒK\ÜÍøí/‡â&½s±ù˜ŽèEžÚŒÏv€_ªñQù³ù§ÒõV¿bw_#‹ÝÁ?Äãnög¿SêÕõ—N×­¢º:ÃßË8G9WOø_…ukaéò¶QÚÇnC«pb¬§K3?±MÔ”­} |—n¿é†š–iC®lâqÔ¡#Çõn¼ñbDM4WçÂÑ…9øhE¦Ìz©85h¸í)8ÕÏÿo$þÙ|¯¿º=VHÔ}yõWnîæë6aìAjŒ›ƒž9CÖÑ:ƒþVõSrsù(ÇbSýc̈NÂÕ( 6Ó .BÜ(.]ƒP"˜Édj¥RDéøáÌ2¸»/ìÚ;¿“85\Ý«£`à+ƒC?‘Ø ñpÂ** «!l ¡õµ•e2„%È B<¨W>EܚёÐÒ×yµÕ]ÃÕñÚZžÊÕè»ÌVrò?½8ôñ¡xÿ®û΃·[ãê UÜý‘Ñ%ô¹šg}DŸ²¤}Ö+Ïõx­€nÓ µY*Omêç7µ?¯©<;G¦òáèÏ4q ƒ×¶~À?Ëbë5ÐÁo3傽t%®ÖÉ«¹²My°%§–ÕãŽòØ=W¶Ö†fˆFˆ?ªí ¥à5_vGS¾9/ùòÙt^Í• ôüµƒžGðNz,¹é=èqm㤧‰ÐÓ”oʵµºÐó,ó“LôüÇ·²\á_UKÁ^{ÏËïè_{ý!üãŸöN?ñ‹o<óŽ“ÜPÖSfžBï%4ý¦ÎûVZåB žIPÖ³2ûÛ½ÄQmï:EFΉ¨§Ë¥B¶­©¿¹_›ŒPkY§ÿaý¿þ;Áñ€N3=bˆ%*¨Y+¼‡CòK‚]rCÉ«•«4í…Ô§6iRçÀ—´é}f¡»­Z„IÈä½&@)¸·qHYs¡z„¥¿µjYËÿ•œo>È?ËjÉIÁm¦3‚—•Ix Î«hø€ßßk–G”;ð»ÄÁ™ÄÖ÷Ø™^¾„þÈ&Ÿ@A—&Ué¢ùÖ›ì9ù½U óßm`·äpÀZöF8y›UÆÇû¡ü‘!Êa³ÿ(~Ô:f¼÷Ò1ÿXó2óZøÝ%«™éýâÅÚD½· „s´&]àO{ |3x;_–9 æ°#üŸÑ_¦¶÷O¶—GO ÅŸ,¡'-óÑ¡—…rÌÀÔª’Vìö¾º7Öího5ËV¥ÓVuJNT™ˆÁþîrg)ßÞÚÒ4Ü4‡~ÑjÙ ˆÉ2<\7e¡”+µtèqb唩‘µ[9æ›­\+çÈ ëmç¾KÓArJ*[¦f®œI¶7D6?‘j/ë†.ÙÞñtªg/á›ëË;’cÔÖE›ò)ømdÖŽ|†}µË›òýYýè7•H‰°1O&­ ôˆ6Ñv@mvÌJS‹GiZšeG8ªºÛZ0¦¥ôæ¡HýI'”Hƒ1ŸÄKŽVBSé({&¢–rêGýÅB!›‹§sÁŒ…ÍCü=¢o”õrÓr³hr1~ÉÄÙ›§Ù]-e9˜æÄ®=ùÙc½²$Io0ÜÂÈǾxh-*×ÍÇã«·MND;tÆê9Wž2Õó»Ìêòl!ôþ+x¨®ÝÒ²¦æ@ѳ Pòœ^úÊX©Yò i‚xaÝdi¨ì€-uQð‘0Voln7HÓ©rg!ßÞÖÒ”©Kõ¦{ãÝÙ Í¿5ÛžbºÐ ?÷k´¢4#ÚÀO²´üÅÁ!ì€z׋›Q%tz/ÌOµìÚ#7Ç£ÂcÉ ³£.‰&ÔZµ/SKsŽ%U½âÝmR]rtd’­ÇG-€îVØkñ—Sþ^€ 5ÅqÞß¿wsí·Ü^™¶.ríþÂÞ\nOž{E4ýÓǬå¼Xq!AÝK<ü»—ímâW°ü=¥úO«ž«5Y">µ²+,™0?ßß7<Ô7Ö?Ö™ížÌúw”+à}C.·aePÇ«Ö×dë­xûú TNž¡m+›é½`ÃûÞO~>£È#D!3ÜZàÓ ^Ìd»Ö.šL?kåj%‰_U^#Æ´3Ëh7뜭ÿ+^T}Ró‚Í^½ãzæÛѯ[ZSBÚk€že±>/Ûú|šÞïV: ^ju­U\R–âÚ|PøkþϬ¸ðsè¿Xq™1˜7Š\”®‡mtý>þÁuÖÄ‹fZV"+² fçH«yW š:ø’/šù²õ¢¯ÞÂÖ>ÕTï ý¶*Mu1Ìë^µŒíí>æ«08ÆOꥼCÍáñgäS—RÒà,³VLÀ%¶™q³àâЕN„VžÔ1ih0§²´`$‘K`‰ߺå"€ Ð!@¼õÏDW}„Œy'z“zfÔ…ÌZZ.!ý£¡¯Ú¬úŠÙ;«Â*¹+,ÖÔ¢±‚]CDe%:³~°^TgÁkGª°’.«Ìöâd` ÏoûƯ'⇅ÿËÁDä#›úMnÚkJÌ`)À?æÿ4XŸøÁŸ …7Ÿ&ƒ8 oNn¾ÜªÌüBØß’Šoþf$I×é¡Í«üç˜þ9óòKt^>QDh@éøVÈÇ®a5)Tž¦ÞTÍ×gˆ¡i«oëno[Ê^ø@ŸùÁ¤+=F‚Ž U«j˜1f2™¶ÌŽ< ¤ûÇ ß}@hU µ’Ù£õìfd ÄÁêÊ1á·ã"iù‹æ_Öž$Ê :Ôt'ëóœ­Ï+Ä_?xUÕzàˆoUtŒ¢‰Ö—'¿’Q®[úR ¨¤7)kð‹­ÍCÐ é ¥§vc%0¼™Ç7[éÃ;ºÎ ¼Š&‰ðžP!œv#±I¤5Uô6V:ãõ™î®b¾µ9Ó[ßKúök}?hЫÍ/Ñ‹÷lz÷é¡KBEÙýà¹7ã‹Ñ‹ÙËX»Z¦?CßqÿŸÈ"ôc|•,Ë8-¶:XV†Ø,hla q°ê34½~®­µ±!3P?@ˆ†Ä¡&¹¹lãͳP›/³=Ýh†Hç Ôé8ô dõkŸéë„{ë”[kGCc9"šŠ(,,øÎh**bç‘Lý5"²‚í}º7K°š8&”ø‚'ÎT-8kDG7z¹b]N];D3~'QšÄc%%OâK¦­|ü ²‹A<ÞÝžRe,–q’w¸­ ¥:ÞøÕßNû> H\€¨Ðsô¾k÷Óó‹Í¿éÀoæ?”Ž]ûHª+]וânŒ%ðÌ%¯y'~±Ê@—®ó´Š«fº:T±7äçažßGðÎ|Í*ùà»Öµ>v®6“(KxâG¨ mZ\“ÿñxùÿîý¦Ãö}FBhLV;c½ø‰Y¶÷“Íf2Ù®lg1¯Y,m‡'¾Žúþž¼÷©¹ýž'€CJ¿V¯V­JÏq”ÅQ=ëOÇã ùx:ž–˜lÎnfuœzœDÊç¯6fL«r¶ŠUnè›”6s½*·&„YÄ*R¨Ëú¿lÓÓ4iÄözu 7lÀ¤ú‚è®­úØXî\°|Ÿ lýÞýüÒÕ§n÷¡ 'qAI{¾I~Z¥É& g”²bî/Zô'œ°ÿ¸‹4Çð“»!ŠY*Áü³X¤‚Ò=¤0½„ß56Ì«Ïvd !íhaëÒÂ/ø 0îÑçé¸}cê}Ùfòï3sÛ;ÏÉA¢Õ¹,d¢ª?ïøÞO¿ï1о·~%ú©QИ)¹ Ì±´S! ü´üdÏ̉8!á0o~c=õäd>(o ™ãeî¯DO¸"Ž–ºöDã›÷FT Duaæ…~byË@ìgjFƒ ³·Š$‚ Ôå››8|ðÀþ}ÊÔîɉñáCƒ…\SOsOC}: àF®1Úhèçº}Œ¬ÛsWêMuñr¶ºxËÖ\ ¯ê²¥ý­›ùkƺÅÏmݨ®['s2Ø¢±n‰à·|W%ÁFÐ|EÂ1/–ާÄtG°ò•áìh–æzÊññе¿Ã_‰_»ÞÏÅe.»9–à>pm’¦â/'s‘k ÷î“óQîé{_|Ê ‰Äsý¤ï׋G»[ ƒÝ-pJô·D¾Nd`‚÷Ì ÐD„ŽÇt¦ÌîIˆ¸Ë ÄNð'¨‡%˜Q›÷¬£wêïa­aªleºl1¬*qõ±†PéÒšÁ7¢€ÄuS{a.«{a>~/×úù9Œ–çÍêëik¥s:‹gµÌiF›D–uW›ßŒyJ¿;iLéÒéÅéÎÜÌéq˜ÒÉ¡ÃíC½;'”ÎÉÅÉNuvÃåžv×)M4¯7Ô¦´w,"Ë;ÈfÓ­™x$0æ÷ßôçGD·|Fµ1?¢ºND–¯À-VÕb ÉàwLK@á©é7ú á*´upOWX»vë*…S¹d.!j÷?Oðÿº¸@ûú<íëƒä‡},lŠ(P•ëÔƒ…ò 2Rí;ýÚ»üø OÍeŠÓSÉ8ñõ}Ø—X=Œ³ÜçðQû€¿9ô!‚ðÔ•€zCºªñà|„d@‰N@æX™Þá p͆¥Å2磽kà¤h…òE±ôó× ?§òðët\¿Jú\¿ÆìL ·Fvón4+|Ú‡ÕP6BO•Õ‰V&©MÀ5^%í0·—iäfÜLü˜|;ÕÈ0ÖgH_ïUû}†öûavªÄ1^q¯È_À$¾‰Ð‹GùWðÿÎì!üMà~M@èôUYä|:Á-Ìå¢çZ$”ˆŽçµtöáOؽ¦^ù–_¡¿ÀÒÊäq>—lOæóí5ø3¢ÚaæÏ|„ø}˜eh ïc †³B]¼4| ,Y7¿¦$êÒPÍ8ÝV×&qŠK‰j¼÷}ÒÏ3j?ßgý ÿý|“û2ó¹ÉߤŸß þÙ%%6™ÄD¾”ZH ø™3Š‘ÑÐ’y©´~Žú-pµI¢~ì2ù‚ü‚øczK8`Kfê°ê£‘88MdÁ¯í{?Kd°K¥åY:§°c¹rU–8NŸÓ8ªÓh zÀU!˜ÁY!Î=Öó­@É Ÿº’ëp]6WhWïO~†ÄûTþ~†Æ¿EúºA î$nr£Z]¡¢ÿ¬¦ Šr¬¶›ël4Y{²'—ïÉgC_%ýÇ„-ÚÿWiÿ%?|™1À˜'AY.Pù®IÿBd_(5FÝeŽç¹3 Nh䡹jYMª(Ýòh¦wø9ž»X¡=á¡à8 N½N[›À íW‘üJLRFÅ‚h zyšÃõ™t2I$·r­~&cæVî¿ 7Óµ*Ò±>Hdlùj4bŸW¼B¯ÀFæØ$Cºg`o ¾%zå ùk6«õ¸>ŸMæuýüD†þ€õ“¾~›üЧߧ®dâp·R©Ñ1x’°rníwãÝY^•Gœ%:æÛ*ÝYŠ 2–+Oêü¼¡#lÃÐÐ*iø“|µÌT \n@Ä_íÌ–'è  óBç§T: ¸?FpO)T„,ƒÖ°IÜI –C°¸Íˆj½ÒÑ^‚ÓöÿƒxÿŽè‰?TñþÕpƒaìjD4Ê`÷VAeSÆ&˜_¦:\µ=¿ÛÕž£ &8¯Z¯Wq^¥´~œ ˜%s…u:ÓÚ¶Õt,8(º$ØT¢éÖLß®]‰§âi¸uÀmý9YþsB–´ 5ñcØfÛ®Zõ8€ˆñ‰r'†"Ë|‘8­’oîÅ÷p÷þæµÿÎ}ôs¡€œ ÙoãïêÚ½c8Mïü3Áûr¡]¿ñÇS£’ €xÎÒrH˜müj‚DÆ™#Rð“˜/±ZL­²ª’ +~Ð[ÖŠJÚX¶1¾K½G~ƒ¸|;à×oÐÞÿ”´œPƈ¥'Q4Þ î½å"ž¸<“¦9P¤y·gÛv´¶Ô×%‘° p‰+éµmëÉJîbymêaoïˬêæ•­ãÁ«mõ‰ûÔªp1’æ·†Û=´9Õp©9Ä ˆœùIÎh-%ìÞ4±=Ì©Ú1wl³RB’^n7avkÉž‹ÔML`<13135‰ÇñxcW²«+RÏ&OûOÉLžÂM`&D æUIÅLVŠŽJ»ƒµµIqí×ï¼|™ÕÊÞü×­/¢·(¡)À€7À©…ðzxЏ奀cm{—A}ØZ{ä÷aÿ²wSzùûÐÁ½ 3Ó–ÿ¡ñ_ts„ÆgX…ä­Û¶¾H©ýŸäß¶ Z ,¦rWhôO!zƒ¦™Ö sê$e€ƒ«(„ËÊŽ©–©îq± = ¹ÃÑÙaBBûCD]àTe¨ Z¶¢ „ƒË¨2¸AzŽ=t`q]†"lŸœòôŸ OçÐ),±Ê{{9Þ‘á»}¤4x× û‡dÎ!5¢— Ô›NÙoaZèM6½9vo=@…+D|ªc° …¼”A­= „3v(äDB'Ž>´´HF?×XÈ5ÆÄÙrvthtŠ'©Vø…ËðÙöâ4ÎŽöò¤æJÅ‘ œ„’ª3²ö)<*Ò[Ô#j1ÇÜèæßÃ-Gï¿ebô†w¹éÑ7,–Þ:Ó»¹³©™ á×'¼æhO_yjÖŸi¸ë¹û¾{Í~ùr×oߘįúòM?ùÚ‹R(Ò_ìëo9<"pñ÷ý46{Ûo:ú†Ý¯øÄ‹—V‡Ã›w¶5¬?:tû½Ë'möMí››ckSÙ½¯ÿäÆÍO¼ó|Ýo®×ïà|‹þB‡¯»¥gr÷P×Áñ\!$S]&rðË$Ôj%ùFš÷eó{è{°²¶^¾õ >Æ–~hDô°–¢te­¢~R«•ŒC‰pmÈd‘þàzK!ðIõ0؈y}ÅŒuèèˆ YE‘Hj.ì|ž>šÔ4Faw<†n„¼™ ¡‹2Y A„ü ¯#/"èÑÛ$Y~·lœ?{úÔ‘ÃöïÛ;?;µ{âØä1c&4_=Eõï2ð?eèß©-žèßgy~ŠÓµ³e½ˆ*¡³!‰x4?YYð·‡ÖÒ_ªª«V0¥Sƒ@,Wh [A»Éˆس0qt÷Qc”rÄC[_£ú{ÙE¥ú{jó‹äߤ"ï_°v ¨*ݳ‘! ´ŠB¡ÔýÛ[[ƒ¦ êÚzyHi5DUµ ꮫû ’pˆié 'mó³TQ¯<0»>¿nWÕÑ6vïeëûTW/muú´>¸·—×õ4mWzVCL©¦ y)fú …B¨ÚRmï®d»QH r¶wW¯Jãžy¢_-š¿nÏuf ©YÃfœ¶ —ĺL+Ñ@lQÕ³5éØIUǾê¾_·ëØ#’,pÍmÉçëžîLÍOv•å†ÎšÔìÖèÉ©6›–ýW,I‚Ê7õMeèÍ%s18—–©žÝƒŽ¡K)²2Ι¦$}š8ÙûÎF‰¥'nt˜(`ËBP^µC»-…Jk•¤æüªâ Ø´(Eƒ6^2e¢ $HXXö††”c'Ž#tÛÅ 7ÝxþúëÖN¿tâõ±ÆòH‘ŠA¢¾ºXæ8Çæ^­•l‹ÁÑ!8áDRŽýÌ竊D¹³<Ý‘Z˜ì„¹nñ hiËñ¾ò=ïìz›€üô½Š|³Ø6 äw u—RSòE1;8oê.V’cïŸ1 †?•»}§Uf„ïíÙ?ÒÚÜÞ|¼çÀ(ü vÁOí‚:ÓoØ…éÍm}½Êˆ›Zk WjŽT*Ç[?2è2Å ÊæÓNºŠ&rà«•´Uƒ´¼A ´S%ËÖÌD—2åm¾Óˆ‰tºá‘„~áªí¼^ÍYüG ¦Z4l©…Œh£µ–P@ œ@˜=ó3ÓÄú[É£q¥Ïg HéÓã­ï+¡X¦PæTb§‘LfM†ø‰‘ĺ1 :‚©°sL-LÚѨ£©G+²ØÁC>jät$îìåõéõc«Ëí¬‰°ØŽðÅ3¶qk*‡ Æ»¢Æx«–Oû¡ž(pm6ÓÍq0 C°¡GÓ¥CÑÇ:‰ªŒ (7â!͘vbB „NÔ‚Pƒ*£‘VÃÔžmÔK.ÄÈP#ØÙ Ž(Ž@HøR)óžxÂ/«ØPe4t£¢å çOŸ:~ôàþÅ=s³vÅg> Ìó?òhe듃ÜïTãÏÅ>ba"Yü© éó:AìQDG6ªÆ¡Q?›PY¦n~<¢M蘆‚˜äˆ9Q#&}&‡]à½ܨ5ÀÔVO$ȇ2ã§q¯䂃Èc|óMçÏ^wú䉕åýûð<žW}´D¬]KÂd¾<âBª·~ÌV%_PãÛq‚ÎW‚®Ê›qLû¤Æ‡‹(A–@‚ GѨ`¥ô81FÅ-n^JIum†Ã´ÞF< Üi8R_\Å_Eñxj.ædWÌ0”å0“Ð9Žž¨¡=4=ˆ1« ­)RÅTüv¤ÊþZðÅI‹x̉׆JFD®w¿êÎ;.nœ»þô©ǽâ×´¿R»f‰_MvƱxN‘ Ηº–™¿« aqè¬fQ"A_È«dÁ’¸)ÈÕdÙ@fÃ4½DítßÀ†Hµn5áƒÙs }X^6£ñ²’kJ+Y‡øô©£«ûÎï?o7•Q5ަ¶Ò#Ž6ÙJOÿ¶"¯­Úãé¹°j‡Îj?.súÞ ý»ËŒŸ;LÚKÂJy&ìekC©‡µUI«jULîÆqÚGD‹Y¬€®¤â‘ÃÔ2ž[?uø–#·¸ÛÆ›×ïSÛ¸¬ÛÆvÕ6ÞNfsX Zìãu»Èx?©€XÕ¸ßÓî²Ù ZQé¼qCལ`˜1ÝŠ³Op¦´í]ÀøâÆM7œ½þäñÃË göž1Û°¸fÃdjÃöhµÖ95&ÛõnEž™àLl…Õ±¢Æà(KÄâ #NÕC'Œ×±Xb%ñ„a©Ôú犙Yªê˜QŒØ‰Ä‰:€ÙYª#Ý0áuG“U:yã×½öî»^þ²Ûo½ù™´3§ÖNÜyòN|×6Œ<íç·~à»Axæì<Ø“¯°óBôäçï>9Ø­µÃŽöKœäçX:"9¬‚yf.(À‹£ôÖ#{ ÑZsû4ÒxÛmkOßrƒQ:‘ä‡lT~αr[*Ä33=ÝžÙ;CÌëØH·Ò£t[›ëRr—¹²¬Þ¡aüx‹•Ân¼›ðãOyK~ÆNeJ7±6Ph¦çs–†TB‚ÆÔCÚïÒÜ:^Ø7ìÑÕƒû÷.¸Ö}¬`Ýðãê˜á&ÌŠ¼$ë˜ûBÎà~Z‘’­Y†Vé‘-ÃÞXZç3äfå€üÿ]$æ"…éqw¡vÊn,… è f5xf”˜Ì­Ÿ:zäÐ/É‹Ò;Õîs±Ù~~‘üû6(‹6ÑAí£úÂwiYÎ"sB}@ûPu޶ iLÓˆ'd-3µ‡4W±àP¨F$ÄÆ™á‘¾¹:yæê䙓g®[÷Z8¦¹ßd+ôãnºnÞEæ*¥È×ãpĺn¦IHÀ…£œ7$Œ“.õ€ÌÂé¸JhÌ2G?Ê­¹Ú0X¹fÃBxÖ;KåûÒΟ=yüÈÊòÁ¥Åù¹™õÙu'ï’&9¿lÓ¯S _qP‰Y,+ÀøÆë:–Ú…³P'T-ƒ]QÇ&¶“ÖÛƒTûS2àÖ¾Šf^SÚç(׎9°æÔÜ)'§Â[tÙ¡Ÿß¯ò,@þ½êx¤¢«¬<ë ªZólƒÒ U×щ킜ëê:ºf8ðiwzÀUYícÕÊKCœš–z×Ò¢ª˜×V,ž[:祚cf;iŸ‡)]7ÿ„ü»W}‹kà&úUÐü•ª£‡"Th‚´þn4ÄAÎ ¶‰œ»’Þ6¨ÁðQoÐ*<ŽBNkÕ­J×ú:ùõsëçΞñ|3¿=õëÕ¯”ѯћp$l•ûi!ª-rÖ¦Ù¢D³%±ºñTEÇ&~V4ƺP‡¢­2_#Š*ÊzM ¹ºÐToKÇÕajVMdš»ËA©šÑáòt÷4䬅¨ w… ˜"< äÍiK3£#˜‡7f­Ü–"¸08ÅMã^^»oÀÃÿ¿/í-ˆ…cë§{–^wjèÈ}ÿõÂÈú¾±T{ xÓ+ÃõéÂP˘ŸŸ¾öìàõ+Jr4X?80÷úsíË}4GŒ?]žïØÓ“¿øÁëïýë­ïo¿˜»öo“¹lt¡.ÿæÂ¡»W^lŸîo}ÙÃwí»uO»ïƒ—tù¾`ÓaOÑW)ÏØ$¼ÂXâÒº#ô‘U^…,SP#‡H2K”‘a› [{¸ÝÆ$ëÈ Ôîš9>{Ü)Uu o¾Íwÿ÷6½t‡È_°±[ß4÷p¤m;öu5À¤m0Œ5¶ãü ît½mUWšîrXÚ»0;ã¡®u¾8bkÊmo/*ò Yãµ)=)ŠFX0¨¹™ÚÎ…mÛÇâÆÈ •IU‘k§«CWÓqJ˜}þÜúÚ±U/–«ñ±+¿é¾’…ïmˆt¥ÈkX¶í/ÍDâœ_æd°òÚ‰€,³ƒvË8c*•QÆzË9GÝKE”¶!îO¹ RO'ªãÓ·HfkÂb'&Ø.YŒc¿‘é…NŽp^2>å@5T‰,Ó»Õб×ÝÌå`WrWWØ·èåS%4ŸÊ{Ú÷Á¸‚ºf^°Môcض6oYrPu02G+kk¼eɱŒ_*.·Å<ã¶ ·R—¨…ZÕ"T™þˆ‡’P‘âH¤FœÊR%tHö±ºpÄêÂmn»õ–ÊŠ(aÑýEALù "ߎcq«.ZA‰KpÎ3ÿ´qæ¯^°LXJB’É“åÎCÝÏ qÚ†„ëP-ˆÕÛ UñÃDyI­jC Ó¹“96¿ðºW½òÎ;Ô 2UÜç:“}¹ìaÏÙ^â¯*1¢ä“8´îL¡±„Æ=ˆ(émƒšmzâ%aI»¨”i†ÅX!ÛGÓÊn9YѨ ­l0½J à5@+ËÔ?8{jmyceÃkaÆ,úÞuoÓ2—¢ï±ázb§¬s9Ví©~»"ÁðE¶ã'$^*¢´‹Ã1vXø—€‰nò‡½|…Zêû}sµ¡©¢Ê÷zb¡Þ r?3pÕãý'«ÂÍçοtòRe!iõã/{øÖýد«û±v%ÐD¿ ›¿R}ݔƩ܇aLs‰ i¶á,¼ddnë{Ö¶0·S—†=5cª"󮈎§f«®ôßr Ó·\ºåÒ·UÓI³þ¨j×é^1PbÄ®'^ã1«þXAqbØâg=ìZ‚ص:¬^©Ù¶'~^ˆÓ.NÆØÓ¿4Ì­ôbKu̪‘¯¡‘Õ—†±šß°¦ŒÝvQuî¹ë•ß|Û›ks2V=òZ»íAnì—àaÛ~ÉxDßwÖ`¥ó4Ó°= n&aÔ‰Á¢yý(•!«žà¨*ëéheþjk’î“ãNGõ뤦S{Oæ:ôí6ÀÝ4ìNûÔêXè1d°ê<]¦joùØò±£G*l¨˜yjÙŸ‡a¯1$ö{6ŽÎiËαힰo»[ŽÀ\´ÎKDeW3ÍêC¥ Ûèo¹NixÆ.½cE,mÔ²Ö‚¥âÉZ“1$Û[µÃ9ã2EmºL÷‰.lýŒÍ]‰ø õlîŸT"SK˜çòØG3?ÁÚDüDj¥ Z#¥ ð>^ðm@ãO³+fê-CšB’ÞM‹Læþ‹…¸è ﺦ$§§§¦f•Éñb®˜-ær!šGœá-Ö1˜¼ŸŒá&åü,öÌãðÍ¢ ¡?èI?ÔÉÑnc¸Qrdeÿ¾…93:/}ÈJI>ªÒ“$ÿ6*‰}8è·Ò£îîÉa.”‚ „T…°7{Õݽ0å`RRKAYºð’ñ)jFE‘ÛÁ®S¸¦4LO¯—KæâÍç ´óß`´chGKš3^Tš±öö$ëD¯­Bùó+ˆ?S ÍïV‚9,JÐ!§_ð“aûé°ÕKFž$Tlk%J Õå;ÕÑçÊtQýüF‚}<¬»°_4Ó× ‘PÖ燉²Å\y.{j€ÉØ`À®†0ñ2ýR„=>Ðw¡HeÄDKwè&ÛS@ˆ)kkß54ÐS6ɉl‘¯v£«zøŸÀ7et›&´‰~°ª¾í£S û¾¡ ÉØ‡Ê¬̉#ˆ0!¨ ˆ No5hÉM€Hƒ«ÎÆäèèèîÑݓ㺸É9}øºØž¡ÁCôú¦zoŸèRô¸Bü9l–·I‘}rÄ·A¦–Àz‹Aì‹þuã\Üs‰lܶjZ³m,Kßèð@owg)×Þ6žg#‹µúï²m}OÑõ}E‰ ðwUcâõ5NUÛYfUãzO¢€m꺪7«7&ó•ho(wµíjßÅl³Èüeý@}´¥D@?ô“Ug_w@]ïpW-m^mˆÕ`ìâ þFO@×+µ)F{ªdÛ¨ Ò %¢ÆJJ§bÒálÙ›OSºnøGòù[Œ^›04Ñ/ƪŽèÑ™c…µä ojØoæk„³q¤7d …¨¶Â•Æñq²”qez·UpÂmµ¬ó)ºÎ7Á¯’#ó8,›åúúäðYÛZµ3¹*ËÑvqز›;ôEmðð¾d÷Ñjr87­·øÿ‰ý&æoštž‚£äç íí¤I-î쩤ÝÙT±JJ‡NŠNÇq zâN„ž·PÑ‹´×-,é’Hâ:Q”+°²¹&(»R‚]Ín™ Œ Œï­®uA3à*(£úl››P…ÛT]ô6ÃŸÕø!@…ï,ü{Bæm¸2öJ©Nˆûµ=h€Œ€RÔÚ"‘ø§þÞÒÊåÒ6Ÿôm?«è×=æ¿W‚84{(ðBDàµ~õW¢µ¯&žs@³WABYŸQÆÇvêŒse>¨¶×àõÐß©ÇpÈâ¡O…#œò…@ü$Ø„,!&¼ž,Q a<¨ß›­KÆ ø­.Xü(螨†L÷ag"X …CRx…}¡0ñé·HY¨†ƒ† pO÷n¼×ßšÒÔÖ¶¸wfzb×`O¹£dÒŽÑlevD W#C ›èÙ1ÌXdRf{(aκ؇Z…|ˆ2žˆzè鋜׊OŸ×D&"ì±jTl8®†L™«„ihj˜Ù†………ý û—mK2ªë&/¿Æ¤£`G¨W ÀјyMBñ¨/÷A.+$‡‘á^$¨ÖµˆFc«(óœQKR“ºŸkÆ«²¿„,I¼ìñD¶Íœ ÏÍp%Û }wÙUïÓH ÷+²2B£80{Œ(HToð¬6÷²ñµ–e‘¨E¦âÊšd( Üe—\•àjXÔ‹þÞ™é]c½{û÷ê"Öcî·yÅpénòýJìÈÑ“fþNɪþ>«iÌ(}®Ù–$¶%SÑ"MË[²]4%MÊ^&©*6zÞì Nਲ©“¶ct˜Ø‘=Êôð¡ÑCK3ülua0ñj„i]$Mô ÙøBµ)³š¤E¨ˆÉôFy4ÌÁ$ûP«QÙ&ïE”§ZVéßB»¹a@:‚ª6@iY\ìï]<´xèà~ÇšŠkªª Q.¬)bâGp,j^S‡PŒ(ÙØY':6‰£Qõ&w-¶ ñ3`ÍT´0Ë «§AØ6Zå@ US 1›³Žýûˆ™±ä’s34©6Ëšz­]'¢ÇõXéÏ-±ÒhH¨µõ¾?º*¸—¶Rv:!MŠÆÛê¦ÛÚE6û¦¯uµ³,zÿcËØwj+Êuó·²f¨ ëi l‹¸–mg%ÙÛÛ»³wçЀkuÜ:^Ø`3Ñ2ZEIGÜõÞ-pY4µãñ^|Í4+†Gu¹¶Nߎ˜ÐDm :O•ºË±Jh*™¹Öl›÷ŠŒ±=‹wm½€·„h-¦wÑùù+º‹ó÷!´Êö´Z¡¾:â!K-_…P”Vë;JúKC·&¨Óy‘Ö¨»hÿuM‘“ÉdC²¡XÈÅÕšu³[/pÿUxäbVë‘Gÿ² 9^ÅøÖ€Ú{‚Ö§„A\õcšoÖÑâEþ¢Q©ñš’J&ÇÇû;Šm­YAµ.ðâGüW4^.ÊP½’¶c!0¬¸F{ÈXB¦u+’ÈaZ¶’;Å@xž¦mLûæÉçÔ&ñ¸?]nÂCü”“Ä:x2Nß“OãÜèï ?<ò±ØCÉ@ÿì‘kên[éK©›¢\þÔæò=^:8œhJJ{ÎOþö=Í$޵͟°ûû )ýu˜÷áY$ù(/)=ÎI¬ËwØ¡)ÃÉ}ÆŽ“hªéóó³}òJ Ô×S/$?§ÍÔã…:Š.rÂϲŸÌ“$ø$¸8*HèB%¥ÏÑš¶ƒ f»öù]SÉdOw1oLo@«©ãz·Ë¸vš×?F>}†Ry…ñ­ þ1øm0ÎÊ?¨Å E89AÐ>x µ(–4¾«pdÅ{‚)=¤x6ÚÕÕÕßÕ_bóÈjóÇ ÄG²Ž“xI[ŸÅÝ0ƒÓšr6>ˆl„(B„ÑH|⺌y²‚‚¼°nÊ÷fë9•dQzWG>—ÝÑÜX—Nö¤záƒ×D./;ärêûP5=Hø*4µ¢eÃlGTÅœ9ЪS:¸“CªªÐа$,æ[[’ÝénBÔ¢¯mîcvz@¦—(UŸFè1%B#§QUUÁ>À ¥’—`ƒ]õUP±µÒe4¤Ò ­µ„&ήkn$].䇚‡LKÓ»\Æ4¥Êó£äÓûq&†7Ñ"û°Æ~îôSŽB¡LBÄAFTöÁI~ÉopŸ6õ’N%ÓÓCäb¨gh°ß4#Á–r 2:5÷‚Z@¥ÍÈ0 ÖQ xÖ&­!"­aS,Ñ# Î!æUI¨ØßK„]Fâ¡cùjÏxB×ÃÌßäuº„}›Ï,¯¼Zlà¨p(Û]‚N|¤È1 G×´ªº' ¤ i,ú„õ í¨éíN–’¥b®£@¶ M„¦çYüÖaÌú•­Ã”gOl=Bâ¸7ï¼Ô§~§<¬§l°¡J]Ô§ iK€Å¶ÑØ09aM:ÞÑÑz4¢ëQ¢Ã^ëÐaÓoG¨GéªìY½T Ÿò „º”8qìdÈ:4d×áã˘o½dÈÜ[ÝôÃÌ-‘e©M©D5R€pOvÓ_ ¢…]¿袅õ%dpê­t*éX.APÆ&´Ï I⧦Q^ÉB©w¨Œê¤,Nü¥˜*'‡x“£4 u¾÷ðI›“t”¦Éá¿rm—ÉGò%/1Þ/“>÷°8Ë¿žÚ:F~¾Ž=«Ø™†yâ+­{É×&âãjíœO%ó¹- õÉŽL—^:µy…ÿñiîzâYBömâŸE¡rpªÔΊ‰…¢Ð+{·†Ûq”ÒlÅ­E¨ ÕÆ ®A‚ë{ìŒcÆò÷êî7D{•ùn²:ÁIb2ݳàNõÌ4E µ¦IÓÌœVB> ¡Cœz¹dGÁßnêç²­Ÿ)ÚÏ}J‚ô# bQÐúòÍjÙ'EÒ¥Ht´a×<»Ÿ‡6Õ\C{ÂârçäøÎ¡ÎÙò,%6°ôÊå­ç}-js™®­o©'úK>a‘LWèƒ êZ‰¼ÁqmŠ[̰DkÀ;'±nWÆàrõ”q'î,÷ý-Fÿçý_Aï†úóL£µŠ4׸D-«ß¥÷fЬØG¨\÷»u…ïægÇÇú{ÕΚ|±þ²õÏ¡éEèêÑx‹ÚèÁ °›h$ u¡¥ù$ßE¦ró5¥ ³²¼oïôîÑa ƒd+o±óˆHî.ZÛy¢çw÷%àq^Õ¡÷Þ›M³/²öY5ÒHÖ.YÖö[’-o²,ËŽ=‘bËql+vl'qvC’¤IS -})-mBÄØ K %}%}¤_!^ðÊRàQÚÂ#Ò€3óι÷Ÿ™4£ÅÁ,ï‘ùŸÿ¿g¹çž{î¹÷ž{.¶Í(Q¡TÎ¥áfh%ô(ìí΋ÁÜ‹ðó÷tjà÷§ÏuSUÎÍ]-6–“ Z=w}k©<Œ[Cm ‹ªXÄB,¤w””ç–%ÖW ƒ[a(Êñ±¡E‚´¾ìRõ¢ª±–ôsøýb&#dZÿjù™L3)è¡ÍÂT5÷cÉŠwàÞ’k’Z,Ëbؤ †ä@–­ú† ÔH×WÔÀv³M~£©îä,µ‘Œ¿¿êû7S›tÉa£6Eœhª¢Í:© a‡Ñ«pÞ§¢„“4€¸°fÃÈÀúu½]­-Íã©qƒWmªÐ÷—èó0×ç[tÈ\í‚Μó^c„›ô^s^³VFnqbX©eËÁÛl ôt7µŒ™ ÂR¼ñ¾ð üþ”nÃΰ”4Çc£ftˆy NnÀz3‡¤–ï?+”×[ Ey/0¿•Õ…Ú®ÞF:¶vm]Φ”Ös8ßþ~¿×˜¤šÿÐÄc”²qyk| Êna8bŠåªÓd+´/¼´^ëµããØVã[Ç·n™XÔb޼íZN¯‡¹^ÃôÚ±ƒÚm¹öê#vPqûü" w€†›’˜T”©À:YÒ9VMëñ‰¼‹7b”í$n“M¾mñ¸¤l†ö»ïüP;u0æ -¸âŽ×¾Æ'eÆÊ¯õ(7>5ç‹C¼.^Qç8à’£ÓÔ䯱æáæáinÒ¤eøÜ|ÞUÌg“™3;_ÏY‚.L–ó¼Í.[r‡ààt¶µ–Óõþ2:rŽü‘ðþÉOaöô¨ð5:`Z lµC[ZÔ¦À¬Èì~8Ëì20ÄY†ëèÌ.J¯>zèÊùý³é=»w]3sÍ"Žœ|%e„ŸæîÕ»VrÍ–õ»JñêäÇÂÃÛæem)¶+—•øSgo äCÆåÐÝsj`á¶/es”Óá€5Úyÿ¢8ø@Ë©M<ß% ¶r6²»aÙîçs-QI’²VOÁ ,Oä²T„´¤š› d2Õ×¼)*©u¬«s˜á婉a6@뤀ß)óIX5…Y}Ç6Kåú}ëÏ\5"É2¥6—Ïz#Sd)µýè-o›tÒ¾ýÉÜÌìÑu{:oþÛw×nÞ¶-ì:Õº‰Íckî}åO¯ßî:ðŽËØ):zÊ$ÿ#%þô'ÈŸÀ¯£bÆ&ªtS•gK\êüœ-Œ ¬Ë—,-“ÖC(Åõào´5¤F Úrº0Ÿ9¯ÔЏÎyÔÿ ¯ÛõÖ(´ z«*Ÿ „Æв”aêÀi‹Gïïk[ëñTÆŸ£Ü xÞ¦åuº›kÓÓÌ9Oh\PGB ?%ʧ–|H©‡â‰'±ÁRò%­ã]p´³1 7Ô{Ýjî|üñìO”ƒ"Æù8Jð_ÅŒˆÞ Ÿ;—¢âšRœuBg·*V›²@Ì|ÅHS:·þpfæ“1/«ºGD~’®XmÖch&lǃ•ƒHë ›73ŠüÛ¶¬_×ÖÚÔØPçq½±€!Ÿ`¹Üÿ‚ï{ŠùÏîÛ³ÛF­6džáÚµbµ—eÚX¸6øp˜ùÈ]J²$ Kð€ÞôÁË ð¯¬ÛvQ»Uð"ëþ ³Ù{9IÚíÖ5ÉÓY$OágÀÈk¯°Û0VR±W@¸H4úØŠøì|¸e9,˜¢ÛJ$\ºÅÜ<']yY ÑKÌþ+ü[cD[¯šÿpˆƼd°lKŠD—9¸q bD¢R±”V¸L¡å¥¸aY,$‡d8ÀèÃ_uåÒ*g’¡X-ȼ¿‰Y ë†ß÷èw¡.·¡û—âò”òç¤6â°Û³>j­ v7 æ…`AƒO¯™Ï-ÐÎ7ÝxêÄÕGùÚ»aÖ³uó•[®,Ç«ßÔÞ÷/êŸÃ¼>¥;¡UíÓ`OOÉðRqý]Á0¿EÜšÖÞËuU¼ x ôdZlÝ\¸å¢\eœÅ}ýþ’¾¾Þ¨×Sh«u'öõ}зÌõêwý½¤fnеb¹þŽsŽõŽ% ÆŠôá¥y'wÂá–ÐÐæé)ÞÏÌcK÷tO‘m^,«á|_/üþF4\±"TóŽÂ cÖ5´˜u—)ôÌm=+/„òð$¾\Õ›¯¼õäÊ…+Ž^N[<"Ïøòýt˜÷ÓÖÐO=×P·Ë¬+[ˆº¬{~…ëë/Ä^•벘(ƒãZ±û¯YZï<|ˆNž8ô–ÃoYÙ L²ûK&;0BoÁO}Ÿp0.Þ³XÙIX’¶Nß´÷é{íÅ$…h¦åOn[‚pžîe‹|ª³|eñ'yŸª†‡þ,®°Ãa›Áþ± ޵ݠ¨Ž'®EÆ¡_›Yb°Q´éde€á\J3 HÊAbLjÃÊZàÉÛÏ̽ÊA±·aj‹ô%øüx^&A> ,–‰‘’­Ô„óp»pz×R€"—Û¾òC@].mÛ2퉼—ø.œ÷¼ÏùÝ–¦ŽŠœŸ'®—v:‡³ŒVR­[^×RUÛ¸"*QÅå0becÅIÇ—p5È’uæ¾mQÝÑÇ}P·ÍS§Õ\÷mn/³9g¹>íÏÓž§Óiq™G.¹ú0*톚x*rgZ6¯¥TØ+ö-‹9ïîðR›Óí´¹ˆ[qºËøÀ‡Ußu±¹ít•Cʳz£É¹öÔÕG¼|ßž™SKÛ=ßrm'úÚbŸûÏ Ÿ» ¼Õü‡+ïs‹c,Ë©0:¹Î1¯›a¶rñ£\‹šZ´è¶ÕhóªçuÁ»WÙQ ÔÔí^³¾ób’ΕšõÄ FOœ>qúúkWê’¹œ®¼]Ëûæ>‰þæ¹z}æ>9Kü^Åë/S ·Ëãr{òÖ?È­¿‡¯×7C|¾|=æŸa7ø iß›ÁgßrDÐjuãXsÏÝo=só'¯¹úèU‡VwBEãN‰/š“ŸÐ„îw ³/¶™T€Ñ­˜_ªÕM‡ÍW°ßBþ[¾¥ÕhõõM+á2TrY”b ßwÙâDÓËúöeåÉçAEr}ŠŸ|uâ¸pÌ¢Y®Û\†!Ÿ_Ê@ú(ÚZ÷êÆív׊cÃEáÄ9Ç×jÇ›åQ£™\5.>( ÂâÙx‰ùh?pN.½â¦7­42øWÑÏÑèŒ1G+îÕü…«ð";ˆ²„*Šìç.ŠJès3¼>UüXqˆñìËkøE`“¢Õ #ylËZq½ýÚk±û\{Óµ7ÝxzåNä7Ï+Úq>O¤§t'ØqÿmÔç5÷¡Yâkë›_¥±õƒ± Q¯×?Cüþm¹hÏ9AbÕýâhèé7Þ/–¥ÂÛeÝéëø˜ñ¶·œ¹îÓ¬vÔ¨4ûO·-¶q8gÍÏY>[4gw-9ƒÍuïò+4EvGß°2“•(¿XÁ×dW2nÓ¼æ¶%ÆF1OþdQ=7,Õ{–]ç,º†Wè+-™ê‰Ë Ë‰œËOc myYI_«)´%½­¨Ž“K)dÉÜÜgž›{—ìL8Ñߺ$RÃZ-îü¼óR}…ù îò E›Vì‚%8Ë.a’fìv«ínbPbäzBä!å(q“j²GÔ®ŽP7{yØ(üä|²‚GÁ‰¯ª¤  ŸÓzȾÚSñ$ÃnÍŸ¢a ¯a ø,ö…©$¶,Q9õÆcëþùè»æ[Çn?}æèRø¡ïl9>=v‹üÕÌ`ûž›6]ýá[Ç”£oTeÜM“'6¸õêj`ëyõ#¨WW£^ý„ïÕ=uô#JW’‰IxÖ‰MW܉« ãš¦¹5W ØS1>?ûðEõzÄÃw0е¢]ô­Ù³ä‘ó½k˜ÊÓYÐ6›}¿ […ØsÐ\ •Qçdæq80Ö^Q*Œ<ÁÂöc«( jp€Ñ;¶nÕvîîlO5%±úÚ€Ï ÍoÕèz¶^¬ ü)æ—œ§w¿;õIƒa îÊvÛìÒtùÂû4FO•'(îÊk˾&}^ÐkCz?ãr>ß}O§ôSE“©$»øÙ] 8VTyÂãU v<»bѬP "QŒî6pü¸7ʃGdüàôÞé©d,š 'ãñ”Cä’¸Å+Úå6¤û¢]È¡žëôž5v&c”‡9YÃW ¦3Œò–®«íloiŽEj{ëz=Íž¨•ŸÇx?RŒ—_#WŒ¹Ø 0b(Ò‚~`¤‡a¼T4^¦ÿeé\ž_>÷o@ÇÅëm[Æ6ô÷qªB·ËY´“uæã®q?"ÖÞ¯C÷Àkè‡qîSVÁ^ ãÈû á j ¶µÆ£ÁH0¼„Öª±Þ øîRnD|7c¼\v&ÐÝz‘dÀÇò'$Ø´ âÊ€À>ŒŒÜªÇ ]p*Ù¬º|À© η·<ÏúçÛýLɇ¿$¹[BaPƒVÇ0¢Ì‰°J‹Êd¹BÆKÅ(X`QÑ´¾f}ÿŽí›7éÃý;×ïì펇=10<žˆ‘[Hðw°˜?rŽâ!ÞÓþ0ª.^ ‚›I¹ë K˜‹ŠQ8œ]†¯À³3Ó1oâÉ)xªÏ¾*uŠ˜£zl× ð2¥o§T%N@ªqÄ슪áŒ4ŒI 8`MZ||‚Žö÷µµâ¨U_ D-Õ A4ûóÌŸs:»hPÔÐÛÖ­Ôgôy+µ[ùͦ—L¼L‹âý¦‘b²°x<Á@b¤`ŒÙhÑÅ>žxd1 W)Ū[ ¸¸K`ëd±Y–ÃdºhïØ>±q|th C"q;B©¾®¾^ô/ ¯2Eûr!qx¿w4’h£Ã4ç€ju”û¡‘D£¦»:2XO”GÌùí}N¿'Ö;=¼~ÿhôÔ §×®¿b4ZÙ¾­«÷@£Ãá¶4 ÿù©™Ûw7;w®Ÿ¹bÓ 8Ý{M‡Ýî¯m±ÛLmÞݳ֡Ý?쮋×ÕuO4é;¯«lˆUµÅ¡êÕ­wmHnØÓÚ°iCŸ»êÔŽ­7íj UÎíØçrǽîŽ]'…Ýàú%t¾u>Èm×¹:B>¤;‹µ,×ÐK(†ôç¯u(§x¸¹VX@‚p9•…ž{øÐlzzjbã"íuTö/ õxYØ–4ŽT÷ò‘¾¦ï;ç1f~hX"¶ÜÀmÝr! P^‰±(}¾|¹’zb‰BF†¼KÖ+kª)m_ÛœŒ4TwÖtÒ*Z bvãŸàÿÆ"þ%òù`9•¯AL;*8 ò½Rç˜VŽ­xù2‹¹ÒýÈT¸¾:VË1ÄOÉSru6+Û`<‰“¨Þ°FÅãi£”‡‚ÙçTQœÄøC¨õ Ó>UsJà‚â_}ë_îfV%à§mÛtœ~§=ê?zË3̪‚¬mÛþŽÓ`ø©ôò‡¼©@´š­Ÿ½að÷žqW_hcâm Æá;qÕðù¤—¬!µäÐ9·I>µ BÊ4trGyÙ„K¿—ÈÅMH- ö˜ªdµø06ˆ¾jŒäú²ˆ~Ûêœj»rnªfðé›öŽ);ÿûqýÊ¡}ËwDÈëËoüÁõ'ÛÓ·ï ¯_X;röŽ»:÷?°¤NÚþࢶ?GÞ <=’o{ì|õ…3ÀË©nmÁÏ+«µáÒï¥ ë§td¨§+Õ®Ïé†Mäí8šÍJߺzmF¥ð ¨ è묔ñÓ[æAóÀ¨Âæ,<Õ¹ÌUH®U}}}²¾1öÀ5r€£tH_Éû1Õ€æ8Õ^P;œäÀÀ·P„'êó4+âLä ‚ÇH¹táQ[˜1àdz¹ª0¼† ‡"ðÑS¥\Ãñâ‘J^zT´Å ¬[\ØÃ.B¶Ÿ“L-Qi>Œ.¯ËðE«õ 8jŒCùCZ·&“৆„ã.Ö¿ …ë ´Éð7:àNA^U@3uOÜYfl"«´l—¸³…SQCÕz£Ù¡5 Ê•Lë¿ÝÚcýmqƒ¿œJŸÛç5['÷×Z@/¯üµ )!‹,!ÿE·áØàq­0.¤/ɸ^b\ðì™ÁÕ…ÆhÜãÆ“QGnîòlöUæ1b¬ŸåcFdmƒ:QtZq8Qóùak>¹óËhã@;Ò¦×i½‚R\·€Nï )ÕÆúñ/è5Òù5GÚW‡m÷×ÙG3*ZLîˆáÔãêVÎÕniŽ4TUÆeã5éËô'Êç8øÌ(LˆzV¥´ äÏÞ/=.½Oy¿ã3¨Ô ýëB ^ÏoÃûoŠ2ü™ÑA3ú àØ+¾ã3‰Â÷'¸ß=Èã³,çª0•ƲäLæÉ¾H|yyN:JèD2íáëfùÅTçî 5öÔÕ÷$C¡dO}]Ocˆ ™Á×_^ Ï€¨ãÊ[ïf²Žè®j‘BAfeJ­è˜Vòpšlƒù5³Z˜õ±ÅfUf¼ÍNmxP>ÁÝNfp ˜2t­Î1áW.O±E„gä•T‹*YVÁI9,úè›@SH…äoÕ¬ ø÷ÌLnCóÞÛs{L¾àߨœŒU@_3dßc´í¬§]60o)°ê…oÁ&Üœáü8÷éíë¥Ý ñ›ûCêxÿ‘‡vßžì?òàôø5Û›.ÀrŽhR£*S…¡Ccsæ/>µoø¸Q”) ÊSåXÙriÝ944´ihcc²-•Œ[r¹Ëóô$MO×—ð¤0<<©+òÄ‹2•K¸åyr mÚ4¼I0eøQ‚§G‹y¢_¦/fÏÒ/ˆD®!ª)ijÑ8‡Ò(ô"Gá53½ö–/í/Wº¤ ¦À;Hî`¨¾¦Îƒo¡Yð–––‚`©9^,[ ¹í4“â’^ -¬Pª±b±7ILïÊ1UcêhbM¥Gò e›§´G• ÞØÆ}¢­çéßÇÉ?eÏ’Šõ¯Õn£…©–Y;UÀY¥+pÙNÀZ˜¶P®¼>††‡Ò{gvíØ¾eblthnx޳êÈéeæIå5å‹ôò»ämD#G‹y-ź±<wáb¸š{l…bàü'’œMÌs‡1m+ôY´3óuïï–¤zí]yiY$Ë1¢I˜a¯ˆ“ÁC#‘Ê—Ù­0tpd¥Ù(/iQíX¢´0ŒmmŒ¶­këëîlJâÚ¶ß‹»¶t-[k¯Z–Ç'©·ˆGMJàѲ4»M<òò’x´PK9wçyl{ºÚÖµ¯[–ÇGó–æ9°4ßßÖF­Ú6ž>ÌèÑÆI¼ñ–”ñ/*cþ\0(xšI²Ú`®µ·Zæñ lÞx\*»¹¡my(x1«ýMD¹ÈWMkÕ^[5ÙØ¯BV_—‡”,VÉrZÝŠPÁ—‚ sSÃ(&æèêX¬³Ì•èéÇÉ߃za$yN0Éb›uòœ`´<ëE¬÷Éœ[–.…Kë1ÔÛm[6ŽëÃý=Ým;Úw,æÝeðžyRÝ ö¨˜wF¿{R#·Iù–í÷è’òålÆï"„ÆY²Û›Ë¦õJÆ7¶3fã{òóÙWecg¹ú߯æÛ¥OUÂ,7–põ“*8ਠ9"îäP£8WǹcåCCÍM@Ü—QLÎ`ó`CÛI“,i3âxÊÓ|‚|`Mc 03±¢"Mžž°<ÍaNS¸'ËѼ¿˜&}™b^âO›ü”½àPpŠý”½”š^{Ë—ö—+]RÐlV %4ìb¸L‡C3ÑÔý1ô„ 'm©â˜bµ­]K×ÆÃ¡”/f±3åëøŒ1KÔ1ï]ƒìòtv+ÕqE ruÔ,è~1ËêêØuÕuühqÁΓ$Û.L“Ý -àå*,çÌ¡ñ…ñ. ã_¦\ñ‚iD/„Ym("‹•¾Ä~žCä…e/+aŸ)1+Â->n ¯ ‰•XWŽ0=²*Êþ7M¹òW¤1œÔrð9÷k)h}ýE–(·ÐAJ'`´Ç} Êæàù_…®ÝQ¤k`›÷5‘'J{SZ …âí\€àŽi"^gº¢\.rq%ô:ì,ÜȬëíìhÛÒ¾¥P—¨GæƒJ¥°±…zÐïþ+!·—Ö£±´‡æ¹-öãR+ôåbOÎ+B” ¼ñ»™‡”WØsê±F¬¼|ýr¥5ôß ;,Ÿ’|j€ïmÀ3À\Xq}¿S>E¾%`ð„{á¿_êÍÁÐïoï0øL¢ôBön /;¡|¾#%÷é4†T#¸Ç#óxßyƒ&ðolð™Éß}x²3ûªôå&Ôé¸ÝÊ#2{ ¹W·WQ™…Uð˜±LiÐìšM2<6î?„Æ0Ë0ÙM©±-Ú´T9Üå…éîܶ^moOKª¦æNýÃm©î–îx¤º¹¦¹¹1,òäÍÏgÇJÎc+!{ô]Ðke¿ÈŽ‚¹’(Y(¦mÍeX™ó?mÚ88j†YÛŽM;6Œ ŒŽ7¯O­oNDSŽ:žC+-bhZ‰óãv*™Q˜,â¤ãv%n§Íg÷©2££ºR£~xhP4ÿDßWU©4mô…,{ç«–mSþ íå&ð_ì®×ƒþq8_GZ÷ɇ¤S¿­Æ8»1­øà–@µýû™XÔõ¥µ=óýbb¸ õzLá{mø z’É¢ž—ÒuØõ¸ì~‡_S¨ÙTîgß¼^kÀÃ3e6 ]%4åxàÿâÍÕ5\¨ëcɸóÙ­û…Ì¿$bžïAe™¿_,Y¤÷ù’zv‘wz½ %^·#P°¨ÄNíœ^ )œ 4ž¤›«âÕA;íŸ Õ»2'2ãk»+éé}þzg&¢Ü¤ØÜöÅtï‚znø•Ût6u¼k ´äWþ%v=¿Ë_eÏübq›f­Ð½F›Ѧ™†Õ·iÖ®|…žQ¿nØô¯|–÷èM7÷ W@èDy<‡åÃôiuÔàã0•ÉÜxÒFš³|ÜS)ˆˆßcãĘ€<)2 …hFBœâQý¼ì |Œ6Æk«½ž »¦Ò jÆÜ'ûnå‹ôíCÿ_º”ÓmÓ[06÷ ‹e2Û›Û ÷’ñ` ®¯­® $‚‰d\cÈù4{U¹‘ã‚gÊîðFõ›|œs„{xN6Îh…óxÃòié³(C‰Çí°© Ѐ"”M‹öŠ]7-:,±æÎLTª·ÉŽÔäààžuµÊ-jëÞ»•›&öw7ô5ƒ} Ýû'0|„z€Öƒ¿­…½7ŒÕ[e[ãDߺɮj Õ²ë-ei‘¿¹$8­ ÂIö!AF˜)‚C_ÊFZw–ç¤{-ͱBßÞ3Ñ´›dGe PçsÈ7iõÝ“å8Á1»êý¼h#|äË®º ¿ uyÉh㯊6þÚEÀçÛé‡ý‹Ò‘.€Ïü íö±Cwo¯VÏ(Îö=££û‡”[”ö¹ËëÈ3òiå©7OKzáÁçO·*w¨5Ó]süuj·É#wü·2´ Þ ²8aÈ•À³ÄXæÌj%óÛ‡§Mr‚^P¾ÀÛŸ“p•V¯¡Ñu`œÏ¢H‡êÊ8Ãú_–@ybÀâ3àfÉê ¹>ÿ–á©`þͨ»×]R·ð :ŸwÞÑûœB˜M‘LÔT p~OÐÿwƒ¾ŸÓ—³tµ~uøÌ3µä#Ù—ø¸—ZÉ< Ð#â PˆÃí'L’Ø Ú%ò|V뱞Æ$vÜô>}ÎãåÎ5ïë™ÿ¸ÿ à¶¡Ý4ã“%ù8ºä|Ac}nчôyôÖ=ŠÃÐûº´0üÍü õÝê£M™¯úoÍü`ü}ï;ù§Øk䫨‹ª· ˆc)oåÐâV6¾¤ŸåÍlÜÇø?ø[Œ˜¯ `çÊEèaù/gÞCîÍþ;ççß¡´#ƒÑƒ[ιÐ2–$è˜r…Íoaó,³Uæ÷”JSÆW‰N¤ÏEãñi£šÆgM4*2ŸWŸ¯p0,ëàñ–àqIûE- —‡å¨bÏjñG`Áô¬ô”ñ•°Íéô¹@<ÍéJf·|\‰äÏ«òSÃ̉¤¤OïöSÜÓÏEƒ7iÑË,шª©{ù}3F8p 8z¢¾°×БÙ&½ }ñvðy°ÀË³ÌL8±wñTx&?n^²`a.œq ÔTo™Û€¡ºù90¿;+[‘ÙFÎd?Æëý1ä,ƒ'èÂzqØ/ƒÏ fºò½O?Ï‚ ·›Ø·%6É rcY¯|’ökŸäðø,±‡¦Ä‹ÁãLM…kB¼h“+— þ… 7­•½ÒíÊçùxÏ '„<£}n(¦Ø'F½Æôh„Ñ¡Î!’ËZîã¯|}ÌAs í'¼•èÞB1"l4°4%óS»ŒÌæNÎâ0‘~6æMļüô9—ë4ðòŒà äê…:WfºÛœýîòõÆ72MôþìQc¼? |­É~®×fÓѽ.Î š*»É¨ÆãKÅF<}6TÉðLG¶î×ÉtöiÞoŸŒUãn±ãá†r,—Ï—¿õÇÉׯØÃVëk°«Bße ¼Saah ã5ì,”w³ªÄužA 5¼')FŸýOxW)¾ãVpTcô5%ßWQ†|[>F_PÞç à™±Ú¼-ÃLxÄšð>ƒýI"~OeÎ×ü¥|Œü/–¬Äj³U«ƒ&—žü£¼MjU¾Åááx¯[¼R.• r­‘g žZ÷z"áÚêPГð&ñ\΃ì.€ÿ‹À³ÄêJ×½Vƒíãj˸ÉTöy®wϦú̇‹ô`,ã"‡²Ÿáß?ƒ-üƼþ©Â÷ÌÑŒ‹FsßI¾W½þ)*)Of·d¿F,çlEbBh•|}C qÃgÆ"FkˆÓc³u»,‘&*=~Ñ”Åä{¤I€õ-_>è–})j$˜éËh`Íñ í5;7(ýWŒÅZw^£ËÿänX[ÛÖsyuÇÆ¦Žýõ¼½»€§ à?K,’]³J/|6œ¹œÎþ—å¿t”÷¦}mkS2ÖP_ãw»V‹ìäYE –„ñX€vÚ^’øúHæùÌûÈ?f_æ¸^\±L’û6L±—WGEp®ˆóïüúq·!wj‹¾0&Oå È0¶é¶X,–ˆ%cqâr3{JÕyûÂ3à‰Í#b®QÉgåûeî€?"³ÍÍ6‚|NƒŸÉqóW½Úü§<“hšwñƒPipœ|aŸGÁû;²/Í}‚>ô‹f¨uœKð7ÄoÃß:PÀ‘לÙÃàÕ&ôh…C…&Ô…':Å7Å¢%6 ñPäâ¦/¤Êá†Xc_$Ýw÷?éïê?—YŸy5ó“ÌΧÖøÄ‹ÿ”y!Ð<ØxíÌþý3×66¸ÎÛ”WÔÿ©þ¯/UfàaˆµóY‘¬0a8göSÔõª¹9øó¿+Ïs?ðÜÍõû¢±]Z\à7öKã†Î½ÂëØ#îO­ÐÀÛ££˜¹PåÉgñ‘HÓM‘$"!ªE|_,±†ók¾ýô³yFÞz2oy³/=>rp|ÏðyÎóúöŠ(7‡–Ëy…ë„ÍâX!OóË77 Ž‚oØ `{ <ç«7;}±˜~ñœÛû’‘Cõú˧Åz±Ó ó˜PÔ‚½á·/Tñ"-Î>DÄb‘` U$:§âãÊü£ðcG ™0ŽR… çe b½Ä3`þª^V…²„%…’˜ÂŠÑªÊPÀ‡Ng…¦ÐzVo1æàP—˜Q¯ >¾ôe÷ý¿^³ÿëum¯áŸrØãµRC™y¸;èi Kµblþ ”»1ïC`X—­_êÍÁ·oSÌ[9oý"kYeÀeÃÌø)<½Ô wT&š"ÉH~MöOîTÝþlÃÅ`¹48ÈßJß—ær¾ ô}¨ ÞúÒª73 _ãIØ`høÏ‹Ã§‰d“ÁKÀþ]Žx–Øú̉‹ÇD.1.òC¨_Ÿë‡¼~ßËgb§óvÎÎ.ŽoK$“”Ý °< F„Ç-Yî‘.Ÿ”})©‘†$‘a'—‰ÿŒm§woÄi %ºýáæ ?RåÌÜç§BÉnüë¯qÊÒ^zíÞÌË•-a_²2ÕàsUEèÆ5æŸB~9^sòc¶?¼ø:g³ôˆ¼G:¢ÜÁ×Ýád7„ÞèÇqeiƒ¸@Ú+6 qfɦ9DP\a³(÷‘ާÏùcþ¸±O˜½ðÝ/pƒîîvÊD.vhó¯¾_¨2Çý5Ž»žìU7í1¾ï}FA¿>{íÅ—áûTÝeö©žÈnÎ~§xŸŠ4f_“~¨>„e ‰`¦8èŽûÅ3¸â2H­Zˆç׳Pͪ#VÕz ¦¬yă£Ï$…Iàï+ê4ž6v˜“ãQ:>6ª¯ë¥=´'L46s9·9]/Õy†º1]ñ°°eHh¥$ Èy6"FögÞ#\úi%ƒT«z.?µ²N˜€XãTヾ‘LoÒ¢`Œ¯á¾PhÌfaÜ-Ç©1N´ÝÆÙ8±‘$˜Ç1·Ô´|y ¨hÁtDå© 4EÛ· ¸È;“­ÊçaZ&§…iƒÛÖöv¯lŒ‡£žX2Ö`¥ÂÑHO£È¢•KªÕ×;@¥®Ò¼ÀñÎÞ>ŸˆøƒÒ»ÒÆÙ|ÝTó¦Û?v|îÖæ -qxá®5ÖpmÊa3¥ÎÜ5}ÏàúÄáÑí·îY{uô–ɹÞ#ï>p÷7ß?7>4½ðôçw¤1ï¯ÇÓ1sÍàùÉÍôz×È-OŸzèn®±ì«R·È1C}x#‹ôÞ5 M ÆçMŒ÷03J9„ëÁ¹¤0¹LDó±—'¡ÙG3dO+76ò€hT[‚Z´ ˜ó - è1|ÇѸQ8¬â4#fÒVjœ—Ä¤Æ w¾ùØF,Vbµà"6Åk& 0¦¶F|ž‡_;²¶%÷Âñ„Çm«LÑ.-*iN çôá">4ëˆä‹öuù†¥|µD4¢Ñ›çÝ’[ÛÖ££ñ¾ú:KÐaUÚ(K>åúð míÖ£c±ü‡ÃAÖp&óÑÉ©»÷w»½[Ηvhºó ýÙÔ]s]ï– Ãùö¹ÆÌ¥ —VÒCUwÄ@2N«D¹là§Uü,H'f’‰Š)BÀ„(J—¸à‚/œw†T‹Š_Ì`¥uWÛÚ–f’7ÄÝöÊT¼‹‹¨Q--#%)ê¤Ú0í£_ÿë€%1vp´§³}½Y\lÿž5'Jd5Ób‹×vwYïüÈà53¾€!1ÏÒ»MÒúå?Ž]Û*ïÝ*tô0~T9Åíù;PÙ÷áÃæskpÝltÛGSb#Bõs[…<¯ùO - ÷?Dž×s‰h2$š§ÑDÎyxà¼ÀÅÙ8 [ I˜0V’ÓBJĪH!2N„s~!ùÔïî;+¾Gøƒu|º ÿG…â{}ë5E_©JÚt€xøÐý¤Á÷8mÆIx¾ Bò» ¦3÷wJǙǀ¹“Ë£]œ¥§hDˆ>NÐöCÛù‰|Ûý Ôy½û7œî+l;çà—‰° ¿‚¹ia²×Ž#Ü©¯VpšJ§Ì×ñ JÊ&°RñD,œ÷5¾ :rРóeN§ÓˆÿãX.<ÂÅÀµ‘NË >L—XsËlQ¢q†ʦƒòá`·H7Ž´u¼Ñ ý3ާ[dw)ÆyÂÃ$÷ÌÍ·=ÁpHÄqýþy‘~¯+·8°Ç# 0f›~´“‹ бÿ¸¾aàz…ãÂuˆ>½»‚<øÂQW©A'ŸÂî€9qe P6ÔUUÒ 6‡½ZmnÍp¾là|ã\odTQ—yôú$º dšãÇD µ`9_ùgé˜dÈŸ~ úŽŸ”2Nß…ðôÏ[aW•¢s™?…vzÜ€ÿ)§?(V4b•VI2–{1ÖÍXëEޤOË`ÁF¾ =ÃZŒ³Dð 8îz/äAA2ððв”£û÷Ò3Ò; x&é. ÄÒ]ô¨8'NA‹ðúƒxöÙçÁ³ÏØW‹ÊãäU#žú:$öe½|ð¤{8næ”xŒ÷óðô9O¥Ç—_Çø5â¦~éöj?ïð òº>ôê]¸óVËøØž®±é¯.ìÁÍ`—U¢u´.šhâ:}fË©lÍÏ‘ßóõËx³It[tm/%*: Ià½æ™À¿;@ñɼëK<Ì:çz5LM›/WZ$­;#‘H2’—(÷X¸ý»ø˜5_p"nj9‹w|ÐÜ5YLUfK1ãjÇibÝ.‡ñ>eʨ[TzœgÈ‹*#™¯f?7¼f¶d?O~¦»Zª@C;c¬*æ¬æÉ^-6Æë0o× £Së x×þ1ñú:^µ‘¯µˆ:oÉ…UÁÄÊÁ(3˜6¯T,ݦBqbU¨uŠ,Ug½•©¦Hdp}wWÓhj´ r‡ˆ­ß·„|†3\>z¶äó-ÝGù¤LòÁʶÛ$j\è'~°‹¨¢ÝÊT5ÿ+_a/ù¢‚Y^R›ÁŒt‹$Sïð+ÑÔÙb˜œ0Dƒ2Seò«ƒÌ’X38‹ ŽŽnééjm‰ Ä„(Œ˜ÍÇ@QucAäEqSiæòc®+¿Ì¾€º¢£,6k²pä® hÇ|>]œ“§‹«P´Y—™ã÷ÒûÇÄEÕþÒ @aJ9w18ôžeÀI£Såà@0á‘¡hóô ]6rY_WgûÚTSdärsß+‹w„Î*zÁNðû[G2RöoÉUÅvXÔ,êÿÅš¿²)g#²‡Êò gZ‡Ã¥<4‘Ʋ>jÍ”5j$Õ+a"™cBØwa«‚%¶ê¹á³ãQ°U¸ªÅ5]Z?O€£ÔÂÛwO ¾O{†lЇMø40 L+6}˜>‡;Q€æ”0¹O6F:¢œ@înCÌ‘²›Z$»áÌIèá.Óý|OØ?‹¹£[”²*áÉPF]@äÊA.W”Ó‹õV+÷Ë®É|Vþ®X#½ǺÏOÊBÙ€‡9‰\.ó+a¢ ý'²<%¿G76Õ½|^aì3ß}Mþ¬1öÜÒaÆ=‡Êž%—ë{kÅê+üñuA\å"ãU.ïrRÓ¹2ÿ˜ƒo®ÙÅ¥­-ŒîÝ359±qh ³£¥·µ7®ª ø6šb©Šª\Îúvq?!æ¼–¾lPþPÓu+fàÄìß³¥·lÚL·lHïZÛÚÔX]ˆ#ɸ½†¯ûîË<#}Jú6"›È{t'Ö8·ôðõ]Ñt¹ÊÁ«…ZÓ…5?Ìæh$Íæk·ùrI©ß ºYÖY­Â}|™ ñŒd±¶SÉã¤p ‹‹x‹:Y'¯ 8|þ†8+@ršwÍË šw š7­†æ[³¯jiqÇæù”G¸oó;H7“Oëî=´Âdd½‡©Znz7äv2G©@§]V4EÖð¦3M=âõ0»‹V8ìi+7¨(-ág‰¥4nâ†sðèr_<p¼wîÜyjçÉ«_upþйô¾]Ó“Û6oŠúÑxs,õá–< î̤âçò|}y«¨=¿îÓ›a¬wIâªl¬YÜfaü‚¿y#O¶a¢l¾ãsÈL’*pÉ!‘+G–+ãÈØ¥cWŽœ›Ý·w÷ÌΩíÛ6ŽÓQº¡¯'ÕÔÒè4Ö¸èáìãêÛÕ ¼ýóÙS ÞšÆoÎÀ›zžÓ½5T†>ñ(ŠlçQMÆ-.¹ëÈ%•g9޽=¿·çË]ŽÈϵËA8—€(.,1É€(W,¯kpppãàxcSs<oD'³åþ§=£PeãüxÇÿ-­'xÚÕTMoE~7qí¦M͵T¥z•Š¥ìzÛ%©oM•(’Õ”¤j—h³žµ—Ø»ÛqÜÍ…7$nœ€_Á¨àÌ#'Ä¿à™Ùiˆ[\)•@«™yæýþÑ='!‡Êß7X%v¨†[‰—¨JßZ¼L.}gq…êô“Å×èúÕâ*Õרí|iñuºíühñ }àülñ ê:¿Y|“n/Õ-^¥Í¥-¾E¯–>³¸Nw–?B$NeÞYþÄb‡ê•O-^¢ÕʱÅËôyåÔâ Ý­¼²øš³UùÝâ*Ý­~hq¢j×âëäW¿·x…îU±ø}QýÓâ›ä׎,^¥Aí+‹o9_×þ°¸N•;iVäñp¤x=lð¦ßò]lm>)XOÒq ¸?= ä9?:Åà<y« )ò31àÝ4Qü8˜^Û òq¬Ò5ûq( î4ˆœÕHðÑ~Ÿ2‘” V`ƒŸ‰\ÆiÂ-¯åYý}Œã¿ÝCýž¥»%£×Úî´{O;nÇóñu¶»+Á¥Ä%›0Á… Z=î*Nø$Ha d—×qÇ’Vy0“ ?…‡…Õ÷1Þ¸ÎY–jÇÚj&¢ “x\èj I(Š“zÓ!è+§3#Á´ K‰ñü& 2­!²2c£—âî/Úš€´´ºSã_[cc_ëÑ>$™™–½ìaÞÂ(ÏŒ¶=5²-xÒkÞÿ>öÀÜB+á£=[‰>м¼;§Ñƒü6uŒÆST ~V|»:àv±ÏÛà9ÿgÓCpµlËÜú@ ’‰éX`´t¦öºÓš3„¥4]…ÔVO×QBVÏCš4uèJ™ÜÄW ‰{Ù«Ð Í—&ra´&-ŒaìGøz&:…/wj²™8˹;3ÕØ¦ûfþ\dδeŽ« ®Æ{v6ËÛKì%š™¹ÙÃÒ·çfÞÙ—7ë›È£Îu`2×2§6‡«Ï¾wewsÇ,ýòműê7 «š>Dæ…ÅÐ/.f£|IåT•““š×62Êè5ñÍÌçY›gãÙþ7¯,yËÙnÎå~üVîÍÿìÿjýÊÙ4þ‡ÿroç9ÿö=£1Æ©ßþüxé#Wï}ôþÖkIqxÚc`f`øÿˆg2bÀl5VÿÿxÚ%ÒIlMaÆñ÷ûÎùŽ{%!檊Š*6¦ZÐZ¨ÄÌÂhMm mÍS©YóPEµÄ°&¬°-†­–j!ìØ±5þÃ=Éÿüò,nîM^sf–aÿ?Þÿ½š@'Z>šb¸ÐJð «Â{l/®¶ƒøÕáz»‡ïóDöÀ­1çÊüs>ÓgZä‡ú©xšŸ† üu|Ãß1ïïFçÍE QƒEÑ…x—¹¸*>LÆ'éÙ¸‰6Ç-ôv|›¶Æ­´3î¢oãôKü•þ )s¡_èG³B²- ÃC.žø=¡4”²”… ¸2ÔÒºpœåDx‚Ÿ†gæÃóÐ_†.Üô7— HZ” Jø/Iv2 ç&|g’—äÑ1ÉX–q=¾™ëñ=åͧ¢ÔMs©[©6ü0ýÁ\úcú“EéÏéøgúþþÿÈ,’“3'¯ ÷T†yõR/Ü[}Øûj¬!ì™Êb¦l–ás”ƒG*Öhœ§ñx‚&âIšŒó•§¨ªO× \¤")îJ§tŸÑ|Vçðyq‡jÐܨF|Q—ðe]ÆMº‚›ÕŒ[Ô‚¯ê*¾¦ëø†ÚðC=Âõ·«wˆ[ÒK½ÂêįՅ»Õßè-~§÷æþ`H‹„xÚí]xÅ>3³Ð;¡†=é=!€!¥ƒØPšR‚‘ÞÕ§TáÙE@EìOT@*”J@zoóþ!~ç|w2÷ÌÜÙÝ“)ÿvw†94‚!ñLÿž])Û =ŸëBþ]ŸîÝj!‡5­S²IIeè#“4|úÆÉ¢$‚BB©`èM Rñ°Ðà‚ä/ÈV)†£4¥‹NéäEéŸy¹×Ë¢â'UÜVÅTÜYÅÝ»<׳;õVq¸Š‡¨x„ŠÇ©ø-OÁi¹×uû˜%)æ¸ê Ñß|Ü+Æ¡,*‡hÔß!WÔÕéTœBð½®¹rÿ uäG'YªÈ°HjÎkñîÔBÌÐ0ñ­ˆ ±Z9­ý ÕÔ‚èWm²6™Vê>º­Ò—èKhµ¾WßKkôýúúS?¢¡uúIý$­×/ëÿÒýš.i“áoøÓ#À8I[Ófö¶ÙÎü/»j±ÎðÚvN;¯cçµËðö—öo¼•}Ð>É»8þŽ?ïãÌrþË_u>q>áýœßœÕ¼¿³ÍÙÆG8;œ|$ñÁmÝÿsÏu}2PGšŠ+ÊG/R3EUÉ”»©, *Ôò–G幂<ᦠKã3‰r æf—[å9ù§ÜNº<+·Ë¡‹Évø,E¢M‚¹ubÛY©è¿Åä•c©B‚¹äè©nÈo!²ÓM%&È•˜ÅbúYÕsÆ&ÍD*ñB›ó’ð‰Dì`.{˜-ö®Š—ß"w¢ü(:™ç%~ˆM͈£ýRΔ»¨¥|G®–Ãdl.ÇË52T¾ é'¿“?ËÍДSå8yFކt•mQòMÊ#‡"g¥ßZll¢¹Sb“z²k>‹Ùb°\€Qï€<¨4{å¡8ù£Ú®ÿ=0½òÛDs¥¢æóø|drõ6å"åOr‘Üýmk¬~žüŸüP®º9(?0ö\~ßœÉêûæLv%𻿾9ÏE`#÷Å|½$˜»=•µŸMR©ÓnN@æ¾ìw·-±sÖQÌw«~`Æ“;h± щV·)èuÿ!w)dǧzÒçT¹öÙjCì(RÓÓΓm½M@¶å5Ò(£‹x=á¶;"¨DFʬØH¤HŒnä°“­òÜ¿GîýùÊÝ÷ɿνû{Ÿùr1läú„NÀZ[åNØJq®R³‹¸Ç;rŸX,–p™îÍ|–;*Çõø>â#næhåÙ„æÎ8ç{ÞƒB¢CŽh‹¬‹º×z3?GÝ.×ÉÅS]ï‰q¾¿Ïôø}f1éSª—¦Wú}×ÏPõÚžÖm£1ã—k!yãÕ¹›Êx˜@ó„‡2øwïÚ»míúŒî™)o7¶ºw#”9!U×<0޷ا7d?¹÷Á¹jù·Š§ÅÓM¹Ýÿ9¥ãËu.-OÆÑnŠWfÚCÞW.D)I(¹On’§ã|?s—RÝóûóQåqݧ“h±?Îã (yAFÆ´sXlĵGfL¾@"Iå"âÎiqfº°zyB‚Œ9Þ|÷Otêà#eƒ…±8Éý¶F=°_Ÿ[0~¹Læ/ùWÌHÅjPj“âØWb¼¬h0“e±(+TާÛy‹Âü!m5É|z@aÉ$< ólQÔxï> ùHß÷HØ=cu2ÛÛ¾¨çãß'p}¼ïûo•ó€]í¶”Ìx7äŠB\1³`´îÕ]×ÄÞO‹| ,ó’ìOY)ö919Zö’Oª'ú©ïê}‘ôrIL_‹ýåAù âŸÕǹ‘ݤÙùuUñaJæ“là™È‡ü€ uò’‡1G§SÍr‹#þ'NzK‚%–>¢ƒMé[Xl}Üñ(á>ç©ãÙcÇM(ëÆ³q Ýðlðý基ÿÞ¹Iä\7¦ò÷×G°I™sï3‹ô–ÝÕS÷½û\ÃÓòÙYå¼¢âF˜K))ϺœðŽœaÌ{œ™’õ«1§• R`yfÌáä1¾v´øŽ Q†4«íØ]>ýì÷Àb«ÑNÒÎbiÁÛŠÞ÷ͬ‚¹Ü•¶yÞ½S>×§äú†­ëïŽzB<‹E½Ed3[»opgzÊ]µØ%åé½߸ì÷š\ãy9Á {LÌ`«µM|ãa°¦øT—K¨Ç< ßèq,N¯”_E½EuçßnåcîhÅ Å=fI$Ô¹ù ñc.¨÷éOˆ o™ä1N"!O¼5N€.ÈGþœÙ{qÿ1ö=õ4™ßbŸ:rÞj‡O•{`±ï¢¹Ò´Zoª?îX»g½2z­=Ï›ôÉ ÔßiYÛ] 9šÿÁŸwé@îú÷j}•Œi4‚<¦,¶,ú[ÖTX}›[û<À-ò÷Êï‡Ýâ—w§½ËŸRô«+ןÞS¼ò …Ä<•|mûµƒ×]» ™KÖ”u¥ÙF|¸Ñ©Up=!1‹ÝÀ"åK›ÜÆb7x\ïÌ3H³íäñGçMT÷¾’åñ'q¹¥'$¹ožðØ Á`&Žc=!KyÈÝJßÐj§ÈIlß ñqÙÀ/÷L„)zƒ9Ö>X÷¯}=ÿÑd†·É/öÈYäv~¥RžF“ÌàicÉlc·»Cl{Õ ¡Ëmò >rñN¸é ¤ÄžÒwýÕ®'@nu×¶zxV¤÷0Ï[…k'®¾öl"ë˪òYÙAæ•¢s.^»,³H_YB–”þò†·Ýâ³$ùžœ!g?tzí½Z-ÿ\Ë.·¸OtÜ7k¨¦6ñô^r½·(Ò†FA4Úñ¦l´ ’‘¶BòÐß,´ ’Ÿv@rÒNHnÚÉL»!¹(RöBòÑ>H&:)@!Yé$;ýÉK'!Eè¤(IˆÅlf“ób^Tˆed©Ëʲ’ɲ³ìTŠy3oògeYYjÏXµ`•YeªÀcÑ㬠«B¥Y}VŸY "Öˆ5¢º,˜SmÖ”5¥ì ö5gmY[ªÉÚ±vTƒucÝ(„½Â^¡ê¬'ëIëÅz‘Íz³ÞTŸõa}¨{•½JX_Ö—ê°~¬°þ¬?qΩÀPE6‚ Zl$IO°Ql5f£ÙhjÂÆ°1T™ec© ÇÆQ56ž§`ö%û’Ú²¯Ù×Ô_ãר¼`‚Qzm°6˜Bµ7´7(H¦ £ªÚpm85Ò&j©6U›JiÓ´iÔÔ¨hT¤fFw£;y=ŒTØègô£âÆYã,¥3.¨œYݬN̬aÖ Ã 5C©˜ÙÚlMÂlc¶!Ýb¡2æëæëäkÎ0gPYs¥¹’¬ÂVajeµŠRk«˜UŒž´|-_ji·ŠÓS–ŸåG¬êVuÊaí´vRIXâ{h5¨’ŽL²Ð–2@ÂñÏŠoÞþhù„ôÇÿ½ZU1pòTb‚k–À/ü ^¨³$ÚMYHQ*™Hå©M ªT quj žÕ2‘ž‡ê ™”×qzuô‚L¤Wq$_z¢¾Á4ñÈ$ÕÊ‹ÐhH1IOc!Åi¤‡äÇq&àL—CrÓ HaZ ÉMë!/«¾ñ’êÏÐfH{Úé®úIWÕOžWýäÚé¡zK[ÕO:ª~ÒAõ“çT?i§úÉ+´ÒEõ–g)ÒMõ™ÎªÏ¼¨úÌÓtÒ‰Î@ÊÓYH¨ê?-è"]¢úô/¤ ]…4R=ª4㌣èL§fÌb…©>Ö„¥cé¨,zZzèÝžVƒef™Ñ+²°,hÇn¯kʲ±lи}/ˆå`9Ћr²\ÐçfyЗÜÞX…åeù ÉÏò£Å`.È ¢Wb…ÐÊ}˜úFaVú¢¬(zf1æ‹tqV}©óCº$+IUY)VŠ²Ò¬44eXôj·osÕ·uÕ·3³ª¬*å`ÕX52Xu†öÇj0´lVÕC¿r{¾¦z~ÕóŸR=? a!èÏ¡,q #†þßã‹ÛÛóªÞžGõóªŸ{«~>TõóÖªŸ?©úù몟¿¡úù ÕÏ{ª~ÞWõóÞªŸ¿¦úyÕÏ«~Þ‹½ËÞ¥l2›LcÙ6…Ʊ©l*dÓØ4Ʀ³é4ŠÍ`3h8{½GcØL6“ƳYlf³é-ö û„¦°OÙ§ô.ûŒ}†ôl6›ÞdŸ³Ï‘žÃæÐÛl.›K“Ù<6þþ`_Ð;jdiɰԟE²HÀ²ƒ[±C4‰×åu)œ7ä ò@êÏñFHó`ÊÄ›ò¦4€‡ðfдàORFþo M+Þ ¹íy{èŸåÏBóéø Ðwæ¡éÉ{’ïÃûP)ÎÑ~¿Fø@>ˆJò¡|(•à#øÄãø8h&ò‰äË'ñIÐLãÓOçÓ©8ŸÉgR+>‹†zfóÏQrŸƒ3ŸËç¢Î¯øW¨s_€ô÷ü{üêügüj!_ˆ2KøïˆWñU(³™oFýÛø6Ô³“ï„~ßCà(¿—ÀoóðÀq~œÒó³ü<Žu_ RB:•^"#ù‰œ"'MÞÂ›Š‹¼"/ùD~ä*B‘&ÂÈWÌ3(§˜)fRañ‘øˆr‰9bâybâùb>âÅ*Š…ÔL,‹¨‰X,#ý»øú¥b)ÒËÄTW¬«©¾X+Ö"½^¬GÉ bµÅF¤·ˆ­(ÿ—øémb;5;Ä”ß%vA³[ìF="š½b/5ÄÄ‘"’‚Äaq˜êˆ#â4ÇÅqj,NˆМ§(Xœ§QæŒ8ÍYqŽÅyq%/‹ËÔ@ü+® }U\¥@iŒkš¦cÆ24iS³(X³5›hŽæPu-–z/Í‹êhéµôÔPË e@n%­5Ójj5©‰V[«Mõ´ºZ]Ì ´ hiPOˆBµµfZjè¥aT×új}©€Ö_ëOã´p-œFj´4V¢ ¡‚jŽ¢æÈWµ¿µ¿i„¶MÛFôíÚv¥íÐvÐpm§¶“Æh»´]4^Û­í¦ÑšÔ$½åN_4ƒ%£wu®s¤ÑèM]Ó5¤èmÝÐ š¬›ºIÿÑ-Ý¢wôlz6š¨Ó}i‚¾B_A%ô ú*¥ö(×÷éû¨‡©G’Ÿ~Z?Mµõ3új¬_Ö/S ý_#¶~E¿B¹ôkú5Ìâ8•R³»`T§6F £å6j5©ŸQ˨EõŒÚFsèÃŒÎпh¼M£+fýnF7¤{})\¡ÊF¸Ž: ™gÌG¼ÀX€øŒq†,…üsÆ9²Ì&fg3¿™Ÿ ›Í‚ÔÄ,dúBSܬ@áf5³4.†hn¶4[QF…$²*$ñ„ÙÖl M{óiÄ̾4Àìgö'Ó 7S…3²)œb5ß /s˜9’2™£ÌQ”ÓcŽA<Öœ‹B²›Ÿ›ŸS€ù«ù+5—™+h‚Â%™«ÌUäg®6WS.s¹úæFjln6w ½ÓÜM Í=æq¤O˜' ?iq*ey[ÞTR¡–rVe«2M´jX5·²:’Ÿõ´Õ‰rYÏXÏPcëY«74}¬× h „f5šÁÖ`jj ³†!oM"_ëMëMzÜzËšŒôTkêßúñ2kM²6Z›©¸µÅÚAÅB*cí²öPz+ÂÚý+’‚­ƒÖa¤ZG³N@sÒ:M%¬‹ÖE*aç´sR;—‹¼ì²v ½lÙͨ»ÝÅîJ=ìnv7šd÷°!=ÔFÝìwìwÈÏžlAìùö×j/°P ûGûG*lÿd/¤Fö/ö/T×þÍþ úÅöbªe/±§ {©½GüÃþƒjÛËíåÔÔ^e¯A¼Î^GöAûõ·Ú'i€}Ê>‡£œ·/Ó$Çp šàXŽE~4éà6'½“ž‚lN6äæpr 7—“ ¹¹<Èõwü©³ÈYD9UÎjjâ¬uÖRcg›³ éíÎv &„TÈq8ÈFà«­ÀRÛ€žv;ífÚ ¬t¸èðÐI` Dã “HÅì Èá1 †úÀ€šõcÆÒs~/ û>˜éûb†ï™}fô‘˜ÉGc‹™{<ù×üš`@âoòžªMCì”Ý=æPu ô„ÖhûCЪg˜+˜‹)û!ûï$>ÉÝeú‡ » Ìö_ò¼é™úßc‚TxnÞw½mr5•!S¾&ßC{jý/Sn–‘ˆk‘ÿÊÆ²¶<&çS>®“\+£ ® Ðµí$Pƒ—l+& µU’Eå·2|-·\Š2ÞruôŠ «‡];¢V]° »»ÑEYE—ä«jÀ˜RK¢Öñ…2Ë(Mì®Ùò§8OYgŒºÓ#çË|òø•zÒCÖU¨•†”¥Ç(Hö‘ßÉßaõŸê%ÇË~8^/|Ü£>hö;Œöµ²¡¥¨<åq9I®”Ëå"j§TÔšGUä<ÙF¶@ªäõ=X”>潂Ö1w[eKµ+Dk•®#ë· :àïѓò“Øßþ*É]øœu?©Ûü®[î­¥v¹1ÔIòÏËÈó°æiÄÛð÷>žÕF£‚—’$¿3â¹·”f-zŒÀÈ| mñÈÃtUÉè¿)=ÆŽ¤Õn²ÔA îÐÝÓ³@YžÕƒRj¿ ¤Ê~›å5wÏtyíðʽOÁ½±ß–ëë©¿Üc“dÙ/úªØ5(w}ýÖ‡Å~îžR=5u }—y,’Lûyxqj-¸-/Bn—çÑþ¸ÜwOHOVëèZdS¦‡{å±;d¿Hµó€åމžù8ö;ƒÙ8 9h{Vüu==!Iös±`6ˆl˜ÎÓþ’m¿r¬æÚMš£=þ™dÚÏ}º,˜'j%OH:&Äüá±YªF@¹xð0páq÷žgI6'qû­ÀÇ3{¤šÓÝ­ÙC®_ß"góm~™À“1o@FýV.‘‹ÕßÛî–,ÿ–»£w\ßœ†×ö¶<}ç÷—sÜÝ‘h«Qk^ßù>N°äW2ÝpónwÛºeÞØØÔâGtæ¿OI¤¿q7Þ$ÙS©¼²³±ëuE}ÿ$ígŽ$•ûLö½e^ëØÔ±÷çßTÌ+)»4F‡Là‡1§‘ø Ã'…Ï\aL}=öø¤VÈÍ+—Éå`ŒŒikÉŸ’¦£îÿPŽO¿qÇúèµ ¨<„¢rÝ·üQVa! ¡Ñ26î J.J&úDþä~Ô³Àw`×cÔYàŽöáµIÓÁ~K•e Ü„ŠclR5*½¾uD”5n°_ÕøGQvŽp?*}'ì÷e»G£ãù¨çÌ‹DI`ß›wŒMæ17ÉͰçVõ¤Ëa¹5µõÝÈ€–¶âºÒ¥º®½w¯5§¨ÿ´œïÒþ)ê¤ØOöC¾'•ãÁåÎ% ³–“koñ¾-‘Óåù§û pKòÍøï$}ÍêëÿGµv¦{×ýyÐõk!>AäJù-ä°¼"Êõ²ûÝyO}öŸ¤³ò¨çoÁã<¤èo†»/OBûÜn?mw_8ßvª8zßRù¹Šg¹¾¹'zwwu *F-¨Pü±ÖÝ,OÈ¿\TjwñŽwN»¢YºO’q$ùÇßÜh‹èœ/,ÿ5úâ@5«$ÒÝ}UK="?R© ò9*…™¹i׸ë„Éy.•_`”˜ãî~#¿LÈ—‘Bû}FÕŽ«wrŸ½(d¹šSÊDE)šé³P¶ÆéŠÛ‡˜Õƒ½âû3å9ë׸[yÞ÷ÄÌ ñŽš;•ö‹¤—“ùȬx’®4-þ¿·X/*­|¿ÊK¶/)¾¢$ÕvGž‘IÞ¸5ÛFïB?çBÔø!Ï+¼Ÿè\¬“B½…¨¯ûÊ·d¿ëïÞì%R6ŽõÈAî7iÈ܉YÍ"[o:j*÷BåbÛÝTÚŽu¸ž£jOè´¨/MwÀޱâÏç)ÇR¶wvš\G’ï1ÏíIF½ÞŒø¢¿)?¶Ü-ÿTx|wT‰Ø2Îíÿ‹îïî€%€û¦ 3œ¤öº„°ç½Ç%žpÛpÇßVÿ_•òq3î,"÷¤åsÍw{Oô®ºâò ú’W§âzóM²¥|>µ¥Öÿ¼5î šw¨%ë.‘ëpÅÍOXÅÌBrü=îýKuwr°â“v1çôY'mwB‹áu.jK¸'ß;l•¬#?ÐëQÈcê­š#®‡éÈ;¿Ÿû½y×úVוfÏo¬MèÎ~J]Ú³é˜u¸n[î»[·€=‡›Ry^©÷ 7G=?ôßixÍËsyÀ]Yå$Ú«¾2aûË]îÛÊ1(úÖLæ~Ùo/ÚË67¾.êm†ÄÖmÚ®OBÿá´{(±£GÔj–>xÓQSù´\D SôÃòj2´ÿnòªxGÙ«žÚÚ®qŽšÏC³îLð‡¼˜ £×G¹„F¾d‘`Ïsä{rt jKýóWÕ“YÞë¾ýÿmÀÛß©úìÏßó{AÕH½OîQ+ÍÜ…¶%ÇÄI?)_ºå¯GÞð}jš×Tw÷<ä´{ò_ïœHÞ‹·ç!ñÆ•¼¿'ƒSyþgä‚xßxæÁ{6‚WÏõxÖ$K¹Ý·™6‚s=€w¥Õ:»÷6èqÏ@­»š²+9ŠùgW2ÊGí©´óAÇ/hÈqÇ{Æh!•’\º;åŒM¯K…È–2ñ·*½L½í½Tþ‘¬õ#®×Y7Õœkb¼ïcï€ýÖ«7øv&Ÿ1$„Zbž«Œò¹-åYIÄãí5؆Qa›z5-ïyü版¤=¿»ä·ÖHy@ž’o>NÌ}¹2êžgÌùFû§"R²çIà|¯”ñÇ$ïìæu_^©o7ôŽiú¾@ìý·3ñîž§ðÊ´\ 0©8)úï–4o©Éª ø(Ž÷ëvïÎÜêîþ^ŽRFÿMòŠ))z/èöö;{ãœñ`á¿$µ?ÎN‘ Z“»E{2 É$ xÆ&‡§tÛ¨µ¿ RH9 Œ(•RR­ö]вCÈÒ–òAÚP~Hy*©C>T˜ª«•#*R1ò¥–ÓŠS*)M~ü¨©$=†šJQ* i㔣¨ÃŸ*#ç1bT•ªA_R†jPMäÖ‚”¡Ú8Fm³-5îi¤;AZÑóVÔ™^Ä/»Rwè{ÐËÔ‘^´§žÔ ¥û@Ыԗ©õG ¯AšÐ`H iBo@ÊÐhˆ?¢±iJË!a´RŸVBÂh=ä)Ú1é/È“´J‘'褢ÓTÎ@jÑYH0‡Ô£ `º©G—è2®ø_Hcº©GW!~ÌÝì¤cLP=¦1 iéTÌ€Æd&Ò³¨1s˜ƒ8K}z–ž ³Ì,35bYXªÁr°äÃr±\Äò°d±Â¬0éÌ—ù".Ί£ŒóC\’•DÉÒ¬4âr¬4UYUÒX5V«Îª#]ƒÕ@º&«…¶ö.{—2±Él2eaSØrØT6•Ò±il¥gÓÙtòb3Ø ÊÌÞcïQV6“ͤŒl›EØÇìcÊÆ>aŸP>ö)û”²³ÏØg”‡Íf³)7ûœ}N¹Ø6‡r°¹l.y³ylåd_°/(/;À/‹d‘TŒeG©$;ÎŽSI^‹×¢†¼6¯MÕx^‡Úòº¼.Uâõx=zœ7ä ¡ äÐñ *ÈñFTŽó`*Í›ð&TЇð” å¡Tž7çÍ©oÁŸ¤Šü)Þ’ZòV¼=ÆÛð68JGÞe:ñNH?ß¡*üYþ,Ê<ϟǯ:óÎH¿È_Dú%þjî» LWÞõ÷à¯PÞ‡÷¡2¼/ïKy?Þªò|ô¯ñר=ÈQ>˜¦>„¡¦|†’£ùh¤ÇñqÈÏ' üD>‘ù$> ú·ùÛ(?•OEmÓø4h¦óéÐÌä3©ŸÅgQ+þ_þ!µãñþ˜†ôlþ9Òsøwÿu.àß þŽG•ùü¤ÿÇF=+ø ¤WñUˆ×òõÔ€oæ›qô]|á<eöò½8îa~Wz„AÉcüê?ÅOA–ŸEù‹ü"âKü4WùUüVrIíœ:]èÔQ VÂUŽp I'Ò!×KdDÚ[xSˆÈ+ò’¿( @ã#|¨ˆ¡¨g†˜AÍÅâª+>S31[ÌAúeý25Ô¯êW©™~M¿Fuu©KªoÀ|Ô§ SÃ4Lê`T0*P#À¨N­FM 3jµ¨¨QÛhM˜Ñš— ébt¥6F7£7^6zQC£·Ñ—*ýþˆÃpªk 0 ýš1„7^7^§*ÆPc(55Þ0†A3܇ÆPr¢ñ&êyËøšÏ¡™cÌf®1— óŒTÎøÆø† ã{ã{ªgl7v >cœ¡’Æ9ãâóÆy*e\4þE|Åä` SPIS35j…‹Ã|df3³A“ÝÌM3µ5󛨾YÐ,HÍBfh*šU¡©fVƒ¦¦Y›Ê›uͺˆë›M¨ŽÙÔlFÕÍææSˆ[𭍢ÙÖl‹¸ÙeÚ›O#ÝÉìK-Í~f*`†›ƒ©´9Ô|ƒò›ÃÌ‘ô˜9ÊCÍͱæD¤'™“‘žbN!?sª9ª˜ï™ÿEü«ùµ7™‹¨¬¹Ø\JÌeæ ªl®2W!^c®A¼ÉÜDMÍÍæ”ÜiîFÉ=æq¤O˜'>iž¢2æió ®ô¬y–Ú™çÍ‹H_2/!}Ùüé+椯YD­0E3jgåµò’m° @SÄ*MI«$â2VYÄå¬rÐW°* ]ɪ„¸²U•ªZ ­†Ð7µšQm«¥ÕéVVGêh=mu¢fÖ3Ö3ÔÈzÖê Më5hZ¡d ‚f°5˜,kˆõu°†[ã¨5ÞšDÖ[Ö»ˆ§Z³¨‰5Ûšc}nÍAÍs­/þÞúñÖÐüdýŒôFk3…X[¬äoí²vS!kµçaí‡þ€I5­ƒÖQ¤Y'>i]¤¦vN;'µ¶sÙ¹¨]Ö¤§ì ;ˆL;Ô¥'í0û%Ä]ì®j÷°!b¿NÍì¡öè ûûê`O¶¿ ö—ö—ÔÐþÊþŠ*Øóí¯)Ø^`/€æûªgÿ`ÿÍöTßþÉ^H~öoöoÔØ^d/B ‹íÅTÃ^bÿNAö2{u´ÿ°ÿ {½š öjdo±·Pmû }„|í£öI*æŽAíÓ1©c9utlǦN:Ç šôNzªéds²¡Lv';49œ(“Óɉ2¹<Ðø;þÔÚ™åü—:8PçCçCjæ|â|BåßœÔÜY嬦ÆÎŸÎZjäls¶!½ÝÙ´Ë9)¤^A!õ| £û+D^Paqo%åþ.]AÄ.òî¥wI…¼+*äG!oß8È»”’¾@ã¥>]üIáoŽ:*Q] ð*лø»Ðw[`àv@ÐÁÀÏPÆEÛµéYHÂÜÿæî }WÈ;Èû ìž œQ/è{C‚þþîôy è{ ¸Ä`Žx¤!„Ñ(ˆP(ÜO¡ð …—Q(¼¬Báå /ªPx9…ÂÛÐ&ÈÚ yR!ò©ô7¤%mƒ¼G; “id탘y‘dYFzBáò —`YYVͲ±lƲ³ìÔO!õ‘,'úh–H½ófÞ4”å^­ðz+¼>šb…¨´BíM€Ú‹@S”%ÆŠ»Vؽ9zGÚÅîÙX)VŠB‚Íʰ2ô `ÔŸUf•©2{Œ=þP…U¡J Ù7SȾ§BöͲïÉê±z4‘Õgõ©7 bAô kÊšÒ›,„…ÐXÖ“õÓèÅzÑxÖ›õþîÃúPö*{\¢ëG5YÖØ=œ…ƒÉ `À%F°@ü#ÙH2Ù(6 ¬`4 ž0†OËÆ‚9ŒcãÀLƳñ@ù.¯è¨xųŠW<­xE'Å+žS¼¢ƒâí¯h§xÅóŠW<£xEÅ+:+^ÑCñŠîŠW¼¨xÅËŠWtU¼âÅ+^R¼¢›â寨ȲƒÈ±CÀ÷.‹ðWüÁ_ñ‡òŠ9ø+æPŠ7åMÁBx3h\¶PR±…ŠŠ-”Rl¡/oÇÛ!nÏÛ£ŒË*òçøsH¿À_@—-TT ¡/ïÉ{÷â½(X1„1<œ‡C㲂ºŠð¡|(•Ž2#ø¤Gò‘Håc)HqƒÅ ‚7¨¯¸AYþ —”U¬  ¿‡ò.7ÐÀ >ñ\& ˜@C>—ÏÅqçñy(óÿ X>Ǹ n°úoÁ ‚ù÷ü{Ôöÿ i—!”á ùBüv ÿñR¾¿]ÁW"×å uùF¾—3ÔçÛø6q'߉’.È«øCð‡¨ÓååÁŽâ·ÇùqšÀOò“HŸåçq†øüÖå Á‚ †ØeABb—?”Uü!H1‡`ÅEV‘šœ"'ð·ËÊ(þà'ò‰ü(ﲈ@QHB™Â¢0b—K4a"Œê‹§ÄS¨ÇåÄL1“ŠŠÄGTO̯¨§xE=1_ÌGü£øè|!øÃ@±H,¢AŠ3 Tl¡ØÂR¤—‰?À\†à#ÖŠµH»Ü`âE7¤XA ñ—øém`ÅÁ v ¼Ë )V0Pñ±Wì¥Ñâ€8€8RDR?q¬ ¹8"ŽAs\§0Å š+nPZqƒ~âŒ8ÍYqŽ˜8/Σäeq™ ‰Å¤¯Š«Ä[S<¡€®´©YTlÁ¦BšÎ0Lq†0ÍKó¢æŠ9„j´ È­¤U¢ŠE ‹¨M£À"êR ð‡ h\ÎPZ±…‘` a¨¡—†yDq†§gè¤8óÚPm(ÓÆhch’6U› †0M›F¶â—xNq‰ŠK´W\¢âÏ+.ñŒâ]—謸DÅ%º+.ñ¢â/+.ÑUq‰—xIq‰nz6=q½¨^”‚Á(|©¾B_Aeõ ú£øƒ¿â úQmU?¡Ÿ@|J?EAŠ9ŒÔÏèg(Lñ‡"ú¿:æýŠ~…ê)QT1‡1Š3ŠôªbÀj¯×È­øÃ(Å*(þPNñ‡QŠ?äSüa”bþF?£ PÌ¡¨bþÀý_ýÏ7æ#½À_!~oã¬q–Âî÷V8¾–U· […ém…õ«XE­¢TÍ*f£Ç-_Ë蹸Uœ³ü,?¢˜@m«´Uš‚¬+€úýW&nÕ°€­ «1ôMþƒúT连Bÿa ý*ô_O¡ÿ0…þú­Ð5̆ôp øÊ &¤˜@}ëMëMz|`2Ò.°Þâ°Z@ÌÖ2k5T迌Bÿ~ÖNk'µØCú/£Ði ÿÃH(£8@ip€ÓTÖ:k…e.‚”U| ‚âùh>ÐŒ¦ÚÍíæÀâÝìnÀô.îT¸–Âú%Ö/¢ð}Q…ï‹Û¿Ø¿Pa…ò‹(|?Báû~öR{)Žââû‘ör{9¶WÚ+Qó*`ýÑöZ{-ÒëìuÄÖ/¯°~Eû”}GkÑà¼Yô:Û@–€ºÞQÞÜÉÊ›;…×âõh*PW0ÍTÞÙ•wö#Ê›ÓÇ@Z­èS…®æ*_ì<Þ ¸ê åDÕ•¾V~ÖïŠú‰÷åýè|B •'u‰ò¤þ´4‰–*¿éJ>¨hðÐúSùAw(?ènåP~н åìS~Ð ÓD*?èA…iÇì¥#À.Gè¤òzžV^Ï3Êëy–_ä—èœòw^PØå¢B-—0 tYy:¯¯d¤«@'y™p Ó]¿&3\,ÂL0Ëõk2Ûõk2Çõk²t.þ`\¿&Ë(¾¿±LÀËX>±\¬f>®’ùŠub++.þ¬œØ'ö³*®ïUu}‡¬ðÁiV[\—X}×SÈ1÷›,3}:âÎ묹;—³0ÌåX ­©ÖŒuÀl=€=ƒ™x7{^“ºÅº¹¾7Ö×)Y?}‰¾”…»þ66Ðõ·±A˜/#Ù`׻Ɔ¹Þ56Ü#Ùwvd#õ«ºd£3t6Öõ¥±ñî¼È&`FìÆ&/}Ùr£¿ñ [çz°¸—ëÁâéÝùŒgÄLvžg2.šÏâzªx3›YtýO<Èõ?ñÆf}³o‚Ù«o¹j03Ðt>ÜõñY˜KVðÝ™ƒáÎüKÌgùWæyó _o^³ÿËõîðîèÏwº#>ßåŽé|·UÆ*Ç÷¸~¾ÏªdUåû]?ìztø«¥õ?qy’ðv½2Â×…E)k¶õ…¨`}oý,ª`üÝ!jb̽(‚0†æÏbô Ï»~ñºfwC]Š˜n±ßs0z~#Ù?Ø ÅnŒ’¿‹ö2ûqØõyˆ£®ÏCs}â8ÆÁ“âÆ;[\—^\Ÿ–S#w,Ó4w,Ó wüÒLççCÍv½Z:×{¡y¹ã—–Ãõ^h¹0rm×òüÌx@/xÚÝ™ tUU–†ÿ}ÞÞÂ`Œ„až"  d’y€!P<@†È$ ³Ì†y A"P–E*]ËeÙL ˆ–MSeÚ]tPQ ¥÷ÝyPLqu¯bU­³Þ÷ßœ{ï™öy{ïóÀ”åuB uÂè D=h8&f$ÉÄ0èé-cѸ~‘ò¬ƒ¢(…hT£RÛ-ÐÝñ,’Š¡‰ xÓÂO[(‚’(‡ ¨Œ:hˆ'Ð]ñ4úb Ò1 £1YxQžCø€ôXe‹ªHÀch†'ѽÐ)‚áyLÆ ”A }·níЮG×αسGÇX,Õ6ÊÀ ˆâx UP õ‘ˆVhÎè>è4 Ç(ŒÅ$ÌÒ7‚Òoª£6 )Z£ ¦k}Yê°²”2J§¬¡L6pš*[(Û*;){({‡Æ¦„œþÊ4å0å(å8eVhì¨3M9[¹@¹T¹Z¹~Œ¬’³U¹C¹[¹Où®òˆò·â&HüÉýhäûÈÏP’oùýh‰g*"¾¯X!¯zý·ž ñŠw²T!WB|ëÿíŠÄ?Þ›e Iß×VÒ¿¬yáúƒa=ÖÃúqXOh TX+k¦A¦’ÅøºŽßžÓ/¬Ÿ<çöë×ÏyeÂZ¯ ÞëÖqaÝS ÁaÍkNXO‡õœ¶cФÙ§WVÑèb)9Å¿-¸_²QX³Ãº>¬áù•<Öca=Ökó*…°v(¨/ÃauäC¨iWpÊ85‚ׂ×Ùp—àG8–+sun̉ܒÛpîÌÝøYîÇ9•‡päÑ<–Çó$žÌ/ò ~‰_漘—ò Îæc|’OË®,!™Oå{u½ú^méÉ–}jWôb¼X¹Ëòâùó3ì=ªãæˆÚø××¶J=Y×í/âø‰=NÿõÚ-ú19Ͳ ¼£Ì¿«ì½gùàŽrú§Š[á®ÒÌ}g‘QÞ£ão/l™ÏCN¢“()uOg€Ì6ÙIG1g³¥MN.Ê8yN*8¿rÞGŒsÔùª;gœ3¨ï\pþŒ7ÆAs·û8Ý–®d¥îwÚ¹‹ÜhŒ F¡{0:X=‚UƒuÐòAÉ}D3Ëíî-rŸóVÉÊZâ­Ÿwûy« lH%¼ê^„÷ž×η!—oh$ÅP¬üÕG"GQñð-Dý¼8Ònc'Úí…-ìÂ'oÞi©wZéÖ·Ýi£wžÒ;mõɽ½v3»¹¨ÝȤè.€z»©­%Wõ¤ÔASÉ©›I©+Ùw²Ô¥KyTöÒpÉÞCR‰ç™$Ï} «ÞDüÍqÉýOJiƒ“ÒgÄ=…sRÚã÷R:à )Å']”¬þ•@gñHqȤºT#éôGü´0™²±1°7ðïØnÕ³ᔕhµÇ§VÈ á?­Ö üÉŽ´#qÑŽ±cð¥gÇá+û°}ÿeŸ·Ïã¿í öðgûKûKü}ɾ„|ûªý\¶´¯ã§¬SWœšNM|ç$8 øÞiè\ÂU'ßíKyn’»ÞÄzÑ^´Ióó3ƒ¼D/Ñ ö.{—Í`Ù`”I{×1Á]ÁƒfJð‹à%³8øCðó*û_ÀlpÀ¬áâ\ܬå’\Ò¬ãhŽ6ë¹"W4¸ W1¹×0›8ÌfnÂMÌnÁ-ÌVnÅ­Ì6nËmMwäŽf;wá.&—»swó÷æÞf'q’y“9Ùìä4N3»8ÓÍ<‚G˜7y2»9Ä!óãqfOà f/gq–Éã)<żÍÓxšÙÇ3y¦Ùϳy¶9Àóxžù%/ä…æ^ÂK̯x/3ïòJ^i~Íkx9Èëx½ù oå­æ°ìòÍ>ÎÇÍ¿ò)>eŽñ>kŽó9>gNÊ®j 0Ɖu*:qN%§²Sʼn¿Ë#Æý?øÄ@8[#Äøá;¼âƒK”ŽÅÜâ‰ÿ~£0rޝޅÖ,ë%k¶5ÇšnͰfZs­EÖbk‰õеÔZf­ Yôͦ9ÿ_¶VцÈýÑ¢14–ÆÑó4ž&Hf7éV Ó<šï¿#­×>Ò)]úȤLé#K¬V°Eu ‘2æ-(cm³¶Ééà"ok‚´}ë|¦Ë\¦ÉŒÆÓDy: ýÞXË:ÎRá6 ¹ã|›éN*ÈQj믤ö(’9—_£Ë¼ƒ_§¯y'ïâ7øMÞÍoñÞk—ç<ú†ß¦oyï§+|€¾£ïé*a±dȯ`)–a9V`%VI¾œ5X‹uX ’;oÂflÁVlCŽD?Y­3Ö$k‚5Ñ:k}b³>µ>³~ߺOïñ÷ ë¤u ¯ÊóŸù™M²ÌÉ+§#ÿä!§³’rFˆ”óS99<"'ÿì'»0^¾/Õ%×”Ø\[¢p}‰¸KTN’¨œ‚Á“30#å0c1Y˜…Ù˜#'‚—1O÷ç½öŽöâ{å'öuÈ·ªÚôÆ7¯[ø›gûç1ɳžqz9Ï:Ï9}œ¾N?'Iò¬ÍÎg›“ãlwr%—jè6r»Mܦîã’G-v—¸¯¸KÝeá<ôF«éáVýlºúýZ•üí]ç×ÎAçsØ9â¼ç¼{?’£•VÆ+ã‚•‚•%W+Ètÿê­gkoîËc4‚f>¬™LIÍUJI¾ˆHMºôéd¢¦3Ñ™ˆZ’e/@mS]É­óð˜Œéwh"ùóYtÒÑtu®8WÐ×µ]ýÜ7I’IÇ ¿Žo äÔÍ®£ê¶v[c˜ÛÇíƒáîTw*2$Þ‹:îQn¶›ñ:ú ~惉ÁýÁý˜/£ÿ Ü´ÙÇa›¹ÿàóy0{ >Ûùî.z »‘4jþ£ïI#kéç`§oîʆy=(ÿÔXâR9ãÅçrK dûãááC|˜ð{ü>ÀG½"^1¯¨Ç^I¯”÷é¯ZIr6<#ç¿/ÌL™çw®ôuÓë»ËÝ^À³<Ûs<×ó¼ WÞùtL­%=G£È½{óm©+}@ß;~ïÆï0ÖÞõ­–“ØÛßJºcÄ$µIfæí#Ò¶Æë›ûåÓVÚŒ¨œ€'nœµå\l$7¦£¡0K{ntójЫñ7ëêK )±)pÐÿÝÆtÁ“˜y’£dK&²{ðŽàCœô|…¯q,*J‘M•©5 '¨5u¢žÔÒ(Câüx‰é3$~/’¬5[²ÔÉn÷Ð:HïÓ‡tŒNÓgt.Ò%ú–®c<a"M”‰5ñ¦–I0Ms$ÊœšIžÑRu€œÔ}MÁÓª©è©š†^ªƒí÷T‡ÈyÃ×tôSÍ2›}¥|ý-£™r¯ùjWt¯úêŸQEgÕ8÷ÕJè¢Z]U«Hvák<º«VEÕjîª5ÍÕZTk›Õ:©Öu¯«Ö7[}e×lS-æ²æõÐBèÿçÓ·^+ý ¤ƒ0…ixÆ·š}ÄÿE Ïú¶µ ‡£¯oCû”ð³Nòæ|<#süFf˜#ó»,ŒEkaœû°Ú+Ëî12«§„ñh'¬ŠöÂjî·Âšf½°–û°¶Ù$¬ã~/¬ë^Ö· z²ë½æö a¢}R¸Êl€‘™m––\±§Ø M²Â1²Ïþö~ºê§\²£JQ9ª {ªÕ£F²«ZR;ÿLG}h ¦ L—5Ë’UÜ¥š"­úšŠ™ªi&×WÊ÷~髜¹÷ùêÅzo¨ÆyoªVòv«VööªVñÞR÷ö¨VõòT«yo«Öòö«Öö¨ÖñÞQ­ËE}e—£TKcŠoEoƒÚoªÚïEµß4ßNf»o!ïuß6^®oo£oÏ_ßJÞfß*Þ6ß*Þß*ÞVß*^Žoo»oï5ßÞßÞNß\Ä_q~XWœteüöj·°µ&WVÜ ©øšŸ}öÀ«ê£~v¶o…þ/³Üguacamole-client-0.9.9/guacamole/src/main/webapp/fonts/carlito/Carlito-Bold.woff0100664 0001751 0001751 00001045174 12612226251 031414 0ustar00mjumpermjumper0000000 0000000 wOFFJ| ‰TGDEFñd]‚š&™¿GPOSóÄH($éç—IGSUB;ì Lð‚ÛOS/2ü^`kWç cmapèýèéÔNcvt .€/8&ç`fpgm(è uÛ¶.Œgaspñ\glyfJ´¢Cj¶Bìhead€66}0hhea¸!$  Lhmtx\Œ+vï4¿:loca.°+|.iºmaxpÜ ž TnameíD ªÆØ¾=postñH ÿœÂprep-üšx:8^ŽG5_<õJSp‹Î.0QûýýÒ Q(xÚc`d``cøÇÀx„kÁï¿ßÏq2E×] St ÞñŽP]nå uxÚc`f1bÚÃÀÊÀÀ:“Õ˜QB3_`hcb```âgebbbafbyÈÀô?€áÍ  '3”Tø0(00üûÏÆðñû<&5Æù 9懬@ ( ëŧxÚ½š ˜ŵÇÿ]UÝwXd™a“}6Ù7YgØ";#""Ë2Ã:ìƒè°ƒ‚"!&ˆ$¨`ÁhDc@ôABTD "â‰Ä¢l·ÞÿÔí; #õ™w¿ï÷Uuß®êîêSg©:¦jö_Þ~À‚‚¦öS“ƒi¦ ®üB˜âW@ý²›0ο×û…1Ñª*–›Ôc(bÒ‘®‡a¬ÎF =‚(ûp§_…}ŒBCñ\Ó“ÉZ™9ìsj8N#Qð[b‚`v‹´C3SËþ[ÿ UõIÜ(Mp£é†}+š ‘$̓"1ø_[ógT1KÐÉÕ#1ÔB”^AÉPÂìËS’¹Ìa»¼„çƒgx}œ=¨| áùÜëz )áWHâ±;ï—B‰È`ŽËhàž§f˜nöa3ÔZï[ É Ó ýT!ûGAÄ2Z•G 5·s1[ÞÙÑ©fê/¡ï Ï·Uï"K7BÁ 53“Uq”PF’úÜžUï ¬_ŒÏö$¿Ÿí^$Ej9J°Ÿ1‚~…üÓˆèÍ(¦÷Bëúë¾X¯ï ý(Fª?b’ú÷©Ð…å$µ㼿 µz˜ïpzwâVo>eäeLô²1ÜŒÅËê VðÚñd‰kŒS­ÐÓ;Ëvç°H=ʱ)‡©j#šS¦Zñ¿^*=ÔZTV#]ù¼¾ô*L ËqÞyÌÔK±T?ˆÉºúé…ÈЕ(‡YX¦KP&“Ñ]­Áµ÷¨ý|ÞxHŒoM'¯¬wiäV5÷«»q¯RX¨êbƒî‚þf ʈܚ—áé¯P_d^¿‚ZÁ(ÊX6úSž›˜ÐÑ Â|“F¹|Ñž4ÑVoD_õñûÔ0ùÿý¸Fuå3Ã5f.¦é×àë’h§+òÛ­Cm“Š‘æ64Óí9¾Év¿ιñ´ô*{Áÿ “}~gµgô)<¦Š!S0“8S1BOG†?Œr¶Ñ~£ßGy¿9î Üfø (¹ŽÔå\•ùWóz¨=e>ò÷¢9çï?ÂsK1 R#£9·+ØsÁp~ÿq”©^H&u#½p§ù=C.”¥n耞î]¿ÁlÊÓõÛl@)ÿÒ£QÁLÁ]üo²i†Áõ(èïà BN0sÌ”Ýθƒï_PŸÁ5~&Ú˜Y˜bºcºþm~ÿ 7&™iüm¸“ý,Ò³8÷g£uðšó”ÝqöKÓIz¿û:”R­Â½ƒrV9>Ùªæ’yª¦ûOa:Ï-&s(¿‹Éœ°>GíÂj²™ïºšlÖì ý?Þ|}Þ[Åú1νÞsE$O³馒©ófbªVR¦Wbå¼™Ãÿ_&ÍC›=(ɲ©åæÿs1ÇÿÚFbg°÷96á>ÿUÊÔ!d©9†çÑÔû«Ç¾ª°ŸÌ°ŸE¤éNŠ’Ö$Œ× 0_ù(£¶£¿ÚŠAêKŽ×6Êð1ÎýÕœ_b"ë‹ÕALSáµ»1ÛÛ¥¤çg×vš©)HQSYdu'Zª¡Ô ‹ÑNeqN¿k»ûgÑSàT'HðÞ+d˜ŒÙž×{ùÿ -j‰Ä`;n ìµÖžÝÁó‚—LÎÍNèÌoÞ”´¤z#„àYl$_ÅË‚wy‚8佇$RÜdàïä=“a—ÇÀáØqü<Þ"dfžc)ß7ÞÆ "Z¥1Š:º“_Ÿßl Ÿ/UÍ.t¥®^H›ºÈ¿½ÌüÚ¼Š•,GˆýÐ¥p‹ZŽÑúÜL;×R'Qç¿n­ÚdO©AH.laÿï’îz½Ë¾£ £&i@zsÉXõeú"·^r< åH–zc¥/õíIœøMž°Q^—©òz;–R/xöcãyMÀÿ{ÎÛG½]•X_C^ÓÿÆ<·Šõµú)¬c972­BÒ„à€=?ö>DEï3”Ãw´‹ÊFÔ÷V£(ëeYo¡‰7½4À{•þÍí1]“sª& Òߥ_Äv¹·Ü×ïϹôKêà&¨lÞGSêÚõêpú:©oã5µõ&{»?™¾Ê |þ ,1·#=RƒýN6]ôj@Ùô+" Î£÷Ó~ ¤Í?†ö~Ìd3Â~å|—ã(#ýq~”ök£q¤&šOù Õ0rX[ÿ“ý Ful}³š:n;õ­øAsp¯y£¨“é“U¡Ö˜sq¦Yn¿uvˆ]§Ÿ¡ÌŠ_ÑQ|;/ÈÂñ œqŒöþÚ*ñ—Z¡#ç^iñLùЦ¿Êz Ñw«B)ÕL±‡ôF{–ãÑÖT¢Žo…j=õ*í,ígÉ`5Úkx¾šºöwÚÓroýµ}O?€²|¿Îº:ï÷4î1 ùœõh‹SЊ¶«¥:.º'ø+Ñ’:¿¤ãO(Ö+;hÇ„wxŸ°n6#1R“¾ÓIŒvïØ]|'¾Ëp”¢­oãÎ åû˳?™20š:­íPsþ?˜ÿuÅ ú-¾ï{”׎ù_ôcZ ¢ mÓï1‹þÕøúYv±Û‘´}´Wþ.´õ§Þy—¶\8äÊÉŽµœki˾À$2P|/ï„}SPkìÓëŠn¼Ÿ¢lmFWÚ­¹¾•øUÛù-øÌoÓoã¸ë-Ô³síiÚ‰j®¤ë×£¾mN=ÐÕzû<¿ka݆¶4í©¿Z¹×Uåx”¡5( ã=Oñù_¢>þíë ;Í &5é×OC)úÐmMu´×;9FÃ8ÃÐC§SfF£õÉ úDµõQúg©ÿ7ð»í丗ú~õR-£ïÙ^?vÎ%ô=r(÷©Ž›ÑÌÜéôoö?fý×øÒ@ 5e&‹6e óQÛS&*Pdsn$›©˜á¯çœèJžDy(M›S>]MôÖc£žÅ$SçÐÇÍÄ$o+m‡O»wŠ6##ÌCØ¢—ã½ œ<ƒ4þ7Z_Cz+åï~×G8?6ѤïiêâQõnWw±ßiJuÛ¬¦?ÛX}Jû$¼Ïq‹êqÚ±¯éŸÎDUõFê¿b¸zÅÙ±…RV!ËOã÷ü' }üRšþÇ®¬9J»ø {'PD/¦û m]ý“#Å{fz»8'Îá—ïÉÞ6Lã3dxßqžÕCª÷äxOÓFþ‚úa,öÊs躴auñŒ®ŠµâGëg‘.þ³*HÝ—„>úúC ùœÇÉ1²‹6á·ìc/íï: ¡˜Åû­öN£*Œ>´Ï ¼?q¾v¤^(Fý¿Ùd:ý™›ÈXÎ_ ªö©–èíx‚}~Ÿº}£|Ð|ìØM=TCLÆCŽ¢}Cýø¿Ã›£&hÁïÞQ¯ ì¬ òuÑA´ ^£Ü·¢¿‘jOøPn–SŠ¬í¦¼œ ¬üè«·0C¨3!ÅûíãBÜds>ÞÃù6wDÊROžB6õÑ0ó0"~¥¿œå@ßHúÖ]pèk‰1yÏÚO;þõýb2%H¡@Ì&Úäþö[ûB†ó›{áAg'ŽÇbÎÁî¾»QŠº£"ýè¦Á"¤@5s=JGŠá:êýE~ 4ò“PVìçé3ì/Â9 A?ÎØ9•~Y7¾_kÚÒ(HÀ7+è׃LÿÇlý• |Çý´kÄ(ÎÍOyM*R8ÿÛ’ß§Ž!¦˜øÒöíRŽù-Ç! žøýDW:=߃z>¼»w”18ã=ÎÓδym¨Ÿš ©Kn¡ ľЗ͘( 8Ž)ŒqjË:íOIêý*þ<$ÓFßNþöÈJƧøÌ9lÃñM8ef ãúDóû8KQ“ãÂöAhÿöWãßâ›EÇVò0ÝŸû~×Ô‚k½ž?7ný Ÿ·Rd]ǧ=‘˜(×§OMY-F_ ǹc¥¬„™ _f£lS7oœ“?&’x‡s,Ç‘ŽñXëŠ1Pa;RâÿÚÞlÛŽò¶ å9ÏÛèp«©Æœcå¨e žùÊ1>-iQ„Ï2œe:Ë,Ë)kñ;\« +ãëVœo-ô9ÆÑr®¦þ:M¨Äk‹$rÎ$R×ÇÊlGþäˆý?‰LãñÍr¬† ‰þq+–ÝÕ`t÷ÊП,ÃñÈDW7'¥IGRI ¦;„ö\êÒŽþš×=¼ž(¨æÑÍϤ Iu±Ð•âŸy¸Ñµ›Ço4vgЇ¥PMþË‹¤ö©î£ ùmCzÁÂ%íhœõCžq‘*z(ÇPÀÿ=ü£”7bZØyÛ œ» ¨›í_ÕÆ#‘¨ÆÒ&u¡¾M.Nm!ú›uŒgaŠ~‚òzÿŸÉ¹al¡ßéSúÕε·°,Cn¤<DºK¼Aú%íÚD¶¨–I|B›{ÎÓÓd¦~+tm,#38_³Y. Ë©ôOz;’ÑVÚ¸u¥´¹ñx‚ôOâ¥z’°ônv÷í7¼•„çtM{Š~Ì5ª/¶æ GŽÚÏ"¿±h˺ò¹¶±ÍÀسG Éš¬º+øÝ‘0-„ÿ†Î0oÓÖ¿M¹$¬ß˲5p¾Üï| „?ŽÑR2•ã%cAÆèd{œç‡º®©êgÿÊ1K%Mió×û½½mŒïá5ãùßW|¯ƒ|¿…j¾×L®‘¾±¯Wb}"‹qêI^»3¼ç4úuOÊw q1£[Ï»4NB“"hEÿ«&¿UMüŽý-æ#‰Ø+¸ïW ò¿[¤_"¥é‡Fñõ[]7QŸT…ìVg#…Ûù}’ÉtY“á{<ÁaÃnukÄ_Ð.­õ2F÷t]ûž¿‚s@Ï_NûxiþçèDÐÀŠñæçèëOÀ2úuTWû†^ÆvÇìãÎÇá;PçÜè$þ—¬Å»8k)–ší æ=Æx3=Ï­ÕŽ¦-kᇰGdžvæ5úòçí&³ §é›—qvZ®aÜäÖ÷ù¾²>¦wqîÒçßÏÅTõì'‘ɨiþ`wè ûºN±›ynž`ÏÈš®[³ ×js}búÂÔí‰ñ~øýÆÅ gÚ“n­Ög\c8æ†sÚÇ\åÇ÷ÌàuuemBÖ.Ygà;í—5Œð»gJ¬OŸ¬3c±>ñ}ŒŸ†}çÿÐö°,E’—1‡¢òï½LÛ«´‰íËÄqû3ƒöpn=ÏžÍ%t¶+.ÙËÉGÂk¤%}  ôqbû'¶7fÛÅλº”­ÉÖ·“bd?y'l«tõhe’ þ]WÕÕ/\PU£éÈý,¾î?^d¸};ÏñÙ“ÌC‘|ÇÙ—ìY^pO3/¦ ÿ\|ÏóçFöPãû¨—Ãí­æEbàïSø çIô€ÿ@4…T [yüY–÷·ö™‡„Ãôçòž ÷tvàOhóyîâ±ì!ÇÉÝW ‰ï+_Â0ÚÁ°îá^dK¬ŒL¿¸×üÓ°9ùÏésöÌ•®—ýlµÕs÷®̳}ñ\Ü}ëyö°ã{ÖûܾµÛ›¾ß{_¡+Rýß;?#ƒâõàI$É~·Ñîy»KeÓÿøÖ®•ø•å²—ì'GÈ{aÉc\þ¿‹ldÜP*Ö†~’ì™§Ûýó­R÷Ï/ÁŒ‰í©³M?i«æ <Ë©ìï-ò¾ëwšÑ÷næÈZð%Œ•ùå岬ņ fÆ×GEˆ©æVüTdÿ%Dü8Ñ?yÏé4Æñ’§q™\ õ\ž|ÿ”³Ë׈ålü§|<9’;áò&$_ÂåJØ%n?l’í¤Fבûý“Ñãáñzr·6z‚Ç9$øÞµ±ÿ.¹Öå^ü? ùWÃåztõ®ü/!9$ŒïÊÕœŸ±ãsä òqx||;–kmòdì?~›®vù¼+¹(.÷DN¿çgÂ宄9+ÿMÌ$Œ—üÉ{É›û’—#9<’÷’—¥àò\z!9žÃ"¹+ñü•8’Ç’Ék‰ç³H¾ŠËQ™ÅXÖZ%礛³(›_R›—z×ú<æJ ããùñX×å“ }ó0GÁ_a¿ÌääÇå ä§>cº|Äcèx`$–fY6}£†ô-šÓ&Š­PïÚ{eÍ:®sõæ³×`?§Y>B6JŒ.>•ø$l·/ÖYN7†íxÝ;qÂ\ƒ¹¼_.’{Àr\HaAbüx=Ä\zý€e‰‘åþ¬W“w’uþ÷h‹•Ò>ºQÖ XOÌÃ.Ä®Ã0É{w\þl-Çwà·°›ɇ`YR¼–µÉ`Ù^< –¥É—w´«G· ²¦Ar)XÞ"HNË"‚äV°ôɱ`ÛiB,×"¬¿ˆíî›ïC£pœd[ÄñD|‘H$IE A&ÂE*‘F¤éDz‘AV“eeuY_VKEF‘Ed¹E~QP4E3ÑJ´ÑÝEÑQt]EwÑCôýÅH1ZŒcÅ81^Ö¥eMÙ@YDv”Ee~Y[Ö+ô¯¤˜Hþop²€,( ɲ’,/+ˆ5²…WV\tù½ŽF ¡ûÈöz‰l-;É2²®X'›Š/e ¹Ã«&6ÈVr«î«û‰å¶¨ê«*«*\CU•wä=UMõSÕUUCÕT8±šªªZjª-ïÊûªŽª«ê©Áª¾¢¨†j¨¦¦©Fj¸š®zó%¾¬fðšk_ÙפmŠGñm]¬H1Òf¤4¶}aR'{ˆ:ÛÃÔÅ¡®ö¨•hV¦ÍØv©Æj$®²ßټ榹KimG7ß2q¥³¸Ú²j¢š’ÏVÇ‚´ÛÓN*ha‡XÕÅÃYŽp¶‹QÎâu—€Ò9¦ô.!•uŽÊ¹DÔÍÅ î.1U³_yl3Ë-P£p£¯š©Ñª¹£Z¨±X¶à7tŸ2Ø/(£íD™lgÊl»ÐDûPSãÕ—¢µš &buìCGð#5R“ÔWn±šŒUKN„sè¾¶9T+õµjmǨ6jŠZ®~T+ÔOj¥Z¥V«5j­Z§Ö« ¼OmT›LPm¶±<߸UmSÛÕµÓ„ñ6«-­v©ÝjÚ«~VûÔ~ïÜpPò<èž¦Žªcê¸:¡NªSê´:£Îªsê¼û^]PÕ%uY]QWÕ5©¢ÔuuCÝT·ÔmuGÝU÷Ô}õ@=äÝê‘zÌ'ÔõT=SÏÝ÷ƒzáK^yþùz«Þ©÷ꌧ>¨_Õo¬a #‡À  C8Çw[=–¾àyê‘0FÁh¾Ãwa Œ+ÄJ‡IÌCsÏÜ7®¿䆺n´ï&ºÉâ‰xj*»…æ7ó‘ÏŠGâ1æ›8×ãEËI¹#Â/Ä~q¿¤9¸NÄ øV"C¡â0¥G¨8JõiŒ8FCÄqq/Ñ8ÎÂå¸:wàâ¤8%Έ³âœ8/.ˆ‹â²-$®ˆ«âšˆ´ÍÄuqCÜ7ÅmqGÜ÷ÅñÐÜâá¶9ã°Ã·Ëí†åð¬…y°Â;_­†¹ð#̇Ÿà[X¥³ê<:›Î«³ë|:‡Î¯sê:—.¨sëB°–ÒQ›“7Ã÷°ˆJÂìëMWÂ2³×|B“ð+“WI¦êx¯à+ñ^ü"Cx¯ç-Rbñ ;‰çâ%N¯Äkñ†×‰·â!n €ø• coj ~Ÿh¨RIZ‰’¤•NÆ eÒ'cË82]“ñe™ÊD2±L*“ÙÒoÓa°wd˜½&SÊ™J¦–idZ™N¦—e&Ф(™™÷È v²õÛô6»L"“Ûä|ŒÙ»2\Ñåe¨Ì¢‹ê º˜®¨‹ëJº„®¬Kê*º”®ÊIxºø Sˆ2èBui]M—ÑÕuY]Ã]vWt9]Ó4v'ÝiwÎ]4MLSX7aÜ¢v„x!ãÂF¸ ›àl†»æƒy [àl…û° Àvx;àì„ǰ žÀnxj~5¯`<£™ðö ø^Â>xûá5€7pÞÂ!x‡á=>ãÕ¼ŽÀ/p>À1øŽÃop>ÂIø„ϱ-mÇv´;Òn\FÏà”îˆ;Îkøg^Ëà´–¼ðF>Jì@8£•;êNð.>Í[íÞfŸòvûŒwðI8«wò)8§5œ×.h„‹t.i‚ËÚÂÍpU;¸¦c@¤Ž Q:®ëP¸¡}꣧xHP =@`Áñq> 1 ¦§BÁ±=Æô¿¥§¸œžãëp“M€›mBÜbav›·Ú$xÃñ¦ÍŒïm>üÅæÇ¶~²ÅHØâÞ¦.AÊ–$²åÈÚòĶŲ5(¶­Iql-ŠkkSÛ•²Ún”Ív§ì¶±C¨¨JåíXª`ÇQE;ž*aªa¿¦šv Õ²S©¶Fuìtjl¿¡&v5µs©™§ÚÚcÔçžöõ²'©·=E}ìêkÏR?{žúÛ 4Ð^¢Aö2 ¶Wh˜¤á6Š&ÙG4Ͼ¡ùö--°ïlR;ß^µ]m;‹Òr,F?bqZ%è',I+±­ÂÒ´ËÐ,Kk±­Ãò´+МL×ñkºSè&N¥[8nãtºƒ3è.Τ{xÕfÀk6@AÛ’Âl+Ji[S¸mC¶-¥²í(µmÏ÷øßç—ü€_ñC~Íø u°û©£=Àù-?áwü”ßó3þ…ŸóaoÐx{&Øô¥½ÏY9'gçÜœsq΃5°ÖòtYM¬‹µ±>—çJ\‘«p®Œßá"ü—àB\Ì5¹.׿ú\‹ëqn€ÛpîÀ=¸wãNÜ˹;wåžÜ…{p7î…·ðÞÁxïã]|h{Ú>¶·íg{Ù¾”ƒrS.ÊK9)ä±<šÇó(ÇcøKšÂ3yÏæy<‹çò7<Ÿ“sýœ’Sp8œóq^®ÆU¹7äþÜϰýy2Åßò.ÌŹ(—ä"\‚‹q)lŠ-±9¶ÆfØ [`nÊ-¹9·æfÜŠ[p<à)ÏCx âQ<ŒÇù{þó2^ÄKy /ç4œÓq&NË9=gæ‚\€Ëp!.Íù¹,6ÆFØ`SÁ2ÅLynÇM¸-7æö¸À}ø3î7%LqS’ñ@îýy÷ŧøã#SÆ”2¥y*Oá‰<‡ðPþš'Ñ šNÓh*7åLYS”WðBþ‘¿ãŸ(?åãTÁ©MSØT5ÕLuSÅÔ05M-SÛÔ1uM=“Çä2¹MKÓÊäýýF¯˜×æmô-[“/ú¦­¨‰¾‹“•Lk=Äm3ïd1YÜü"s™÷¦i«/±Ñ—õ}U_Ó‘:JŸÕçôyý@?sät9CΔ³älùœ#çÊyr¾\ ¿•ßÉ…ò{ýÔ~`Á18ÌÖ±tlGÇÕñl%_'Ð u"X>muT'ÓÉu í×Ôaæˆ9*»Èf:¥šc.ÊUæ’\m®šHsÙD™+æš¹nn˜ ª£ûN”‡å!yDž”§ä1yT—'ä~¹VîÖár£N%·Éí:­N§›^¦·écúš~¦¿`šAf°é Ó›¡f˜< ÊGæ¶-eîP1ÕŽ¶Úî°š©Ú«Yªƒš­¾QÔ\5Ïl0Í&³Ùl1[EQ\ÔD=3ÒŒ2£Í!þ]€N+xÚ­UiwÓF•¼$Ž“´´Y(¨Ë˜‰µF&lÁ€IS)¶ éâ@h%è"gëÊw~ƒ~ÍShÏéG~Zï;f3mOOsrüî<]Í{ïΛ'qŒ¨ô(ר%ÏúRZ}(¥Î£¸¦k^+é÷ãšl&ž’Q+I”ÌvrËÙŽ’5‚52žõcu¤²l ¤ÚSxŸU‰Ö‰ÖS/M’ÄÇO-N?>L’@ FaŸR}€ÊQ?–²eJ‡^­–ˆ›R4ù¨ƒ¼¼*>9žu `¤2•a»|­\Ïvâ´ï î%±Nðló~Œ³… ¤dd:ò‚¥a e,u¨•8:HaïHÜ}”R#)£˜U¡³ÿgÉÙSÜA6Ó„”tËf5=d8¾Î+¥zª:™PE[´ãQQŸėb]¶†/WL^.wÄl2càRJf¢»$è0‘*W÷°ªbHÕ(9e3THh±d6JU–âìý_­Çô9TÚsã…î%£oSŒ“ú7XMÕ1.ù”¼4¼™Ç/áBS .âæüŸbF¹‹ ‡F.ÂDT­]UŸ¢¶ {Q"ÀŽ9vœ6@À%è™c×z>°ž;äܸKÁ69Ÿ“Cð97¾$‡à+rúäìs à9÷É!Ø%‡à9_“Cð 919 97’Cðˆ‚oÉ!øÎÈå±Ìßs!ë@?Xt(µý„E ‹‘+cö–½oÙ‘zhäê˜zÄ…¥þh©?YDêÏF®©¿pa©¿ZDêo‘úØøR9”âJÿ ¿Á_ãø3K¸ÈRX±ŽY¹c °#D°#p°E °(`f ŠUX°%a°Ec#b°#D² *² *²*Y²( ERD² *±D±$ˆQX°@ˆX±D±&ˆQX¸ˆX±DYYYY¸ÿ…°±DxÚc`@Œl ‡@˜Õ€u.óC†ûØçý½ÅjÿÿÿñÿÇË@|{axڕ׋»Vãö>ðñžß”„°J¨„„()VZB¶’¥3B›P„²E!J…(„Ò9DIQ¬N„ÎçÃê ßkï?à÷[×5®ùÌçã÷¸ÇXó3⟿Dëý_KºOUg³"Ò="2M"²Ó"rµYw¶,"¿:¢”ûR%‡5eK"J‹+-¦Lk¶-âp‡÷‹(k¿ìˆˆ# "ÊY—ëqä舣Ë0ùÊ§ÙøˆcÄ[?â¸E| zFT(Q±³>¾ãW©sÌ“úDTWUÞ“ÝŸ"îTüNdz†çˆ?7ÏÆDœ7/¢6¿óDÔ‘§Ž<âÙÀúâA…í"ñm§hmDc8—±Ëql"÷b¯¢GS¼¯†w-»®rÄõr6§Ã»"Zˆ»•.-7EÜ6$âvüZÛ¿‹FmêFÜ#¶NíÕÑÑ^';Ó©3º¨« ]ùt-Œ¸¯+“ó~¹ïÇý–lrD7zu+Žxáðp›ˆîê}¤FÄ£úØÃõ1{ó{B®ž£"žZÑG¾ç¬Ÿ—ï…f};F¼ÿe½ý³WûF¼Fëê}Coá2—·qy[-ïˆy‡ƒÍÃ` ¡Ý»b‡òæ:ÜÞ{ò¼oFÐeßÔ5Ÿ‘òŒ‚ñq ¦_Ÿèõ'#>Åg´³Ï`}N“1øÕƒ±ö¿ÄåKs6Nãí×£ñtžÀg‚õZOlÎøNâ;©ƒ5Y]“aOq6Ÿ©4˜ŠóW8}¥ÓènÔ¿jØ$ÿfù6«a >[轕®[åÜw;m¶ëãvœwȽCîtÞ k—<»Õ»›&{åÝ+¾„f%¸î3£ûå9`þšïƒÿ\Õò9û{`$¢›‰D‹H$›±]‘H5Dº³ÎÔg="‘­‰\šµaüóÕX7¶6¥:Fâ°<ë‰Ò¬ ;|^$ÊÚ÷\IÁ¿\sfïÈ"¶.GmŠDy1å{2÷Ç4aƒØêH[ƒugÃÙ"†Çqòï¸ñL΂r Ï~ð F¢‚Z*àWA]`VĽbæìxûÇG¢’˜ZFâDX'©¹r™HTUU-'[ŸÜ;ÕÊ3¼ªÉ{ªýÓp:ÿ3ð«AƒöÏn‰ZâjYŸëz=j‹=_ž ê2¹êô‰DÝÂH\8$õÔ¡u‰úý¼#"q1»D­—ТP­ á\*÷¥Ë"Ѩ£W‘>Á-Z‰Æ4h,oc<—Dâ2Øž‰Ëk3<šÐå çWâr¥ÜWÑú*<šòkª'Miruu¦¶«éx5Œkì_#öZù›Y_§'×Éû/÷×ãs=íš/‰Ä xÞ@·h}£ýÕy# ÏÕÄMðoš‰´i£…½›ùß<†Yߢ··šƒ[åkIó–|Zâ}Ž·Ñ¥ÕèHÜî¬5¾­Ýç3w‡Ø;éÞÆ\Þ­o÷ØkKËvòµÃã^ý¾wZ$Úói¯Î8uÀµ£ô²~ùv¦]¸]ÕÞUžûøÜë>àØÍœ·a4F“áü‡Óâ=˜ï©çý‚ÿ™\ïËùëêù€ïêa>G˜ßåüˆ©$Ü‘4Ég”ÿ—Q8ìì:}êþS5ŽÆå3óÿ9ìÏÅáì ~cœÕ÷±rùMI|I‡qz1§ t›`Ž&Àžè|Ž“ñÂoªÚ§Ú›j>§ñ›ŽÃt¸3`Δk¦>Îr?ÞxsÔ<¿ ‰ùêù¿¯ÕðçÇøßÒå;<¾3Ç =3ñù^ßãñƒÔޤ÷»¤÷»ä…ý#YO}{ p½ØÞ%ö »F²¡ýKÅyçK5eø5îÉËÔr¹œ~â’Wð»rD$¯šɦò\ã5m"y­ú›‰¿N]×ËuýêHÞÀÿØ7¼‘7áé9•¼E^Ϧ¤çRò¶ÂH¶¢Íí|nwÖz[$ïïNµÞ ïÎu‘¼Kl›~‘¼»Y$Ûòi'ÿ½r¶‡Ñÿ4ë(wÇdçi‘ì"O÷÷É?žÂ{xm$ÿM³Géú¸óžå¬^#ùd:’OÑüiÜŸ^ÉgÔØG ÏÁNÏ‹^ü E‘ì[Á~QÌ‹zò"Œ—ð|ÎËúÚ¯O$_¡Ù«ô꯮þ|^£çkð_W÷ëú5®wÊ䀃‘|Ç7ÌÍ›êSŸßâó Þæã“|Gì;x¿£fï™IϘ䵿Ëo(†‰6<’ÃÝWÛ{´xOüûx¾/æ>Àm„>ÀíCù>äó‘œ#ÍÀHõŽâ;Êœ|¬þOäü”æŸã÷…ø±zü%^ãh6^¯&ôŒäDÝO¤ý¤A‘œ,n nSh1•}…ãt>3*3s9Kßfã0ßyüç»~M¿¯ñþF_¿Å{¡z±ï/–㇒H.Ás©üKqúÑõGû?ÒxÜez´\—ãù³~üb–ŠÕ³Æ q+i¶JÞ5z¶FÅõ7ó±–ïï4ûÎ0ÿ¤Û:½^oƒõ=Ù¨ÞMôÙ¬›åÞ þ[ÔµUÝÛømã·½£Õç;pÛaöv:Û©ç;Õ½‹æ»ÌßnX»õd^ì‘gû=î÷š‘½0JìïÃ{Þûaí‡sÀý<ú¿úKmÑãﺑŠî‘J4‰T²v¤R=#•Þ©LÿHe‹"•«ÎæE*_™-ŠT©á‘:¬Y¤JŒT™ƒ‘:¼y¤ÊZQÈEª;rZ¤Žê©£›2>å{±w,ü‚!‘ªfö½Û¥*ŽŽÔñ®Ç—DªR›HùæL(çIK"U¹_¤ªÈQfUx'—gÎ}^¤ªáQÍúœN©Ó:2ÜO/©3à)Ç™ "uîgGª¦ü5aÖr~Ìs«±Þ‘òLKÕn©óól|¤.h©:ðëtcË"UwL¤.TG=~õ之ä©kýM‘j o¸—àVX#R åm89R—¼”fZFªÈY®Ep÷`p/s9œ&}"u]®RcSë«õâz]Cƒkå¼V³u‘ú—usoÀý¾ÞÛR7ñ¹…ö·âq­œ·©ÛÕü;×FªM¥HÝMÛ{øzÖ¥Úñí@ƒøyÏJuÔ÷Žtîȧ“Ú;Ѭ³ú;Ã茗ç_ª‹šº˜‡®æâ>ùï73ˆï&߃x¦Ù'æýS=­žÏ­¿÷…¹cæÆ¨w,ì±8~I¯qâ&š‰Ö“`N3Ù Mæ?Y-“WGjоL¡åØSÌÚzM3UŽifbš˜ixL£ó4ÚL§ßtúN×óðgš…™¸ÎRÏl5Îq?ÖØsÝÏ;Ÿùο†¹@¾Åù;|¿3+ /’‘~,Æ}1-ÃûÁ\ÿ æ³±„nKè°D Kå\ªîÍöO´þ‰ÏOp–É»Lߖɱþrº/û³Þý‚Ï/æ¿X­Åò‹-¦ý ¸+Ìç*û«`¬Ö¿5æfë¯âÃu­®å·VÞß=›~Çíúü¡¦?`üéìOzü©Vßè©õz½¯õü6Ðz6ÒuŸMþ6ã¶Ù lÁc‹³­êÝŠ÷V9·™Åm0¶Ñy;·ëÛv¼wÐbG_Ƨ~ø~OíÄ{»Åî†ïÝ.µ‡Ž{äÜ£¿{Ô³—&{á–èc‰½}4ß'Æó9µö\Ø?¨Æƒ4ùKž¿ÌÅ!q‡üýmÿÞé`‰VŸõþ¯%GD:=(ÒÙ&l [é\]æ,Wé¼u¾[éRÖ¥ø—‚sØ¢H—Yé²Í#}„u¹Ñì`¤\é£úEúè2̺¼½c–DúØy‘öÝž._¡Mf›"]±ùÿ¬sV‘ïñyVt¥f¬/BÇHŸˆÿ‰³"}RËHW.ÏFEºJ›HWM³á‘>öÉýÙ´HWã_­øÿÏNÁýÔlL¤OÃã´v‘>}Y¤Ï®ÄÔ] ×shsŽZÎmò?+þ¯7$Òµ+Gú|¾ç«»NíH×ÝiÏÿôEîàv1®…tlØ-Òà©ñ2¼¯ ßUö®QG³ˆôuE‘nÎ÷FµÞ\‹ugj½¹$Ò·Ðäùoås«œ-ål9ä¿vßÛôñ6~­ämEÓVÖ·Ó°µ³ÖÎîЛ;\ï¤ßòß©OwáÐF½mäi£¯w[ßïÝ8Þ­'÷Àº‡miÓ–FmÕÓNþv8Ý+æ^3Ð^½íõ¥½˜öðÚëYõø½Hw§£¼­;ª©κFº³óÎæ«3m»Ð² ì.òx_Nßo·›ÙxP/R÷ô{X»ËóˆøGäxDì£0zT‹ôcüýv¤Çé xO˜±ž=#Ý V/u=I·§èÑ›=+Çs4xÎùójy¿ø÷ÕûÕõ’ÿ‹—é×Om¯ˆýÍúËџίáÿŒr Ày€š¸HŸjܼ!þ-|áó6=ß–{°9y—>ïÚ{ï¡ò•c¨ýajz>ïÓû³à[;í=9íw$ý¡ýù}èÿó#µŒÄe”¼ŸÐ`tÓHfV>ç;Æz¬>ŒUÿXµ|éýK>ãÔ6NMãOPÏD÷qŸH·‰£ÿgøOÂg’ØIú5™>“›ü?˜LÁuŠþL3µSãTû_Ñá+ë¯hò•ÿ…irøNOÏÐÇøÏPÏLq3ϤÇ,þ³ðœ­Ç¾ÝÓsœÍ1»sè3×3cý繟ïì:ø~O£ÎêÿÖÞBs²¾ß¢ô"}úžnßëÍb¾‹ùþ`¦ çRýûÑú'øËp÷Ÿö Ÿþ×b3YüÏUü¶¤WÈ»ïÕ°×àñ›yY«¶?ø¬ƒ±^ è°A¿7Òu£ÞyoOo’c“œ›Õ´Y¿·˜­4Ýjo«Z·ÁÛo;®;äßïNšìľ»ôx·~í¦wöô¸{ÍÑ^{{iä÷ ]b¯ÄÿË>ûÄîSÛ~3·ÖÜèï9šïïé¿Ø!‡ìÚ™h™Dqd’uÙȤÊD&]-ˆL¦ˆMŽL¶™\ŸÈäÛEÆo@æ°u‘)½(2‡D¦lk¶+2G8?b[dʉó{9RÜ‘½#sTeæì(gG7cÖGO‹Lùú¬›™cÊ38Lj;¿™ãð*S1Íð9ßœïÄA‘9Iî“\+/‹LUX'ãJ“ÈœÚ/2ÕÝŸŽã™Ög:¯Q‰µaê©q02g©÷,Ü΂_ÓYÍž §šrœ]ƒ­ŽL-yj©·ÖøÈœ“gøó\¾çÙ¯­–ó ÙÚÈ\ Gë¿PÜ…°/„WßzrÕ“û¢rŒvõÕZ_L Ü_¬³¾¤)ë™B5Š)´WˆoCû Õz©|—ÒèRz7‚Ûˆo#œŠìYY7æÛXîÆ%‘¹Ìú2½»L¾Ëi}9]›ô`ê¾BŸ®s…˜+ù]ÉïJu_E»«¬¯ÒߦbšŽb°®vM-FÓk—Dæ::]שûz½¾ž.×ëYsñÍÇ0¼o ÝµÎ7é×M|Z8¿™>·ˆ»VKÜZ©¥•ØÛßn¿µ¼­Å¶†q‡½Þô¾ï»p¾‹oüÛàæw*s·ë=úxÚîqÖV=íhß¿vðïu/ÍÚ›ö|;èE‡¾‘ñý’餷ô¶“v¶ß™OuvÁ£+Î]õ¡ë¬Èܧ¯÷™±û]ï™ÔñŽÝèÐÍõA<¤ïC®ÃxX|w±ÿæ÷osõŒGñx¿x=¦Ç~«2ëýãâWëÍ~=ÍDOõöÔóžúÓKÎ^üzÉÛ‹OÊñ$üžež’ÿ)ü}ezÿs53O«éiýzƬ=C¿giч=Çžwö¼üÏÃ}Aì zñ‚ü}ÕÞ·#3Ã}õûeÿ§/Ãï§ýpzEþ£†WᾆßëüÀ~·7õï-~oÑpÎïÐo°Z†È3” óžz?ðlù£ðúDìhú|&çú>ÖÞXñãìOÐljxMRãZú ÉLS×t=›ál–fã6Ÿ¹öçá:ö|<¿¦õ7]#³Àþ·r}«–…z°Ðõ{ñ‹éô–ȳ”¦?²ŸØ2¶Î/t÷)ö?°Bþ•l«é²¿_á¯Uÿïôû“~ëÔé;!³Q®MtÜâÁ7Af«µ÷þÌýÞ©_»è°›N{ä÷ Ï”à´ÏÞ>¾ûñ9@σ8þ]9²‰Y‘MÕŠl¦_d³%‘ÍŒìaîK·‹l™ž‘-Û-²GE¶\³ÈiÏó5[¾0²ÇT‹ì±-"{\ýÈzÏÎzvf+Î‹ì ½#{âøÈž´)²UÄVYÙªµüª»"[M\µþ‘=žwÞìi|«ËWoõE‘=]®3`ž1*²gVb|kàY¯ë"{Ö’ÈÖ„QsPdÏnÊ#[‹-yΩÁZ²áL-çâw.œóä?oZdk7gòyoΞß=²8¯ƒo¼ë–g=0|.\YÏÔl½‘½Ç‹ä¬_Ž­Žlçç#{‰º åj÷RÚ4©~^­Óá2œ/ïÙ&8\!ï•0¯sÕäÈ6…y5Î×àx-¬f86³üë`ü«ud¯‡ë={o\Ù›ÔÚBßn¶ ì–|n;ÙVj¹]í­ÇDö~Þ·³wѼ ï†Õ–nmÕz¯úÚÓ¾];Ч#nhÑY=]ªG¶+=ïƒå™“íFƒa>„óCÛ"û0nÝ›0yÿ­â݃v=Ôó^ž/Ù'Ä?!_Ozö2COâú¼§Õÿ mž•û9\ž§é jê 륈ìËøôÃûš½J“þÖ¯ó ÷o˜‹·œ‚õŽƒÕø®™ê:Lü{|>àû!Ÿ‘z4ŠŸàþYÓÈ~Ág,-ÆÁ›`Ï;gvŠZ¾¢ßtëz2S}Þ ³³FGv¶{ÿïÙ¹}#;O?æã<Ÿ_›oì}+ï·|¿ƒµPîEúö½š¿·^,Ïs¸TÜRëÕù½Âo™ë2½XŽßrõ-7W?«ågýûY~á[ŒG±õ ux&dW–a4ZI÷Uò®²·šÿj­¡ý|•ûW5þ¦þßܯu]KûßqøÏ?ÍÒŸ´_'n=¬ 07ʵ‘ï&x›qØ¢‡[ÌâV·éÅ6½Ýn&¶Ë»§|wØÛiwÒq¿]ôØí~½öèÿ^}Ý‹‡÷Ål‰u‰úJôaŸüûhµ_Ì~ów@®bº?hÿÞþ=(ÿCöÂñayº‹ý7]ÁûQšö€ý˜ZÇï מ-"׫käžìÈð{ªMäzãú4­Ÿ¡Ý3âžU<ž§g_ø/âû2í_é¹W­_‡3–¾esoê× õ¾o0^¾[srû6Í„÷±ŠãhxŸáý¹ó/ðùÂuŒÚÆð«/¾OscÕñ¥ü_êé—c"7¯q­œqz3ÿñÖàM í¼&ªg’˜Iö'Ómª~~åÜ÷enšœÓÄO×£é#7Cžjžál¦ºgêã¬Õ‘›­¦ÙxÌ–Žsp™Ãgçž«OsíÍu>ÏLÎÓ¿yã#7_®ùðç[g™Z Ç÷ßâò¾-TóBó¹Hþïq]Œó´_B—¥tþÑÞOz·ŒŽËáþ,þg¼ŠqZ·oÍÜ :¯TËJÜVš•ðVÂY{-WÑj•û5üÅïW½øïZXkåúCÍž)¹õî×ã´^® êÝ@Ó´Þdo3~[è¶E®­êÜF_Ï“Ü.3µK=»í6[»ÿYóÙãpÌ=âö,ŠœïÐÜ^œ÷ºßë¼ÄŒ”ÐpMöɱOÜ~üöã»_Mp9`¦Ðí þÅü¥Æ¿œÿ%ÿ!ùÑæºþÆÿï!‘J¬>kÍz³l]äÕÙ È'˳¾l^äSÕXkÇì¥ì¥ó¬ ëÏVG>Sƒ5eÙ@¶$òÙM‘ÏÙÏÙË|Þ:߇ÉSJl©1¬$ò‡5gb[ùÒò–vVº{äËØ?¼ƒUVlÙ‘?Âù£"ï;8d°i‘?ª'›ù£q:‡£a½6òåÅ–Sî1ê=fYäÅ÷¸flrä `¨µÏ ´¨ ¶ j©X›áYQÞã+3üÇ¿^•Š#BW6œÁ;± “óÄ~ŒÿIòœ´(ò• m+ãZYUÔRE½Þ óUF¾*ܪâªâ2ßj49EžS¶EþT|½æ«ËåÝ0º¸ÓaœQÈøŸÉÿLüj”c0j¨¿ß³ä>Kž³Ôt–ûšzYSý5Õz¶³³§ïï¼÷Ç|-:ƒÇ¹tòÝ?OÞóa_€GØuä­KãºêðÝ¿Ð~=³PNõÝ7`§.ã}I †_!ŒBý+TkC74_ ñ¹^#µÿ?ØØîøxÚ¬½`\G™8>óêöݧí½iÕW½®ºl¹É’lËr‹â¸ÄqìôâôFB àÚ]\Î q€ä€ã %p÷» ü€K8Zèð?’Xëß7óÞÛ¦•,çÇ9Ú}o¾Ù™o¾>ß|ƒ8”Bˆ½ÀŸDQÔŽºQ?ºç, ƒg<¶d[Ë`†Åós æö#„1sˆC¼†ãçV+L"A0 !FœD¢h‡½Ùær0i‘ ]^šÍûû$É–JDk|+míÙæc·™83Ž&}\kK"11ÑHÒÚÒÇ(/ák=ƒÃR¸þý¬ãÁÝ£GÖ¤<Õéïx¶J°'›ÒþêšH]ØcŽYÆíñ怯9n‡¿¾üÍ}žýÏóÕìšó/p3u=ñá]­k:«£ûÜáXKM4Ù& fC`aµ æo"›¸]oß7Âýf„/ü>÷7¨*÷ Ò£ª¬™<™B»ñp,ÂHi¶¥-Óì°Û„h$!éDN§u¹—å¿:‚Àâ^å‘9P,¶bŽÅƒð#oE,kb0‚·Ã’µ†x[: }%Ù0kÂiŒÃ8ÌÖ㤠‹Ü«ßˆÖè¦sÎ)M]ø[ø§Øû ¾ênƒÇhðèÃnÌçÎ;â|ãùÆŸN55MÉoçÎrà·c¨=8q&‹®«ÁžÅ1ƒÞü‘œ•_‡‘–WÃnEÝÊ1hj £9ä•M¶×³Ëã.CqlÆ,Ë^icSÐðŒ×\óÅÛG/‚Q=à>ûâ †¹±hÿl[b$â~lp˜=NÝ­Æ+Ÿÿõ½KãÙrlö€¾Þ>ÑnIØtºu.*;¯Úÿ!о¹Q 륓RgYJô 6épŒ³¦1q˜Ò7äœÆ”²¹n{æwç|ø§ÿî™m¹?·ì~`˸åÝ-̇Þ÷ç3{øÆ=gþôðÁ§OöŸ?ÖwòtáJÃoþ~3¢ûQ´¥p;üLm¶šÇHÀ,¬ô,à_…(Ò•02ÃaIŠK©°¤‰: ŒûQèCê³ä;Ð>Cô#?)`"*aøÜ$ôcä†á©é©mil‡)bXbŸZø1~&·™‰âóÿ‘—Ð+ôŸ;Ëþ-|Š¢R±}–¥ËKþj±*Ëk€p‘ȈÙIªjȈ'5˜Èi­®D¨7é`˜ çµóËBMPîŒB"Úí6k•d1›¨|⨾L¾+“¢+ˆwöXAª/|•­ÉOw“*Ñ bäòã/Þ:1¤Š*O}6yÒg´YÃnÓÀÉg\yæº~¯ë—>Û# ¯Q¼ö\ÜüÜ=¯âï ZÚ˜û Ð-WÂï ó{‰ndÖdÑóøÐ…Dë'Îø`!½òo‚ì" k+þyoÖ Ï€ŸY*ƒJ†.–F´‚ÑØˆ ó§ >îGH8óJ d6æ®bXF¶k åÌs˜¬l/†mR 8ØáìðDl7ËÈ iI“¾Sq†ÙшÛä“ð‡4’Í#ýÓ/X6·=Øï æ¶pÜ/ÿYrÛ% þ0ÓÞ? 2ß6K"óö+l¿·?¼àéêb~ðÉÂh,æ…Fºn×ý+¥ñ`Ög2L y|29Â. A¢UÊH”vtX˜“\VIÏ<(üâî`µmájá—÷ñ¬Öb<ÿ±ÁnV÷ö+v;Û×zþ·„R0º:÷îo(^ŽNœ©¬ëCA†G6pk‰[”ÿÆQ¿ˆ4p òp€÷@Õ,K¿Ä@'ð,7_ÜD5Ò ÅlV+9Ãñ¸Û":ÒV:~P02ækëÅÊtròlN²ì/bnÞÚ¸¦Ö£ñIFŸ§·wááÂÔ>Êú`noo¸gO{•uÞ_¥ß0g7ªÓ„un¼ð&ߺ̊’ÅöS‰ÞJ¥RQ ZײUlò]Ä’zøwŸÚ¾ýSä/XSjÙ}ÿbK«jËýÔ¦zëÅýû_|ë}¿uöÀÿB-«þ“O¼â™}}'ž!þLÓ…ñƒüa°]êQ'šÏ‚]ƒ;}löÁ=-Ì_¢¡œÕž-ïÙ£`"Þœ½µ ë>Jð‘N Hn³ø³m í9ÝÅ|³… fæÁŽ…ÁE ¬Wú›Â˰^>°>¾5q&Büu/È"=æ(a’/¢üeV~Q­ÐH˜Ò‰Œ ؆´@ Ü±ÀÂôNœ‰AËÚÒ–ˆ´#ÅIUÛ¥0Ù¶•4§6,ýÈOÚíüðìlÖŠP]:†IøR²s¬wÌ.Pú¥n\+[FiÂË9ŒÈ]/,¸Æ¿tü–WÞ?9ùþWn9þ¥ñ`zêäÄšk6¦Ó¯Y3qr*· ž˜VÉnfâá…Ë~úÚk?}ð ù/Áíq1Fà Ø/5Ù$0-°+KÂUysžÚ9풚Ò ÉšIFå}d|=¨£OŸzéú®Þ^ºéŸ¸öÊ«N~+—á³Ç>¼}ÏÇ®øæ-G\§ø0fø] ø0Äç~V ¢ü÷âŸv;­´4ƒ;!‚®¢Ë,þõˆ2üø©/^×Ý{ãK7üã×;~ÍÇ÷;¾ñµìUÞ¾ûcWgÿõ–î§4Õ•»ÀhÊ žx:›Ò² &áX^ÂZ*Ï(Hߛȃ‘´6F$ú¹ ËÊ…áëã™WV…>¿ëŠ‹‰‡gr¿Ý–»Ùý?þå„Cü'î0àÌb¨9Û`R"*Æû+oÌ"‹ø<ÐÚU&x+^3ÅÌH²õ˜ ;oñ½w¾|]Gld֊«ÛyÓÌÞ•ÝÞã¿öÆ{ð…æ;÷?83pb÷„Ó”ôå|­øI—íï"=SM»/'L\x“½pÛ:³m`Ôðª3à Ø] )¸ý0TÊ1fg†¢É†(q°êŠ €RA zÊZz ­-õ‚Ê,l«Îk³Öv¯Ëlkš·óPÊø {بÙ>®¯ ¿Á ¬+[òä¦R£Ñèh?k“9G@œ¥ûS1 fh£Šx9™—+Fd´I6*Wl"ÙçhmiÇÀÿgÝ _cFÎßädz¾Ï7îNÇÎO~w*Ä>÷]Ðã«.œcïeÿëߊÈšB˜åëÀóNˆ‘O Øht†åˆàG¢ÀˆsH1&`W š¢ŠÂH¬ýÚ¥›ËÔH¨eà¢ÑI0ÍQC}Mkº5!» Ö*Uãj9&v‰U ›öüî#¥Êͱ›[s±õ³F}’,Hs6u%Ÿ•EjëeÕœÓu‘EÕyCq;®xMéú²„ø?Ãú¡I´d÷Ö0Ð͈(è‹‘h³üë'æô¬º¨­H# ¢Fäb³Ì¼ :ŒYÏ™ Œ(¢)­ –… šadÿÍN}>Ç»v篥p(6„ôú|°À‰©mšÙ¸ujëÚ‰‘áþÞŽ¶LS}]<ê÷ÚªÌF½ á!‹™t¨Ñ`²~ ΂t±Ée–œè œ_óVÄ08&Üþ™÷]ÑËÒÂ×M·î˜l‰Û­.ÍpõîË68Û˜ÞUϪ«'Ó÷üä™É5«ehØòwWy8Ñþ±Ü—ݹ!<ÚW·®3ŒÿiçA~£n„=7xý³‡ý ‡ö®›Ü~·A¿¥±?n™»¬y¼Þ¿|¸ýØŽ>.°ë2?Hëäúu“ã#“ ÿÂûjÛ}ݾîíƒøé®¬›LQ^¿l"l¢ô¬,Í,X`?ÃsvÌðÄ®.~ÂòybðC¦!+Þš“ØÌ”À2²%%÷¶L#CM–²÷ÑÄr+Åú'ÞëA(òÔxkˆ_޼¦*‘2R>qCvSˆM°/É«Gv’oÌu=8;sÏ®–ž#ÚQ¿ª/ãÒódw¤[š›7v=M«øÓ ßéY5|Ë W^÷OïY[nðg½v6ÝÙ–[݈OÞ²uññÕÉí C†Ùs€­ËI¾æ]˜0ŒŒXÁÕ"tšwµÊD23¬ !ÚÏ¥ÑD©OFü\‡L–Á"DyKï°YCÕí5-k›Ý·7u¥ÂN¿eujëÎÝ Óì:ö×GæŸìg5&ÉáuÔn<¹ê‰¿ß¹£±?a¹ãï¯?üÒ½ëVÐ9‚¯Eæ8\²¿ÜÒH÷’Š÷— ÏV¼¿ì|—ûËÎE¾Vo}mu2¿¿<Œ‡W´¿\@ Ù[fí…¸'¾Êè±»Ùêñ›¶·*ªŒ×®Ó×tDLážéL´/äd Š_uʸáÔ™#22çÊq½v•¶^ï§ÝŠ¥7*4%šø7ÐôU™glk0oØÑQÃb~f!1ªmËkN¼µ–Ó”€ ¼`˜|™†@X…i°Jb“z€”k+…Óc­ÖXB YëÆ «ÇF¢ÖT%V£kEÄZØÌs0 ïjáCawÒ –äŠ(Û0{Bu[NMîY?ê¼sÛäÓéܺu]ûêrœ7Õä®í¯±ùê:|F§~y`˜Æ}m½G¦›]>×Âk-ÙŽ½÷o\¸‡¹vfGsÝB Ÿ´¸%m 9©™\3ì×; òú ïÀúíÃ÷Lœ ÁbX§0oÜWÍ`~ë5ÊòÉëä ¯ª±–߇±V}ËÃâVx¡ƒ³Š¬í…u–ÐÀ:é=è@X(#/çÁŒôò‚QI¢Í¯ó”AǨ 8ôÿ¯§ ô•u¢Õš‡Ê[¾£ìÈ»éÀuºR Í`ŸÝ¼qrúÕãÙ¾°5%‹LÓJ¨°„É.8uWˆ×TÑ¥ñ»£F¯ËÉcÒå6¾hNúù)î°͘ÿi—çEƒ×)Pf ¡Ûoò—§L¶q_«B˜ç¿m²â›3÷~GÄdN:sÁZ|•Ó”ë-Ðgõ†Õ@Ÿ.“LŸü€>¯`<²!fÙ.OK€Áh­ì=–g­òÜPö¼yÑs-ÿê#úhv6o÷‹X‡EÝ<Ò1@ÂûÁ[‚Åæ4•`ðÂÂk›´­ÞpCº69~`€ x=û‚ÞÞ¾ol°9P3¶­¹ãÊŒ)j›[޽„øDbä¶½½ÛOôÊñ"°ƒ(ßÜÆêd2óœPùæòµâúO2nÖxÂ)5J Ó’-j—iaäUn+k¡-n‘©Yür6Ï‚=åŒd>0çùÀTÌHÄ(2bƒA? >“[¯2ã%öá*ïƒàkp Ú­Ø^ª§še¸ z2©=™¦ÉT‰A+uJ–bz‰N `Œó—Ð92‹ú&‹8·Dß`x1Zn~¥¿*þDvË¥õBBé»\H”w=+Ë÷Í7^}Õe»wíØº¥DNXþ·äDYZgf±Õþ¿+>_WœêT6SÒã7nký_•(ëÆµÅy£²Opñ W2 êò;à;Á£<ž5Û0Ï€£MÂ6ˆSƒ’~IJ¦¼Gì¤Î2šÂ‚‰ —½hø1ïRÓð£ÃíÂàA»jÜ5{•E# 'vjÔÐcÉ8¸Õd{£]ªÇªåÃ6;ü£ûƒû×ß±³µçÈ;7«Ëå£ÝÍuöÄl¦i¼èƧsvhjø¶/]ôK÷¯oJ3üÛ—½döU{ñ‹ñ䆦gN¬ “¼£ð…sÜó!yGa’wÔþšwÄNFî͚퀃`pÀá !;¥˜Ò©jeT䃲©ÊÍÔ€l1Fuµ7ƒ;Új;ë:£awÚ“v:¬’VÄ.Æ¥÷¤åXèŒõ1XŸ0jF§²æ Ö ‰8£Ñ1§QÇ–Q °@²q‹‰Î¥G38PMQCÍ’ iæiMm]Oöi¢‘šT¤9ÚlM%Ã$gQïækÍØ FK#Æ4s!Ê‚[´hLøg¹÷½ï wˆw†Æ7îhŸyÏþŽþŸÜ¿ùú¦Ÿqk5¦÷tvlÍFýíë¸}ùËO¼ó²Ó9éH9úoüüuG_ºsuKú›ç¯íjè…WŸœ™¾rȇš}“ÓñQ-Ê’æ8Í4W©6m5uœˆ³ =‰Hš#ûOjúK}]G[]¶>›ˆù<¼9mý8‘¬Çý œíN‡`ÆŽ*{Iâq“¢•PcÁ LJÍF÷@g6•{ÎN~*c28Vºeà–—®Ý÷™[ÇûlïÖ÷wÚ¶­ÿÊm-³C)Ö\Ó=Ñнµ7ëb/¸kM1{•¶+³ÙÒÛph£³ÆX½'÷ú×zíñÉìõgŽnyêú±¾ž´Þ°À&6Ý¿?ÒSëIom™‹©üúuöJ tÃYâ¼a%ø“T£“ Ühl’øÍhJÄùȤÿYQKêa»RIŒÒ5ɆTCÀç°If½%pB[¨d[_N%ÉxwÓéÿíÏrþèým'^º½nÃÔö.“^¿‚ˆ8÷½)>4×Ö«­AßìŽô‰kþsÁrÙ.œÓü_ I´‡é—y†’@µ¬ÅYÌhSX6Í0}-гhD^UÒŽ_¶Ý’M4jÕj¡>0°‡I>“FèÓ4Te6²z=šÔ ,KÍa‹Eœ’L 'ŠvQõ`dX¢À`t1X…%sÍ.«lHæ;b™Êý\ SÊ,›Ä»è‹Ðû(É£03WÖçr=A'¬Fc”{Dro0(ŽxÚ¹’/±ÙÙˆNmDhë–{¦ö©LnX¿n횉ի†“©X$N¤—Õµ´ô ç£x•¸"Ëx),•ñÉéqìÞæwo wx¦Î›ì©và=mWþ¶Æ©-{ûÍ•xå™à`¨mªÃÿÆë¹Ü'ùkJGa–Ì̽»2:Í-~©*=Ö––yG+ê~°ˆu¸·õ†«Á¿JûN ÿ¹›¹}M¦j³ªD<Ã8ëÍšc”·xÌÈOòà ŽR=9ƒ&yEô:D£ÒY5,á"« B“Ä}ºì LÅ–r.Œº£½ ¤[’.§áâË Oè*/܇FüÑ;›·ß5ê Lړ誶ãÍÊÞhwÉ“ÿþ¿¹Üs•aFÖc7›ªtÑdk„üÚæAVµâ™óE“EÏ-äp«¢ÄdæV^‹‹^Ó7B…7³Š~ é7BÈ«7½® ›ÈO¤Ë*ºE†‘UK1H-µ°JÕÙEÁHfd‘»XûYe=[Qks£¸“ª,ÃÅTV5 Û<,‹vÌt~ÿ Y?|éŽÆé-{³š’Ýî •õV~^¾=ƒT^Ñ< k¹[Y,Yëèpft)¬A˜5$œæ”•È¢7 ïÔ#n±ê0€¸Ñ¤^‘þ³QËÉÕä–¡ÊÿrPdKC)UíAf±’"tw‡ÊôÒ_J'„‚ºKD¾³usàŽát¥Za%°²* Ékÿãc£#پ]QÒ¥)’ Å2p™o)Ý? „ýòÒݰð›rŠ,î$þ²†3ñO‘ø‡Ä?&È,ú s#Ìþ¦¬ÞL;‚|@Ž%VÓ•ÔPÚ£yKÊGpƒ_+¨û¹É"‡@©ÁY±%ÉÅõµ±öc¿$YãQ§.”–ùÅþš %Ñ7ä}CƒÛÄpª²Ec‚ºiÞƒªhH«„z˜ž¦ï3êpÑ$/ÿ²†£ ZM¾5Øòÿ•!@J‘c 7_òKµB²ÀD’( KD£Q‡ÎYD;$Úè´Óà% £Ðz¥§ˆ0zü!OÚöê”ù®÷ú£ÖïmÍIf¦ WŽž>Ãi8¯ç×fÛ÷ÚŒ£“GÏ'Èâ*kû/€»Òµ}ÿ' 軲P1$ÌL~…›f°a`Ã2òEô$5Xd#Rá)mù’ëÔ%W”E¡¹V[Ìû•@@BxÇZf¾ôG–„Ú€ZêjkB½è)½xÌù¬Â ³øgøwü×Щ2<¸ò|7³4S::•¢‘è | ï Òˆs—JŸçküH¶£­”· !%žIÆzÆêC!4–&)Ì„¯ù’H‚R*ÇPIp  üä8íÙ¡³/b­~"ÕG>“ç©<'}†[øB1;Ý[ÄG|ÓGU¼~b]}ÿxê*™Kô„yò¬TŒb`#-3ÄŒÑiÈöʰ¬1TDòÄ#MÖB;¤•­‚Vno¾ä_ öYçb½(”‡ž˜SÇU^‘` àF£/™ÏÒ„C°‡Ð{C=¥õL0S #% ÙsÏsµ »ÚÐjô+YTc‘'š@Kè:As‘Èd‰¥×ç?jµyƒ’D¼‰qKíMIT7ÎzÀ‘<°ùw‚Šú "p¤@Ó-ò‹ÃôôvÃìÚZ[ì±h’’¸Éµ ‰çÓ>3åÇìËϰ>UÆBŽ/¯FG†ÆsÏ1Ç=µ=áPgÚíNw†Â=µžJŒRÈ𕃊-{ª)ïà° w‘¿]¥:ÝG"ö&Åu,åúMˆy^/ð¹æ"|þÔb_н oƒ.ÌÉ6ðö´lq_‡¿wËŒeñ¸ǃ:Œð@æ³ ûXà³Þiêè;/}¤)~¤Æ~H>?eÔ0Ôœ5òŸÉy%nR'ÂLÍC&=9ecÄ*‡¶¥È‰o&XÎ4ŒSÈh\:¢@k¦Œ2—h¤«—„n¦úG‚æŠ{¹(,Iš/C ԲÕ·z1&ÉS¸wv´·4ÇÁG§Nª2GÓŠ/%´Ý á£R§ÛÅ’eÑ<×–…¾Ð—¼È‚åÍkäçŒü\]˜4Ò œ ãæõt´Å‹bPÆi,_:VM%S­â‡Ââe@³M*4C‚Ž 7^†âg ôõã€*N%3q”ev‰.reزHE •oÏ}!—{&G˜wÓp=6Ú<Æqav&'zÒöï©–ëÿ ÚšâÌZUJähÕ{‘ã à¿]wã¦:Õ¢ý¯ «"…OE³¬ƒ‹øôY.'Šô¸ÙJŒJ_hK^d%©òó2V­5bàÚ@pMÆTOÍe|Úž‡¡ k*¢‰‹šh)/& ^6Ûœ3áELWˆP…8mÕøhO×"^³¯®C€kVÆuˆâZ¶£iq¡We$YÝØÀ{°``a[y¥-{EóQ ÒÕ׬*4Õ§Æ¢§ù¢&³–!˜Œ–à‚M&Ô †Ò)V•-I/] ÌŒ/¹“…äñ©öÀ¬¸²²}6ãzZ v+]® 0Cx£À¯‡n  “±x…/Nk¼» ãéëÖ’ šl׿îÍ‹ÞMËë.¼UºîÄ`Þý®þž½ˆN¤Ê¯ô¹GÑ•šR]©,»‘ƒ±|Ù›eÞ3+ fÂDcªßJ&k)[÷>ºîjSô.zñ”.¼ÒSܳl”¥ÍØ­9£a®¬+´ lA{V¯ $¬0Þ±mzcE *EŸ‹¬/±½:P^§ð±°„1œ5x A"ø ˯ø’WYk‚†0å7¬ü&/9‹#ü ï “ˆÐ&qòy“^ËÇ(ÛhÙä’ #²²x«à¢°€eŒcÉ®ÁÅ€uÚÓÝÕ)ï"´¶{”"Ù\)"¸H–X©Ô.ô™å¨¬6sT×2 E®êRª³Ø¼U=X¤ÈtáÜ"™þޤâ;/ÓMD¦›*ÊtòJ[òJå`ù [ТڂÁ«2›ŒCSÿ K³‘Ú1ASÎd˜+…_Èüî~ËACÌKJŠ•t!ŸvQ‡[Êä“Ë‹ˆl†¼W4öâ–æóÙl˜Hó‰ÕT’¯ï^¿ˆÙ«¢r,È–{^8Qðk˜O*~Í×@ÔËh³UIÀ3ƒX§!?H˜ßŸAr0<}ÏÊï½ô1WþXeÿn™ý%NTX,ùÏDlæYŠs$±ö!-0šn ét’®¨[YX,Ò’¤öQz⦤²] èH}¸¢žtû„…EJ—Øñ§ÆVÔˆZÍü»éu—©ØR:ZSEý(ô„ñÞ=[·à)<µQÙòÚêK*²Íi¥Ê£àu`·b¸]L%÷‹<×–z_â}ÑÇeÎWDf\Íb+×PÆ«µ•á%a"4 v1Å[€È¦ˆâå–¶  Å´]ÝJÎ4å}*ã%ùT—èH]’ûto±ßdÏýg,ìñðÿ¨ìñÜSx„ÄüÆO$€ îó4Gú5šüGAÈä1%&…ìÜ$Î×X·¥QÄåAf³VŒûzZšq®³Çã4”NSÙõW¶‚ìâÿI‘]߀¿ÿ(SŽÊ®®NF¤ÂK["¼ìðÙD¥mÀÊ ¼ô9·è¹JŒ²ø2)âËhÌ&áù<+™ =þ¥ÑÐx;oÉô/iÉPM²’n"J78~‹lõáÅ»i¦îO±‰£t·òNäý×E&ÒÊf2«x™[·lX‡Waº…:wsíÚ ³žeè…†£MkZ¼¯¾Â&%)pKÀãþd´¨û½ ®5·®gðÙ* ÖãaÌèA ¡V¬¸Aù¬M+¹´@#hçhÕ‚òtpIŒhÒ Êy’ŤãÔ´Ç"àòüe€#ÔßR©„/ýù‹u@–¾=ŸT¹£%ÁaÜÒ3œ~®bKÁQ'uògÐÌ´¢:ÇG‡ú)eDª•$ tã“ Q-ú£·5n»c*ÔAr\±Î¤ ¯n9öüÍõgv÷3ËÑÐh´ym«÷ßËå^VÈh‰ô  Ë5bÏ?‚‚èÁ°¬BÈ9©@ñÞ;QòθFdÔ4¡FZ¦±lϽBKhTé=-É£6‚¥ wPQP²Há¸CëX„úŒ‚MAFæ£þØÝõ»Ý¿þ¡±OžoÜìß±ë»_WkjÝûŽ‚Œ{ûZîû Õ¡”œh,Û@í0×NÜ!ó‹A²0òÕ/䫨~•›DEªUøbM¢‘§Ñ cÕLM×!9JK´¨ `‹âb^]¤™ŠIBçë €%­IÂd|2•1*Ÿµ§Œá|u¢Nê&gè’3É´?¹—®SIÂ]¸“G$Á.eÇ›ëw=²Ó×ÉžeßD]hÚŠ>µh±¨ÉXFL3<µšÉ¦E-¸ZˆaE†Øµ¼Àò¤˜®™˜Ã<9z¶K©J·Å ž_ Vb6ëÛ°£MÓë·nØ:±j ?q»$3Ç¢.Ü¥''ÂÎ08¸fìPÖ–Ôp•K—ÑúUòÜRÏ%É '¹Ò#KàÒ¤ü­ž+ê°Íí_WÕ¹ïÞõ®zg¼eúбCÓ-}'?säÐSó™5CÑ´9TŸêÚw?î ´Œ®Ÿ©WÛ7t÷Í´:»vÕ* Uì›×N‡G°4w¬¾fºŽã\!¿GÏê\m[ÇoÚÓ];¾33´Ýgms44ÔHÕwÏ­¾fªîŸz#V-Ïñöޏ»& YÃõÌæé‘ÑÉÉÑáõüÙ3ì+ìQ3êGŸ‘YÖZE®½Df=8)šCžòeOge€šüDÁjx†ãä2ì¢'v”³‹nDSæ”Æ$bRuòR [2 Ž;3ý-ý0ʦTu4×Ùˆ4.¯©ÛÇô–œåg†¼ÃêqN²"ì+÷~þê£s«}Ýú€Ë™ÊŒ¶õoï ú»úÇëæÞßÖsä‰]uý­.£vå^íß–NÌf.»vž}ýüám_Ùo‹Ô{vIVWÀëߨ0¬õ›FN}þêË_¼{Å_ãiv:Îÿ©£ Ÿ®K~ðôi¹þäM@ü¯ó§‘5fëXP½rEzZ°‹”%•ëuñô¢-f˜ò´GŸkqm®B"îM¹’ÄZþôâú:$vráÃäw…Ÿ8"1ó4ñ·ÈɰÑìPÈÏð"ñÃpØA$òâAóˆ?X6222Œ“‰xÔíÄì€ñUi4¶váüΟ`~~Ô‘mÕ`sc^ a ðˆY.HWaAózÇ–•LR¾c‹%rÃð¿ YÇQ|osì¼wÛäÍ3µ¹ ºÔçîåþû¥—Nÿªe€NüifËÌΖº…nTÞ/Ø {ÆsßJ澑;ò¾¢Y wZã{ŠöVŽ´ÑSÏÛ­z-æ0WÅ{Ig4÷<þG¹è5‚ÿ˜ïãùÞN¹w„+¾w$¥‘ŠB³z‰¦¤Êbµ&I¥Ö%w‘8ÜPzÝòe<˜€Ö“Vi ßÀ¾"Ï•µ3E³¡ Y‰µP? ßBCxº)÷×ì«0¯$X9ëѺì jÀ0´QB€„!‘LJÓŽg”.• jqq7!Ô5«å I’"œŒÇíQ½-o%µ—]…@™S©q°èV‚²ïøÈ–þt,hÉÕîúàÑ±ÄøM«›Ý ‡×m:µ©§9vDû¼–ÊâÙˆNKû`Ë!á¿VdÇ9Iwyd»y.ÚÒ2žX¼9`€‡Nl ó4†2Iö-9Nu,ú7(‚ü1•Üç"ŸŠ¡úU¹ÔÞø‘ŸˆÈÍUËz÷øòeüuþôÒÕ•NÓq$ãøÉM\eã’éîÝŒi>éò>GÆtN“cúœÁãÉäî[bL„5H}tPŠ&Ÿ™ÜcÒi†T*—Ëôš ¬eFf'e­vziLT¹2?z]pk?³v½/e®[00w·=1¥Á,!r þËÊÍ6X¹ÖFçšÄöñÞ!˜ÞÑ»úüÓ©íõÝöâ•Ã×´4ž ã¦óÿŸÑ€g›Ç®=õÖ‰SFCîé »kðóÇ Z’©ð·_:AàõŒTyÌâ³­›¢øUß39ÿ3§3›¢¹j/àdðÂ9á&XÃFÔƒFЯÕjXƒ@ÉÊåŽÉ7­òMͪÉWéШÓá9£–]›ô´Ö°AdJ+'. †Övk_ÔœÄ÷ÔÖhIH`X_osFÙÞ‘¾‘޶¦žæžÚšHÈçQÊ7âFS¥rÆùjÆ%%ú0.öþÔúÅol~ä\¿øïÌwÌ·­ínty«VW_vðPæò—zº_¾iïÓ7ŒŒÜô™×}¾«ý³ /;tèØñ+®`Ó{ñŽúñâZÅùz"ÛölÛ£ïî­›ö˜¹W©LÌ r/söO´.ñcY3H~D »hIb¥þ°R +ÒD))N KùS´¹ò¹U^Ÿ¸¸Í¢úÃîH¤ª´µD")fŽd-=^~GöÀàÚÙ–õwì"µ“vM^Y÷ïc{Ò´nRWÐÓ8œ{™:œ›Ã)Çð©—¯;úòýë›jrWq‡;Ûrãõ¤tÒÌ4)Dë5¼€ž·¡8ºZ.Ììv^¬Ex W,¹\‚›¥Û¦µ:ì`ÀxíqG~Ï&W]¶–U]^<åsïV EM©û·±Ýé|‘(˜¬ð_¤ØòЭ_¢3íÎävÒ™®nHägJüû—Á¿'ë_KêR“•¤…¡BA†#wDs´.5ãË\8Ç,€,î%õ³{ów-ªŸ]rU‘³øª"ï š‘ˆøçV|§%[.¿XÌuN©*ÙГìLô¯Ÿ^ߟhÞýÐŽÆ›&¼$öâHÖgc™ ypïñ½ƒõswomÚ69äaÝ’Ñ괶׫ƒž@uϦžñë·4W…k=»$›Ëïª ¦ìµM±Äà¡Ù¾£Óð¢ÞKð‘{šâ£ì^#c…{Œ—|¯‘ó]Þkä\â^£TÂi¯²T¼×/‰Î‹ÞlTo6š|oÃ2øí\´*‰‰£«Œ‹o6ºEÛ_·$Ê·•/Rÿ‘™N]céUÏô.­sü«üèF·ÙÓ.žEⱆ˜Ae—À2µ¢‹g–>§…q•G¬ú¨PŒ/ÄðóÌ °¤ˆ5xŽÖG‘¤Ì6YcÞª°½ ÈB]Üž"H=âõ•!én½Q.[Ú‡‹æÏ-ß­A?k'åŠÍKv磕<•îo€ÿß zøÿwÝivõòýE®ö†/Ö™R÷Äñ#‡.?°÷²=»“T„¤ëB¤>n¥—æK-΄ÊþJE4­ìhIK«Y+ÕŒdú¶öø{z:=—*ŒFú¼©M÷ìÚ~çLõÏIÉD·ð2§Fnº“MÐB–=½=žøP&ÛÑ6~ogSúÒDךãwìëîÜqMï×^(ÝóSïj£¼ó ó¥–ʾլÈu¬Ž=¥eîlFœÊEžjoû62¢PÖ¢PVWiaªÐbÉ—˜/¥îEF‘̼“­–Ü2`àxÜIÏH>‰,2‘,KÓ4íCØBBÓÈlv›U>¼ÄN\‹:qÐjN…N4"£¬ÑiX²¯W¡35otq_„5ׯ°/*ÒégaR.7¹d·d=fÔn£í~¤%Y¯ÿ “¥Ü¸²Î‘ɤt­Çí9;»|§Œtäjð•öNâŠJßJAÜF÷Ýså‘+.ß±mýÚÕãòÆg{[‘J|?(­H%^T H+”4ejT*¨Ñúw!V$z’—ªq—–JIÝ‹J¢þ#›:u ‹ôóñ ç¸þ!4Ž>B÷ÉÏú1Ø„ƒ^ø ΀ê ñÃ’z!àKãüþI¶±ç3–—mæ"‘h¥ôfe%|ç¢÷HUa44Øß›ŒGBn'ÇãbÞç`Ù¸e'¯¶@|} ³óØé=µ‘LO&R;°v ¶gß«ª7®Q„~mk‘ô[ǧÆãííq[z¨þÄ‘ÜÐ-/ò í»º©}}Wm²Ú¬¯Ë¬>°zúæ™t‘8¿ÒÕPß7;Ô±¡·)Ó™ v¤\~øü4ûÙÏÞ2®ä\17~]¨![+§[a1ƒE©±ÄiP®Í“Ý„xÜÁWUN€i=žÏéh™ÏÄDirÈíNÝ–ƒï¼Ar&ÁGx´y}5«ÓÂïÉm†ªÓO¯'û_ì$Ýy¢ñ¨iR<ÕŽÔLŸe¹*æbžb¹T@IcTÜš1,sPm»D³YZ"ù< Èn—AàP‡5  ½(á²<~úÔ}[m¾´jîøóŸQ«ü%šss§Ya?Š(‘ý·/œc9&O÷wnü…“?ßÞÊêÈY¡"Ì¥äKÚI¾°Žn’-¶‰+k_ÀgC >+áb0bØŠDoÍç!/ Dí‹EÉÖb´1Ö(_¤7Š<Ž0¥n3F ¡3@CÕèõ¬N„®Â`–«˜HðD!"fËמ\7ª+Ñ®Uñ°’Ö.­Š…:¤DA#ÎËPš P¨ˆ”6"䵨r{2}su*Ò»¬a‹Þ•¶fZK ŒîH—ë•^m¹²@hØ”{³„ØÆNÝw%óä=r»çÃo ì·åÜqð‹44oo“,;Mu˜ý f¿t¡ð@ó1™XDC>YÝ4¤#‰ÞÂ$Iž0Qá2M3Üìl![oe.¶­ÇÄ&¢æ—†De€ÊqF C^3Ÿï`%°¤þëŠÀ X¯7ÊÀˆ‚–eëµÇãR‚ØW³,Y^Ÿ.p¸b_6”»>^²žÙˆ£³6ßcd½ñ]Í;(åÎzk¿þIiº^¾Ê½‹°øJþ2¨*÷ßÕ¥ö’ GÛVÒ‹NN„à8a¬JWYOÙU嘀SôÒ¯¬7Z G½¶ˆ(‚Ѻ5Ã]™æ¦Æ†ºd< WO›MFƒÈ£^Ük.+w½8yIÑ|ÙkkY‚×M ÕlkÙqÙ@vCnW,ŽEH7Iˆ©Æ<¨ž*Id‹õë¥Á´l»€uw‘x\NæÕPF“QkšÏ÷³â²}—\…%ɨtA$ªªwÉ>É¢G‡ˆ#ßÓÝÕ{sXÐ6´–êै"Z‘œ*ëfþÑÜÕɈÕC fMËã·Ý—{_2jõ¨CžÌÙü¢v=ÉgÆdvݳ)Ò˜:_ª/íøÝÏóL]‰„\~á²æžþGÎ ³’ó韾@ÏxòðÈzÎÜ‹Yc“$§(õ’'R^«w™h TUeBV«iHOÏM’³[¤'h8Ž™¶èÈ&›qH²-ÖíZœWƒËt¥ÁËuDk¤¼µÊj¨šÏ÷ø®ú"ó_q_ªIÂ4˜í ŸKûË®_Üx‰‚$ iå}"I*Ñùõ ¾üÀÎí3S´LîèðÐ`¶«£5C.CECËnUò-·0[ìJ¾%È{áS ï{q§Œ1*ô» ÒÐüå› Éç?Äd! ƒ+È]£AËj4hZ'˜Øb%pñ¶-6’²„X«™¯DŠy–‚гV².È. ’mX®5"™F ˆ)¯>¨!%_ ]“ŠE‚~»Dƒ(†Te–/QÙ+Ò!·Â<žËíUԈʹFV#ï¡jÄ"K…»+¨‘"Ý^Q“мIr§û:Ìmñ}6ƒ°d”ËŠ¬<§¦Æ2S,VsÑÔ§ÔÜDˆ¦‰©nÇÃÖ¨`U²dÛH’l´èæ( _¾n]çþºÉv¨ê~hI½’3Y3µÌ7ÏÎñîÕ{Iòª’Û ¾´@ÍèãYc5ŒÑ‹åÓ4&§ÕÆ)ÊV9‰6Ñ^‰W™5ª4°2rùnÒ¦àKr"+rìü­Q¡±R<¸5Çã´dTç,™ö+aƒ­Xá/£ ‹Ñ†‘ÖåeÀ™„¬Ò¹¥ÇĘûëìë^kŽß¶½~®¿cóÈk""Äÿžý J£PÖwhX’Ùªäb*¾T8Ýâmi+ýÎÿHéOÒÄÖDRY!Åoø’üÍz«¨w›bž+½ á½>›¦Êx÷|%÷¯ßŠ?aÓªþÚTý00[N¨Û\S³¹Ÿ·y<¶·m*½©¿móœÖ2©Þ®žž…ï› Ö‰= øð!Öc"v89ªæ2]WGRaΚÖÊéÃýVK–,I2pY’¢†s¿ù“×­;Í4‡‘7r¿Æá?y#º'9-“>èþ ôúºYʽbpߎåjþè—p·Þ¦ÉýFòËxcs/³_†q87ë2ñLÉÂHh¸6…Áä׬¸qøßôÑèß?—{è¿‚AÍ_é-M•þ 1áü×Ü=^ü^öý¾ªœÝÛæõµ{ñd¥0j–x ~ÏŒY/=+‡‰àÝ$¢Å2$»dƒ‰c› ’jG„@q¸5̼UeÌÝ‚?°0f‘ð­¹k˜O±×ù𥅧¿z¹Ôlcv~ÑìûO°1æsü}ȆªQ+z&k cN¨Ã-x9µF@ â9‘ãE°E†™ý TµH«!g%5Œv=ñ€t vÐfâ½Ò3Œœ|£SŒ¦,c(àøNÇ–À|Nרí5Ô×´¦[íÕöêx4às;IìO§A6l3šÓX½î°´·f$¶…¦¨PæT’–È©šVÊÃÌ·[opV×e‚÷†6Ü;ÿüóéêP*°[ÝšpûíkÛgû¹E2¾¶‘”9ÌÖšœA«Ïª›Ùl3ÎÜøê¦”Ñl´»šë¢ý›š/ —gc´¹M^Lç&6Ääø{hîèQyÇ®!J³×™)šuÕƒÙ6ž˜µ Þ\hFÌ^/ùDóËyR¹•Y¯&—2xlöÒ“Kñߘnh˜>16~r¦¡aæäxowwï@w7OÍú«ÆÆoH§7»j}M®víôôš5ÓÓdÛ~ž­e¾ÃŸYã@—ŸµbŽÅ 18Lóõé­{s“ä¼L¾¨›MyËÁ9¼•´¢³B`TßÔZrâ|#Â}Q’¢V"jä˜QšOjͰ™d=Nó2ßyÀæå_´óëƒÿýñ;÷'8eÁS¨µ™Œfuî‰ãz§ÉäÒ“¼¹«Ø#Ì  ¼í&t&kòa–¯Æ‚È‘ Å”¨I;E#Îåu‡IΟ¦hš#1çX†\ݪ ¥Úgù¦<Ì–gçTÒ†¤òr2A®zK4%›ÔŽF@qש„󄽘ª¥dž¬ñ9·[§M g&6ºm úÜA·ÇV$ÝxÍPãÚ6ÿÛΨ-ÙèÕÚÙ“F»-œñt5FcÜV„dê¬IÛ×ÔæþáJ“µ•Ñ›$m•YÎ…~px‡°9QÍša¡ÏËðœc^UÛ!’©É#ŽŸÆìZšfÈ`5C ÓÂdá>`„ã÷Ë-‹ÍfMnW(àJ¸R<A”•æÊWûdHæ¡]ÍeM™dÅ–‘Ò8ùN×@QÛ¢•f<"ÒÌ5³ös©D4¶(®âuÍ'~Mc2¬v=SÓ˜vyíúŒ§£³Ãݰy0Ü=п3ÁýN¯×týýóÁ2ˆY…ØÐeýí»ÆªS)¤òÚ<Љ¸íº¬ÙŠyÆë¡Išò}ž1Êl<Ã1ĵ"rm--HAo4å¦H¢¥É›®,ÏÒÔâ:í—›–¶šÍ:\NŒ‚~gÜ·Û$³È#vhÓ”_l ²‰šƒéÀoE4ZÿDÐŒ/4o¶=÷gØl[,>‡1ÁžôGBÉØ0Ìeã)›åí…àíéF!Ø’k!s= sZ‰ ô³6å>H=ͯ@@—Ü!±N ¤[| ä’ıÌÍ‘eô‘nŠä4#I›SaJI$ëŠE1JÄ£5±«ÊbÔkEÁ‘ü“Byò¸zŤ˜iWôhËæþˆÙ•ûµ®¥Æñ{ÌVcÆÓÝÝånÛ=žŽ îê6?)H~þæPï–ŽjocÒ`T¨&22?Ò¼m¤ú¦ ƒÇIî©ïfOâ'…È‹vguôz+%Äo!Gê=æ9þŠüÉuõ¶CjU„J_BãÐZ–µÃÌf@š^䕬1z¢µ|—ZÈ—¾¸Õmsø;6dlA+Ǥ±Ã``÷‰œÕ6n­oñsÜÖˆ/m¼1w­l/mb¾ÁLò·‚]6ަÑ7åóá6†{k”óáôbÃÂAoq·™&øcrã%M& >—“WµLýâóáE`Z\E«kRòN#"ÍÜâ“åØTë•4O#ƃoõ*’Þ¹jzõôÈPOW4ìqY- žÛq»r¨"çcO2'@NÙÐtVGÎ7[•3[´Öˆtñˆ™QSz1»#á“øÍ¹ ~ýcö8y·+éýÌ7pŒËŠ’hÏ‹ÊPy|>‚†-á9œŠäATc•½&¶,bÖòÊÁ £§¾bK>>*T:>ºhøAkÝD»5b°®J²ë¹J“iL·8n§Þœ6üÊâ™|ƒõ öm?k Ç:å‰yIvïUyrùëkEöз$½^mC{hˆ™±‚ì‰'bùƒ ÔQ­ñNŽÝàˆÃ" c–ÔÇÛC^éú§lAqXLÁç„͹ÃZýíÞ¿ K¦ð›éi“ð}æu2îË€V¬0n–øƒŒLL ÊNT[Ï›‚’¼ý2{’ýè¯(È\½t2È~ù<&=$QtÓ¤¹T¿Rn ¡ Xssô)© ;"onEQ4 k‹‘c™äÀŒ||€D à¿*_jüòÑÀyúêº$ï\ÃÜŽ3îÐóî ä7Â⠠˯‹×&öo§ìð‘?~¿AŸ#g &Ç`.v4“Õñ°€6…ŸIØÑ?ƒi’OZ*_È:°ê_Ñ"@¥ mGöˆ=–Zž¥E鑽&8ÖV3Pë<ÓÀpÔHOWç^£¾Ü“@ÛGÁN¸ ÆF|Á5²„©è R Ùž÷÷ŠŸ¿ Gï†î=cÉäØžnø›JÁߺTª®®ºš¿96¼§·ïÿ±ö€qçðΖëm¯ëzïw:ét:êY]–%Yr‘U,[–«Ü{ ˜ÞÁ›B'`ƒm¡¤ÑB /$äBÊ{/Ƀ'y `kõÏÌîN²MòþÿííÎîÌ|óÍ7_ÿ–5ûýÍËêë–5û¸u-͵µÍ-u<ý¹üyÊK&Èçã¯0¯oâŒvÄ^_`ŠCˆ„ëBmBÇTkR¬€Ê$BLY€^Fç—sªÜJ:ä§µ@ñ6%f‚€rA¨$Gf ù‹ã°¦ †i¢A‚¼a£ÆD‰s2¼|Ћx½^‡W£v³ˆÕ“iÌpÂÇT*¯+‚@ƒæ9Ò¦eßà…_©]Žx[Øl5„ âm¿ÍOL£×‘—L·¨¢[ü™D2h`!øˆÂ2ñzNþQÙTsÈ+÷ §hmà7ßã,4 q“!/¸Ûó-ÈÌ&B`á  …¥ôtÞ8S™¢ò¼1>½ÉÕ'mÍ¥yÎ8Qefd®K åÖø¢r­™Ù>1ö‹#Ï;L*wH«}M)«™ã+µ)இté{V»„X$CQ,RÉT,’ {×t~öhWÂ6ÄÛ’†G båð ïö ˆ¤1šð{ÖH…˜%ÝEb– ™Y?nÑÓŒ¹.à1*IÈòºd6Q­ÉNê®RJi»‰{Ÿ§‘ÝÔð'¸wÓÄç‚<˜†²–Á5<‡Æbr&ü¼ÿ¹©XDDY<ŽÁDæ=Ë/” …*ðêBF±KJÓZF°à‘o™— g¶+–ùª|Ë©FÈ*òl܈M§{±VòN¬•SÞÍPŒü¾X­PKlÚROyM¹Ç^5?c©LEÕa¥Sm49Lv½§¼¶ÜS’î©ÌŽÚèÏåR‰\bU³'6&-2Ö¤j6É”2­Dêu¾Ú¤§>a Ù ,#lgD¤¢+yßP»àáèàÝ>)r¼à9‡¶V¾„*(­¼¯…òžP`hÚSì¿i1ùo:‹‘ÈsýŸØä^‡yÃŽ«»Ï¯Òö¹$ŽÅ;w*TJ8)Séê¢6,j;˲íúµ¶#{'×ÑbI¿É€æpÉÇÌvHÁæñö•`Ÿ)l Ð+¤ ¤æˆ@¸’Ó£»Ëè*¾Íc<¤†¾‹˜hŠlY›²œ'7ºdb{c9âA™íÜ V—Z Æ8Ân´¸é¶hæq¾n†•pŒ,QzZŒ”’ÂðÒkñFE•$rC¼8ÅF]”›œð çºð6Ü®µÐ)B¬–WÐö†ÜÏl×(Ÿmªs5xN+Yõ· ò!ÃþÂDîYÈËQSÜ—ŠÓ™º„·zxŽm%‚j8vG³yÛ„“´ýðÀÆÊöüX DP𳈢`Û­:+í6‰TÒ¤Üß’'$ †‘‹Œ…½îþëÒ¹˜’2uRÛµÌv­î‰ÍÖ*Ç·|þ'ì–’ û“Z-„Úû>ŸÎ§{×a–0Ûç`#êsR½ JkSpSQ;´”:8’èÁ ÒQ<-,tA ÃJ~Ðb`\RcÂÇ÷?¦cÅsEЏX§üÆ7}\kwSºJlbÑWüC¢8¢7 )eŸqŸi7À=´F®ã×´Ÿ]Û!GSuFÉq¡hQµ<ÙƒyÈyX#6p*:eÄÈP3–,‘›Í‡—¿qLodê•RF)®c,êk)Æ¢}¼©•?áñëýÚ‡•,?Žx.ü‡Šæû7ä˜Bl+Ák}|ò ˆb9Pü(g.ÜE·»Ûà pš5°:lýPCy?€7íÿcýö»Ü˜RóüÀmL<àþÃÕr¯þÓ«ð˜ºè•¤›¹Š þôQà´dѱžzv6:± Ò„H),! <¨É—]:Ë\…K)Mðóu1£b!'L‚.‡$$ð)§x&΄–9@Do7éåï<çp¸‹E¤T£â¾k±0ÎgñÇÿ`>ÛFŒ°§›ȉ¢yüÿAÿZN¨­’QÈÉEKÅvõ£Æl—‰W*”b¥h¹XÎE¼RaæœA–·í «Á–]àå÷¤gü—É–Ïþøœ:ÊvÓJÓÍFÕÏ1¹Ä8.’3Æ5& šÇåôps¡DðA[eFiÐ …ŒP%ÂèHç-,`¥M)6øÊˆk°Ž¹Ï"—Ú³C­vä‚‚¾w#½‹¢˜m„eCAZ-s!PFÓñÑ1 ,V1@P“L{€Ãd,f½ŽU+d"†°;“©'3)³@<šWðÉ„®D#c®uVvƼµé¸ÖëQvf:£¾ºŠ”h¬úªÁœ×l3»™áœÏì0£ø æMªTôäÊr ÑSŽ]]´Q~(ðȆ’žŠŠ4ÆÒdpêe÷X+æ”—Ï®)/18u²¯YÓsÊS5e¢M¬Å©.j‹8ü-‚kÊù/Ñ+ sŠúƒÒÿôþ*ùîn騹 ôûJg¹¿³úû¥ v1ï»—7[£um¦~Y‹~d+]CÆ™7±.¢NQdw&FË\Pfa£31es>í `¤Ò È“—2z!ünà­¾ÚžX¬·Îç«ëÅzj}G¢<îDèLÛíéÎD¼£Âf«èˆû (‡ƒ¸šù6éÇñØÜ9Ç¥Fð/ôµ»jv(Ò™u»³‘Ðì*÷®h$"QѦHk¹ÕZÞ 7AÔM6…}¥¥¾P"÷éõt ù_â Þ§ðšðß1Ë ƒùñ¸åÍÖ†qdT¿ÔàÜnwÄöÃ1úÅÎ(o#¸‡y€2I$x^Ññ;.—+êÂßñ!‹ÅWÌ0üéN¥îq¦›ýÁÖ´Ó™n ú›ÓÎMá 7õä’û™ÉkîŒÂmQwhVÂbIÌ â%%ñ†€'ó¢Qpû›jéKJ5²OÃõø½’ú1æ7 D Ïq¨y…ü†Ñ„¯ááo(UŽ•ÉBféÕJ̉ˆŠ9·8?jrë÷Y¿}õ.U( ÎÿMçµïz\ÊmzÈ èÕAlÊ+t*1ÖM ó‚g¥B3š ]¢sܳN¦ýªž!7ôäY¯}ç•êT˜Œœ×GìGNêÓ!Ño~¯×þ›ÅöŸZí¿™Q©K¢·2_‡=j±b“×ÿÍ!$Q3i+¸ufß‹TåÄó.8"š„ßD´O(pÜÿð‘’HÖe¯ ›ÍáJ»+)­rdЯŒÃ™‰˜ÍTá&îRIøˆâ¯+ oFšp§uêÀl)æ±(j )•Pym‚–禦=ÉñM:ÏÊæðÑ “£ð§P ”i* :%.ŽWþ®\}CÉ…ÅOOøĵÜÓ¤”PÀ9 éJåNbªTDΊ‘Â/¡’¤¦ùâ²TžU›JQª„Q/fÐXé2¦‚ŸøqÞwonס)>!Œ¥jMD1幇Çz wì›|œP•¼£àw ìµðòZçžü]RT$S¨P¶QSh ü68²ù*VBÒj«Qv –-+”"‰£¬­ÌƤÈ!¡WƒSÌ.”ý×uËó®`xwj0juSf¡gðÁ ¼º8øáúûàë,z°.±ƒI âA±ñ†ää]ÊX̃"×x7åà¹ÏÉWéUçŽÂöˆNÀU¢ûáµ b³#gÕ`–û8KyŸð ½~#ƒ½Ñ€ÑÜ~7Å{ó˜U°‡ëÜ÷àJºßWÙTJ»j¶§;@Ï=wªcnçÛäOÜ‹+3Ã^žž&'ÿÂT3 Ø>Eç%š2C€‚¦±IT?•õŠlF «Az,ÈLøgrB…0{þØçÁ¹Zª~øÑÏn¹å³Ç/~ ý}t˜û[Åèu‹]?ZQ1zý¢E×VPä;«nýâÌÊ•g¾¸õ–/N¯Zuæó[ÆÙÞаý‘ñµn«¯ßö(‚+R^¾ a£E~›¼P„ ø !6æÂ~›È+‘‡¢_þ7"ŸÐ8”r«æKIÂý øÃ§n ¥UVŽ…ÏŸ5àW)"ò|%†ÒZ&WOäX3€Ñ@„ …8¦ ¯§’l¦!šÊeR -¦Å“@Æ! ¤ŽPTFÿ"~‹úÁùµÔç×¾ ¾äDLòç??÷ÈÏN–ÿZ°“ÁùÜ û£ù‚²¯Ø‡¦1Íaä ÿ¡ïå>Ÿøîs:"` Bp¼è*€ò§:Eê _.ű„0JE±7£ÛÂFF\™|¥Ùðá:€’EÞȨ,ºXœš¾,xóüMV=ánQ²Ôfn^å¼°¿Ôú¹h>¥´8ƒVîS_P¡xc™½ÊÞ¿UôßHLiÏG¿Íˆñ,3yT”bî‚tÇG”¢XxHô¤8xH ⪫M™^¤’%›cQ3k`Í,kÖÈÌQ8"¼ Þt‚œD· [Où!º™XE¼í³‡þì¶Ïoÿ^<òémŸƒÃ™•7/ZtóÊ̹ÉÌÊ›-ºie†þ¹øþŸíÞóÁý#àÄ 0rÿ{vÿìþÅäôÜÆëÉógAýø½½7Ž×JMÖß ìiÚÓ~$Áኹ…!š”PxsâÚ!€p9v›Õ¨W*b ·3)/²x”n8Þ4[ÜÝÑtào8H" oïÁ¥@¼ôÊ^_"r¢ûèïî侸ó7G»˜d¹ŸÛ=g[o\DyÎÄ$z·v«üåœwÛw¯ís?µâ¾k_Þ~…r`Ó÷r ‘ÈEõˆÙEG‰3 {™7¹")ââg"a´›‹sdàq"=-RBóÐγKb$-cOʘëŒVîì¼r¬:Ú¹¬Š‹‡JŸIŒ–vïJ-yä÷WþÝ#KSÃû»K—–>SæJAH:gëóÇn]ZZºôÖ±ùwn#å~žð–‰«¯täàÇ ?ðñÁ‘G¯_íËû˜6Mž¥Ÿ þF„‰zâG‚"ÓDdÐ"ÞJ°r »TÑÝü F¸,èm \*Áu¨ §ª'æq!· Bº™­ ¤žQ«g6ÄNDùvÈ%(Aát‘úh=²ÆëX©˜ƒ°‡@§{e„|Æhjðùî‡4´$¨ü²ÐªÎËÝž¹ û#ýw¬ohÝýðè’;Rj‘Û|o’‘ áq¶1\=ñŸ‘ÙÕ <â*gùýM!OvŽlNäKÙæý§·ì|õ†®¦Ê¬ÉÈQ‰¡¨»Ì©cfÕ³VÏþcIÂkð÷îš·¹Õ‰`%w†v0‡ +‘AÞËZÏÊ즒Ÿú$ _{Lð†ÂÎ4¼+•¡©ùgmÍù¶¨¨rö¤‡¦šÏl„@k·!ÐÆ"^·-cÏÈ¥4EXƒ6ȧþà±V$æiGÞƒe.ž2gFAšÜpë —^÷àæñ»Fã#‹g­hñÕo¾ïÛ£Ëg'[¢zW_uÃ`µ-ÜØ©Q÷sè×w|÷ꎺ±+f­¸Ç¯kÞœ[pïöVjm÷ØŠaß¡‘‰»Ë¢‘¶¥•­ýIöþŒí€8¬e®†pŒBH^““—!_V›`TWó(G“4t*%˜&xÄLü“V8bÇ–opäUL=F&½x,–‰gL¡PLÄ{µó°áñPË[~!­ÍCG¬Cy !fBÈQ?Y,€.d(•Ï|h*z¨¼ô›—x ©$ä¼ÛEÔd¡7ÁDA!_=…·«Š³ Mj •7ÌŽºåÖRÿà5ËêÕödÐ&GÌêç¬ØZ˽ ì}ô¾Æë†u‹jøá?Ü8ëú¯mˆ¹Wÿöw-ãºø:“£if?‘$š‰áœ< ¹J7É»íñ¹£‘?7A#³¥P»`Z.Gká9…|ª¦¼ HQ(Õ”z=ŒöI±ãYÖ›(t¶"Æ]ð>;Ä8Ú–ùCÁl²bþºMëæWÌÚûÌ–ÍÏìküÚ}Á–”=6xõ°ÉØhèÃüD¤§Îg­œW•š_ë¦_tYë-±–¥ªÛ·Þ:¼âѽ--{_qôW 抆9ÉìHS@$WKϯlZÓ×hÓVÍ]ßV½²+îoYg2òoÔõÔY"KtäZ³ðÜA>Ç D¢#2ÈN¥€²?Cb_!U]>òÁ jΤ^ŸÛçõHŒÓ}x!­½¸3ï@Œ&ªJU²þtgåàКÑGݸ|ÉPmíÚŽÈØêU‰…÷îhžªòi*×_³ë‰ZJgTêÍúšdªîÁ†lu—Ím3$ëýªPצŽÞ-.]y¶Ö‘YÑoÉ`^°Œ{ƒ<Ïl'ªˆêœ,ŽõÖf@4ðgÖ%åÁ¨tº*S^JBFÈdŸáhg‹ƒüÁ˜&ÄùAVü3%²”ú]Y¯¹¼§ª{eyÁÅ•TákHÍÝѪßtïp°/>n•ÈÉ=ŒJégžyKÆ·ík¬ì®Žëlu-ݱƒ×v*½~?˺Ý­µuɾ¾5?xñ…Q‰Tc¶«v 9 ¹—¨Ïẟj€– ®F¤äS1› ñr,Ñìó{u^,cÕ©DÁT’eL٩Ϲö–ŠáW–HªIoû6GÃÑe]Û{#°»çŒ”E'ÊÈgj—kàרÚЃr”czŒxŠH¡„ùŠ›òi¼!O2Ô€L¥¦§%ç‰ðW·ÃI®"ä+ yH…ÆÅ P‚ˉ\ü>gض”èïˆÄ{ ­AOb$<]Ö!¥¾ÛAazìNPdÙ>­ù†Ïhk;ðìÆôÂ:?͕о†þªÊ ~øÈ_7¹zè÷÷§Nr=5<| (O>d§†Î%RëÛ±ã±u)Z“\yߦM÷­LÂÝ£„p¹™9q°…8ÍC¥Œ Ä„˜ÂŽ1Ѩ€›PH Õ1Ïr2äsc0…þï/b¸•^ðŽˆŠEÒ¡K¼©ln4h¦ ?ð 9A/Š¢²M<ð ¢Oá JÇor¤D‰°)…åÄe‰è›C‘zªv÷·÷-½yY™)ÔnUûË4øæv¥æ×yà¾äþí½¬ý±_Ež°ÍZÛ-wÆ«ý‰rêrWY²âAî¯/¯ªÙx|ìádÆ:{㺞‡×諸üÇ·YÊÌô7ŽÕ¬èˆ¼ûîÕž27KVö×{*ñþ¸|òÉJˆtżïµÚÓo¿Wƒía,¦VyŠ}9Kx2-žºÅõ.꓉Ùu¹9ÖMïYßÀï;%¤ÂõL»Ï„ø°>~q¬Ò‰Ü蕯ÄÏg5ˆ\¼ ïlƒš úeù§P¢À™ƒ±¿ Îãf@ç癄?ì÷£°#H|ŠNNÈ]LÍaºœ¾¡]j´mmJWeÈ7wv½TˆuÖ€¥RIP¸wv½aX*£D*É]ýß©½Ÿ:üFÉb§D¥‰ÌΞo6yMŠ1«['Ö&zêAƒ)¬×‡ \\‹ý]vp/ѨNNˆ¸šwÂÁ²%€$œ¨’¦nÐ$ö4ÓãíŽbèHjµX”£àДŠõåÑuªI0„f|!† }“ŠýKŠ×g”Gb¸Û:g·.µs™²vC¸Úê¨/U&÷ôTô¹dâË+«áÂ{áÊžvrÝD‡@ëL¥meÝ -–ÈÂÀ¯+Sôó8î‡àüM(&UÏŠ$§• 2F·•š¡,]Þ©KÄ줨‡Ìó6ìo9ÆÝóS›_|“Ê(õ¦Cv¹8TB}²‡ûwþo+ø`ØHO¨õ<ÒDþSDŽ{òÀs­•Ø™“W¡X„FŸ—$…ƒM‡}Éþ|„=å³WB m4  xž“$Öè"~mý((,EÐržÑikKcΨ@„,<÷p¤ >í)¢™G¤PÙ ;*¹)kói‡29ïÞÍËï©™_i±7Žwg{ÊŒñø+R­Z&·Ä‚ЭbøsW¼òb*Gï ¶‰•z«Û(‰d‡öIJ†ûˆ6úS³zJ5>§^﬊ÙE?Tš½&ê£í®„㪗u‡•Ë NƒüÄý±ÅÏè‹§·6[…Vùhýõß?®vYX$'¬†t @^JOQ´…P´’æ¼ iÁy5 \öB·0 S„%q„ÃÍÂÃ3!¯.äcfšÇP–ÖM‰ËaV×^öØÆ‘G÷Ï®Yt¤óòÇW•'{2N{eg<2»ÒE–­|p[®åÐw¶£ÂÉ8·‰\TYš/Jàj|£Óßè£~M”@º8[ðÀ)á£~ª†ÊEŸàÒ)°ýn'.D±PìÈrã_X]˜çô+©žTMnûC+W<¼³±óÈ÷w¹Î®åNIuNcðš}‹G—-¦\•Ç×®;sålTÞoçK[vÉK *îÍä輜æ¾î¼ã^ì«Æ½A5àš(Þœ+‰'ˆoçËnñ;ùœ'b^ÅNIù} û.L0yž„6ÃO_ÙÖ\ G¾K5@¯có‹É± bÑHØír:PÕd`q¥AÁ*04ÆF0PÍÛ^½yîÜ›_ÝöÙö×ÐÅkÛ¹OBãã!*š=>kÖøì³ÛC¶_ùÂÖm/^Õ€T:¿¸}ë Wvpøüž®]ób€¼Äçíìš³s~ŒœØŠ®á~šàž ŽPÿ±äHNê³I‘x‚0Å,rÑ$ WcöXÉ+VðåS˜¿¼X$ #Ʊ„ç/2Èã&l‚uÍùLùfÏú g¥¦0ï]+Ät˜tBÙ< ›Æ…ƒc´B²_êvœ¡k&GØæaCÁŠ…mUÆm"vŸÔåx–ûÅ­RÄ‹û¤¿5Ñ×^[ò5k™Ž{Zg%O{À$ÒyKÔúx{šûYuÌÖÚ&æq7 –|¹Õ­kâsgñù¦!n} âV)Ñž“&V¤´Êûj_­Âö/uA¬å¢GX‚í͈$”¥Ak)& ßRD œ`…"gFKlo¨X5 C‰òY¹V!žË+›¸^ß_{Rm3(o¸ŠÙ~®UíÙ½¨kÝüöÈiøá¾÷:ÈÈ»ö?¹|©µ»ÕÜŠC7è3xN“g™½¶5£§#c¢Bõq¾‰ ôü|¥«¯xŒ²"7úL: ìV•B,"šA³DõãBš)Ã%©àTIªÊ š½˜Wã_ûÊJNåLzmÙ¸ÝîuR*‘Ñ©|®$l×T,»apí áÈõ«o«¨K=«t%åôÙí‰ì™`Ë’ìo?«nôÛ:¬Èì\Ó­tDJ™{öÖlùþ = ›û{nøþ–ç6(J"e÷š™:l6_Ðݤˆ9%ù ^öT佡 ìÆb_µ¬àÂŽš-¼»­ˆ¹)MÔŠ¤1õEÅ×¼Õzº¬Žy¹ÿ„4ê!z!uœy ¶³ âà4)¢ò_oâPóPÂ*=VL EªÑcáf“¨x,¼Üa@ÁRp ŠÚ V¸V=ð7 ¦¢GEü!‘‚‰†ÝN“A)ǃk‹,¼ÿŸy >žÆ³KµÎ°ÉvhµôשøƒÖ1™BèNÈdŠ8µôB7™‚–uMf¾¹yÆo ûUÔð(ó<¡!ü§ÓmÀ^gòN·§x·ága[ ¡ÆêCRL€L=BôICËçšh¹-®U:œ^æù‡¸·•©Âª@[[Wl‡ÝÇ×Ú]5i‚-(Þ*„ÓŸĹçCN«¤a§¸€6®†´`_ËmÏÕLöjG–J .À ˆ &ð¼òrö,ËÚYDÜtbºÙsêfÝ(TàLáÉ~ï W<³¹¢a×S›¸:jüYß—Ôë“}õôJî±ê±«:—ܳ¡†INè¹k©æ@´µÌŠá´öÛö/ùk´qîsò7 ©1AÍ-$ÄD=/§ (WS#3?ƒ, ¸àŒû§;?ß›¢fs>؇åG|' ÀóÊzîU¦ öi"œD}®œaz‘ÁQÑ$B>E¼OÍ£PšÍEvš;ŠkA¥”Y\9;Å"q†uc©†MÕÓÚŒIDÂSÃÍÒÏ$|÷)á?~È‹œ&æ2O¼Ê Þf’çŽrmî0¨þ³;LRªÚõŸÍï¿ñ"÷‹OŽcþòáö9 Ç™E£L›I®k $@ÄÑMʰ¹\*&q h@”•FB¨ª´Ã¦×*‘Í\DdAV®ŽêxíJ-:Uïê½ _‘†˜?íêQÄ øc°Iÿ¼>nüÛ £Tû}cðmî¼9í·»ýdÕÜ’çô)ßæ¿mò¥ gJúªN‘«=¨0s瘤ßË- •Á‰_”Áqo€££Y¶9;ñvYÜvM¼H6¹ÃܲHY–mÖj²Qpãmò¯ÔÃL=Qϵ– Àˆ),™$ÕHˆš!ËP%ãi-Û)ôŦ>Ÿ×çxƒ1‰i†}nªÔA%Æwq0U°h¢hþ)u¤³7ÜRf­ßñÄÆ†}[—{¤NÖœnm^¾Á•[ö¸g|ÍØhÕ¼†„. olûÑ®l©£~3r4i¬i—<¾­©$1+´O­õÇýËÖF[’%à§ý}íý6-°·qvjx?žïNÁljr†ÙcZg´¶$ï\ÀÆXì.ÊUB¼†§\; ¢ÞAôÃÅ¢®’šíÝ«ÜçŒDÓs³¤f‡G÷ ˆ$º˜žþXm7©¾|‡ &×ÔT º'Dj»YÅ$ÏX¶¶6Íû/áýGïƒãp ºÞÊi5„ì°#(XÖò»Uá+ Q” +¥ºu)ß'܇†jo¦Ó Ø?{ËŒoéË{¹Ï„’–xÁCŽàÄgÞ¸JÓR7ñ^О X'^Ä$ŽÉ0ùWú(Þ«sx&Ï„s9¡“]ÑDðAXBšckñœØX¨ój( HóX¤ÁñÁmJyyïŒzJ«£rŸsï+â =KÖ×¶Ô6[««ÓQ§ˆI~ù½êsîΉ㋜%¦oêgßrlRž–Àñ‰ýp|ÈC­Ê³¡dÆiB\ðÁQU‚) ЯÈ%› J†/±.Ê€K {½ÞRoÂc„c¹¥˜lϘ„»dJÁ@}mÏëÊë¶>²–K^dfTìõlgL«uVSš~]¯‡× éc]-;Ò$y”LìlnÞ1˜&'V¡kD7A>Æ ×R…8? Q¦…+ñ5NìÎÒÈ$¡"T¸>äüRÂFx—M¥ø H‹û!¾á Ï›`[Ï«ô͸ˆ/h·b†’èÃ'–è«ØÊæu»…mz3·®ÔŒ[7¥HþŒ>ÉwÀÓöA¸önøm[ÂlC¡ÈƼ×ŸŠ—7©hõÂHˆÙ*€„Qxî9@evâæ¾X~ú؃e@{üJ &e³¼|ìîqp†ëèXž¹ÉõyQÑ °?9òfvŒ?Òˆ>"¿{X–ŸÇ­½ðÅiŸ£ðß{€$8@¬ç^ç°m*—Dé9ààÏŠû6FÉÿÀUXZ-¡ÅK€ÓꧦúÈ/ÄzØÉµü¿'¼÷MuVXtæLžƒp›¼ÏÃ^Þ¿ùR›Â<‹¦ ͈/:Ç Ä¸Ö¾É¿Ðx^$‰Þ\·ТäÀ ®24J¡¸{P“¨š[>Xp—Ñ7å]h bÈ¢£zØÖ£%ÖŠQEl)ÏžóŽbwÚÍ;)'R,Ä'œTd%åÙ¦p݆E5²‰?“*yÕ¼õ Í ³!ƒ¾D²¨õ­«t|QïÝ?¿æÎ_¶¯VƒF¹Äê3IËFŽ |oÞÁÁRWÀ¥•ïî^ºüÔwÜþÅéã+7iøùÅàüªòóÓ!«-6Ú‹4Ês‡æ'B„åo@å>ò¦G•€òþ €ˆ„|^U@ÂYïøùr猬wâ)ݽØmp “¯ÔãFõêŽ_Þ}äßö |ýã«®y·u‘Ô¤7„kú[fm\•“Ɖ?Èjmjˆ´TWX¤Шټb|Åé/n¿ã‹SË—ví‘káKÎûÞÀ‘‘2©Ég•à\›»'ÿ"ŽÁù! ÇmBZHĈjÏ€E¬‰ …&óÁÞ¾B;\ÂG˜î̶¹Ð%šaë5ß–ÈÅ46kÀgMØ,«ù4RÄJ×ìFæÌAÛŠæéào•£ÇÞ?pão¿¾pá×{ãþŸUýsÒM}•–.:Ð×· ‘ØO~yÅ¿ÝÓ¿êô· ÆbÁÝï_>¤Î{fí||íøã;gÁ¿ãká_HOàaH#¯}3á'Ês¥*”µJV¼ÅXˆX€ *¢fJ”ŒÁãrÚQî¨ß-ÖFqq–b±‘·aý1”WÐØ)ç¶@ÎñÃ’Þì«@ekŽ_s aÇ“Áë«u¥së¿÷½ú¹¥:ÒEúÓÜ]‘2ðN¢A:³kÁÍkª¡Èòx´­Ü vÙSÍOY8Ö_bþŽånEž™Èqˆ/i¢à™~†TàŠ¸ 7f+ݸ’‰aŠüaÖ2Ïe"X§©[¹çÞÜóÄú²ºË]רX='Ʋ±9Õor_r?a’osOåéC»œx}›ß?ÚÉIú?ภD2‡DM„< àþ(¥Ð:9±Q¾Àªi!aÖê5bc”]¾€w[¬n4dfͽ÷„½¿L}{øö³/àÞöåIÆ=qöû6÷˜_ÑýìqžVB‰‚þöo„\y]®šAkˆÒAʪg†XÆKÙ*“ JD^Jmf$ŒJ,+7 ÁЍ§7JQP2 ¶p7QÑÉÉitôÔËßÊŠzñåSÓH)ItOž¥ÅÌ!¸.1âj” ©xí€Ì ZG”l˜ÏÛ$Õ8kS‘;”í+Û`Ï'‹ð:`z5D¯‡ ÂAÕcp…‚è¬PQâbR+8ðfü&#\FA3Àêä†yÛ»téGî϶ì=1¾æÄåméݯßzÔŒ;Â}gýêUë~±†>"ªZzhnûpëO¯ßtêŠæ¶cçNݘWvÉ-‡"ÕäyõÑ;û÷»ï#ŠüU·!¢–˜s&á §‰¼ÙK5e„F³ÔÓ8ÑI ÝÆRn6?,M1l”/¸¤w*˜á™z*âåÆS±ìðö½Û‡³­ÎlÜrrgý-‡Ãí•β¡Ãó6Ò:œBœ–̺Do‡Ùþ ¿ßi±•&ÊæŽ_»hÝ“{šZö>±êŽWk å sS™e³£ç6Žufͺª9ËsµkºÞ¶qxHç'wCŠ\•KË‘2R¸^$¯ÐÉ[QK5¤<&S"æó˜Ü&—ÅlԻ̨ηö Ú·Î×ê§ö÷~pëÒë‡bl¤#[>¯Æ=kÝs¸‘³':¨|u¥'îñT‡M_mìênQ…«µ!¥–ªæ$»6¶yÀ#çZjKânÝ1·Ìaˆ¸ô‚ž“|ŽŸÁùΑÎY›Ô‚&Þ`†e‘c _áŽsÔ'ß9ÿ!õ _k¾òý”ãZyk ¦);Õe§=™Òvæð–1!†„[npч4ùwn ßâ¡Î>…øeäÎзb¥úNõ¿¬ït_ ïd‹ôpÿ¸Yê7'B®ã½ßý^gmeiÈ!}[jõFÍ` õÉù¹VoÌæ&¬10ôµëOæÌ÷v~ïîËÂ;Çu˜_Â1Ö ú™Ò“ NóOˆ!gK0CRÞ”-‘ð0¥›³™ é Ü,vTœ©Ù¤xe”îBÍ&…²…`Í&……Éßj"Š¿—êëÙ¬÷õ·LIOiéí“­ÆmêŒwÖßgy+5[å6¹çÉ·L5¥ž¤é­×½Y–¼ÆiwpkíN`tú¸[ƒI•:ãV†c@éõpàaû_£Û ÷IJjU26x‘£aŸ-§T¤Í>´}øæ± Ö_ëmÚxcgdtl4Ü¡µ›Â©ÖÊ WlÚ4¸d^oyK™W£-QQÛ2-±ÎÑ []¶œunèØÜV’#&›ÛÖU]•{°.•¬Ñ›õJ#‚ÓžÉI Õð#ߊQQå£ãw?ácKò›0ZŠ+3õT¦‡ ?çÊŒH QS„¸Í=CLÀšåîS•ÚïÅ¿IWþ7 >™xÏ”±&½6!-Rz hºb4zé6ÑŒ¼^‚ðÆ½±€väAúQ7O¤Š¸—3Á‰Nyõ(q|ÅõýÁ²‘«û¹e?U”Îê\V–-Ï:j³©¨KJ>p_k«ÆSé‡ð¾£|á¦ú¶m J/2݉O¸¿t×¢$%o¯„t¬Œú DÍ`Îga!” œü×›Ž!¦ FN½E…7x¬Å)PÞ%¸¢ õù Ö¦TØØ?êR–÷~/•pÿ&˜¬f¿œ{G"ûýO-)í“NG³×Ûì×{ ç¤Ö?šø½Ù¬' ¡ÑÀÄgÔoL.n+É^ˆÃŸÀµ‘#*  ÒjÈrò.¬r‚×Éâä°^|º •„wÇ5hõÁÓ\7¼q=ØŠÖü”Ð㙾‚·;PñX IŠ rHЍ7® æÜ+CU$=Ø¿¶¸H«œóªøÜçyUÄ­"^uš6C¡®°ÂÔù§Æo §Ænâz¹pÔWp'æöhƒµÑâEšEñZRD=äUåVë Ôè&V_2¶Á4=¶áŸ4ÁTØb³¢ §Ãê¶¹Í&½Å`)Z˜™=5ãv”<Ôd`ý3W´¶^ñÌú|ØÂò]*.¨Úµjl' þÝAm;üâ¶/no?üÒŽm/n;ÿüåðäõ;¶oß!Ø›¨·à:‰‘Îëi0}틸 ~å©·8î!oø@‡‘Áïèñw,8# ­© xBºhº>"£dP”á¾ä™ ö©‡,: >#lDëi^õËPôy?qžà‚Áœ¿ˆ©G§uþxACœ×TSy1äá½Ê÷ì~ñCڙνJW^×ñDÍR{ RÉýý?xWëò%ÉÅËÁ†úî˜F"WÒ×¹bØ]-w†‰bÿ-%æ¤Á€Ã^ägca÷,ÞïjšûòŽ˜º)*vÃÂÉ—óÞWâKy_M¬ 4ÉÑ-/\ÝÙyõ [>Ûüâ5×¼¸™›¸q߾ɿú[–Õ£,:Ìvn—¸ëÊÓ7Ÿ¹²[ÌýÔÁ_ÏnÞxæP—\}~Ï-+Èk”ÝÒ¾e^™hb§¨lÞ^vÝ ùC¸ ÒÃ’Åô3ÕȬWÐêRXk™g·r`÷­ ë¾óg¯‡p„ç÷ð÷wqg¨Ÿâï{r΂GÕ‡?Mÿ²`OwïâÀǽ 9KîUáÓÎIüe¥%‚Ú‚c=à¸Õ3õÇÃ4Ï úc½é!« F#Çúãµ…ÛûÎ] 8÷ìý>8E ×]ùÔ_Uwxh-xçsƒ4ý!L/!¯õ¼Åb¦ôÆù±£·qÀÆ+÷åÛyˆ ¡cxláΈjà÷´D<¾'è‰ ÔÆ,¯1ÖHLÅŸÏ~ ´èÿ‡\÷sõ4’Á}–…ýiëEõÅêúb+kÉë‹uê‹yÒ*"K\uã¢PùÈu‹¸ÕÜÏÉ’‡fw°Þªà´ÓÒËÝW ¨±ÂÑñ@K؉ê\ö"¥- Å^,†)P}©uuväRÕZµ–”»2öB²?5.°ö+o–/9²ˆ[K?ÎM4³½ƒõeÓÆ…èØ'EÄž$Ž@\z­ä]³ú]s‰Ú|)]óÔévgãÝK"é•·p½g_ê_ ‹4&¦c:ïà²A òüúÝ/_Ù‚¸þ…¦gÉå;ߦ7é5jžÕÀù¯ÊÏ? ·C ÑSó¿xˆêÀECT­ÿ—öfš(€!€æ?ÔŠµÓàâµi('ò,÷Æëâ#›X°—; Ù¶¨"÷™lÚ–ÐðØÊ²þcÛZj·?¹eý7gQµÎtÛøã;.‡37-L6Ô-W¾¼{ýóGº;šy=ЮɳÌëpþ"@¼ÂÏÚ=MG-aHtôIg(©]ÿ¼¡™œ*-4äy뼞zfã\øRí°>[š×g›qVæœÁfEjakÀ€Ã· ­¶ .¸@­ wkq:ø§³·[2þ䞦Ö}ßZ¹ì¾míò§9j׺åË×­]¾î—ÍKïY—Íó45¿¾œ…ø½û»ýËÃNG{Ü¿v"DDrA¸sx6âm‡ò%I ,ŽÛ »ÅFz ˆÍjìL£+hÆÜ¹èóªë+Îzì‹Nv³ÒæOzžú4ÙѲ¤Ö[z×Z°ebraû–]Ñj¯\+×[42¸Œr0ÜbQV¶õ3kúʹwgSI¨œ“Ÿr¯Ñzæ” jsYVÚ ©٨ב¢¬ë h´€4ÁˆhfˆOI€s¡ø}^ÓŽOH‰>J¥Ò)¬£åë ¾!˜O:‚|½ºc(ãÞ®¾n乓®šùé7£Mq“µ¼9T;^Ê£ß|÷Ý—ÿ»2÷_/ÿÑ+/-êšÿÍÇO/o[Vg­ˆOÜL+ ð¤Môǘ—oÈÕÂõ&Dã]2_σáòå".ý«4ÊEtòSà>7ãpdæ–[b 0û€n%Ä8ภ]<$2‘ýk¾c²iD‡Mãp¹ ‡Ejý¢Ä°‡Œ¾lÈ`e}Æ=`Ä9Uod^£ô¢`Ÿá\àR“þÿ6cQ‹Ò]‹[Ê;Ë-ø?ñX•[ ?|zò/à?à:Háîk™Ò[óõ`„Üò„íªÇa ¶ÝÆ ±Å¬QÁOHõ¸&Œ^$ö">š‘a¼¦U?¡ÖúLá*—« e™ÃMÌö‰Ÿ/zldä±EdèœÎY2›CUNWUÈd UÁ1ŽMþ…êg®‚{/K,;MÙscؼÙ-’Bql(x`eÞ'…ÀYí N¥2£KF 5Â¥Í"6â dVo0POtgÁ™IÇ<=‹×Ö/¼~y&’›ÓTΘ¿¥ºšâÕ³Ó>G·aæléÏíAciÿž9³Ç{jÊËœ×K4gÓuÝéÆ.³ÍìS¸š× ä5Ù²ÒPÀïµyPøX‘@`@j× +P+¬›¹Â»––ÍÙ?\QÙ7º°A¤?f¯2=ûÇ}ŒZâ™×žÔº“NcDb³0]€c u`ÎæÎ¹ë:Ó©ÚØ­ÍG;ÉÙ5œ[× UUÔÆJâƒDo8÷ì…°ÄvîOàëÀE8QÞC5 ù|Šå³Y}¤&š)Ž- òõ¥ê)xbl7Óî`Ö¯³gæUÆe…DîtZÄÎ ¿þ¨­Üg°–ÖyJW¯ ‡¤¬B¢—(ÜÑr;0…ÒÈwâ ì÷õ*Îíy@Þ=Ê¢RMHɇˆæg4p(¼TlÈ«ìøªªóòæ ÝÈIËJƒ~»ÍO[£ØGãÜ£ù~á~ªe|?2ØOÍ%»0\´ dkšÊø‹e<øýÇ ü¦å¢„ÍpÊÈÌŒ%Þ5séŽ\¸$pÌð›Lü¦Gö«]xÄ(‹ ¨?Àyfª©áoK΄%(”û“ä{Dÿó‹\|ñ먽àó_PF=à Õ%‘YöT¤ýùUÒcèœk`äz?$,Z…ÚÛýèwÖ&•…±klsÖ“âo V‚‡˜÷¦¾p\Y0P‰Ýüá0ÿ‚1ïu?òMô± üزÍÕècpnp\ä?à¸ÐzÀk8»Z<«ZÐû’œ6ëD€ ókû%?…ý¢¶ð¶­Ãmë.Ö–û.xÄø¶|+‚87G,`ª ùÙTSܱ¶ú" Ž»H=uÏ™%üÒ:“§-Ge¢Œ"58öù|—œ= ÑHµêý¬Ì5|áûƒªJûÇJ½DmýØžVü¾¿å+þ0Pp#¡Äá™@)T6ÒWø«>€>F–¸8ø±4ü˜A¢‚«Táศø-4x aÒˆgÛZ0L|-•‡ ì—Z#´ýnÛ„Û6Íl y´Ï& ®„<š”Çc>ò²^ÂcÁÇ¡|¢›^K>Â6'Ö‚ÒRæþh6»’;Mv09¦…43¬c¤X/¸ò•W˜>ÿž÷r/‘£¸­åÈ&p 70/:ÀRyýzÏ€Þ½÷Uø?æƒógÏ?‹÷ÜäïàX·ÆŠJ`7¿f¯ öǸµ­K4³äýãá.s,ýrIi)¿gëÀGd%ùô?És]¾àÄäÓBÝ™ÛÁŸ¨ÕäñòÎíäû1òø3ü;Ï€?‰®û§ï<ÃüüËPá‚û u~ò*øNš·ú+òÕÃpÀ_¾DqM1>ØI(+ÆÜiêüyz÷7¿‰¿×IÜ@ >è“©dèÓ̹³yûñÁÁã;š›w ¿Û›ÉZô_twàØ¶ææmÇð7çƒà]ò¼+·{ÅgO½è‡ç9R­ÁPk…ÃQÑ ¶¦d}¨%åp¤ZB(õ“=Õ‚¾»ûigþ y9Ñ3:)Ú'z-aUƒB@+)B2–,”ÓR­lsûŠœÝž[ѾY¦•Òò…KÈůp68ͬ¸yÃG¿ûÃîÝüíGšÅ¬Ùix˜;ÀÃxö¦õSAylÈû¶de¦R ü¸V¾¹mEÎáÈ­hÛ,góý€+ ýüö»wÿáw…~ÀÎpï‘aˆëJdë.çëbtÂè^ÈV2užC=ù‡$*Ü&Çz<%”×T]+nSn´ƒÛÁûå—ÛÜ{bfÌø²1`YÎûǾG2¸ßz¾_`ˆ)DG±y½²pUpž¡rq§YÛ÷  g™äóX°îM|oÈþû#K€y)`ûüêo!ì¯ö×L ž¾”I9ÎÿB"^ åÖ@o¢À1á”'® Ÿ2 Ý#´¡”‡·¶:]žŽ²ZT< ò‘i^râSJðÎL|¬"Oœ•BЀóM_K×Ö¦•jEtðÚ‘”B£‹›å ¹>XåóUõð²¹Q,Öi©‘k£ µ\SÝq[Gu|`p¨´swr“J§õµ§—-Šû3a™<”iôÅ ¦Û{E ½jSrÑžÎÄÐÀ¿î <Æ1<†O;‘ʱ )¥çH0€uñ~Ä;ÛuáÓéY¨!“ÙÔ8+—N%¢é0b2õ. )àâ<¶_ ©%ø¦(qÁ#_ÀYÀ!jj’)eú`Æ%O=¼ljÀá†À®‰ %:÷,‚ÐÐ+D½íéQ!¹,ÌCciº½O¤ÐAhôïî,ˆ#0⺆n*Ë|›('fçt¥±hÐëqXR*¡U¼EÁ7ÐuvM ê”Á_åD¹¥ÊHãp 5@AMèEÑî&\GÖ€Cƒr·£\´U  ¾ËˆävöÎÚ;YŸ6àFr1³ Þu°wÔÞ¡õjµ>öŽš;ý&%å~RïgUYÿŠE2%2._$3ªXŸþ)üÀˆ¨)J%ƒOôWø_wˆ{Œú1óF¾Ñ¬î0­ ÊK\Zªë67Qct ݬ÷‚A,>’çñ•©¼5ÇÍêøœ>dTö˜³¤º>çrYš}&«Û•««¶8ãvw·ÛÚð5[ÜÜÝÔ&CЩ•ªT"ðž©\X¤RIµÎ a>÷Üù¿«YVMÉ@{јgm¹fƒ’£ Ê|™RRLT&I‰˜n¡L»b ë«£)%5Ç,‘”Hšëj‚~ÎÂÏ¢ré»ô\ܵX~V‚†1VswÕôÀj•=î´T×å\ÂL]®\}u‰3fW*ð»ù—ž³m!èÐJ•*F“ˆTJ©Ö4 qdòê1æJ¢…X@l˱-¨°[gµ¦ ×DâJÙ!^Û8½V ¸'û eàâ/UÏ ò똼!Iƒ&ñvž‹ý›ËŒÅIiœt\Neq¥ž©ê8G+²à'¡®m©Å}möt¶pMîj®³·¿´ÄYÒ„/,‹pnQëÁbQwF fÖ$ëY“¬«¸&æAe„̇KÛòÕK¡°Ø$ Ä!±™ÙÎýé®x2¿ °l·ž|öÅËvëá.[Œ÷ÍbÌÌÏÍI JIO /tÑ{±B]ºPÀ•SF­ÈÜ/Bºìoz–y”¹Š¨#:ˆrl%J´3Ë[B3dP81Òyñ‰Ëçd,”‡T6‰R¦ÓBl¯BsÏlJä[âämf\=.0}¶ÈêQßÑÐ|.‡Z %”:‰P‰L, §ò7›ÌˆÓÆ1JÑåù$ Ò¡€ÏM¸aE–ù†Á+•ÙB•žHcܬ°ÅÝà ;$&-¦¯ƒ9- õe°Ôºlô³#óØßë^›«(­?*_xS wÍܶÀ¬~æªtÿ–:7£3•D]:k¢ÎmM¥3.îòº«Û̶CZƽ¥³-íœWµúŽÅ³¶õÚÛ4v\¹¬ú\º,¶”ïnMõeq.èïLšDû˜j¢#r @JJŠˆe14ò’R¤tH.!) # %"c#ê‡Àâéæùó±xhÞØü±öÖÚêd"à³Yt¬˜!ú@ŸBõ¥¯¨§ñÉ#ÒÏH?%xþf*( ¬×ã ù‚š(þ/£ã ~e¢׸ҘÝßQ7\ç°U-Ø|S÷ÚÔbµ1l B‘¦ëÚïn½üƒ¯ Œ>ö_WuXÛï¶„¥žeÜ_¾u‚{ÿÃ=•Ë®{âÕõÃ'¾vå’týú;9«9ó)ZTvuvùìhêdMssu´sUmfóšÅÍ!“‘»«¦3ʦ÷ÎÞû⹑?Þü³ûzY«W§Vš†fÍßø6}{Õ÷Ÿ¸iU6<÷²ÛŸYµúÔU]¨ýuH*µ`S]m#¯Ÿü‹ÕЋm†ûr2=Ü\(¹/)¨ãPØ@)²WʧÓ!FI¾¦©X¼ñâû?„`ÈÃÜJ”õPòÞÍC:AÐ"RD“rb9±bX4T¨ðTpöÆjÇXḒ *FtÁ‹SÍrÚd²³cVC²>Yç6@¸d˜‘üJ`bÿ¦¼€^ä0~ý¿ºÏÿÔ/¾º?é?2²¤'Õà‘Ë™Ø2ò…`ÜqÿM-+í][—]Û[ö5ƒ¿Üa+óèÜua8¶BðÊÉ*-©J¡cˆ¢ *|’r!•ˆ¤i š’Ð / Ñ© ²¢@× ^,†¨z^oG[Ó,7ëó p/ÎÊ J𠏵º‘=I„½]óÙ¶T$3²Ë¦AöL²»¶¿ÝýN\»_Rb.ýwP÷Üc ‡Mnõý}#Ú»´ÙSVçVÈ•Ñѯ†ðÂ.×ÿjTÜO¨e‡v-8àP†-ܯ{y@úb+î°EíjDQ f<ƒD€¸,'3Õ*ŠAž½BIq^3$X-Æ5 ¤ˆé@õÄ‘¾¹ðû2æâ{°5¤ÜüsЋšcáMãt:ÎËêÝú 6,3E‹«ÁBf(Wƒ”Iðýôóì\šú®D-b4Ò]r£á®£ŠµsD{½âž»Œ9÷×{Ÿº‡Þ%7WûGdâ9`XÁýÞïæeÜŸÈv©Îv×¾}šïÊÇ4C¼rý`›A®ò•üÄ%L- ¥(‹+¼)šqS(í#H)<µÀjB*†ó’‚˜!‡j,!œ“HÔ’|³(Áˆ$"F²ú‚w°'ÊÌW3Cxæ+¸öšz9ÿÚÉ™/Ð(ê òŽE]Éæ2YQGå˜$]â=ìMvao¹Ò &#´¤‘Ë#®’'q?ÿƮȧ^0¯µ9÷ðŠËÍ_½â»aDÓD ‹ ?…ÚÖÔ%*£–î.F‹ë ZÈÕ ¹˜{X^"3Ú}f‹7mÌìU}Öšl¹6ÂèåJ‹+dõØÙ¦lÀ–éIeW¸!ù´:ßÅpˆdd’•&¹Rn2°:ÖjLEfg\ ƒƒí–*TZ•Ý`4BÑ·¹Ê;+åôÙy?9ù?äIœ<ˆ²&þ_ÔõŠE^Xkn:)—W íioß;”Nímoß3TqÒ­ñzk¢æü_ººu×`EÅà®ÖæƒéôàÎ&o ª3ŸÖÇ-–x=Xñ]êuÈ“z‰ òÒÀU·)E¸]³Ñ€2B4¢2Å9§‰ 3&b¢š+Ê#!›%Dkàp+3ÁŒ‰—ù3&1ŸžÒ$r‹ƒM9…»Í¦ÛößZœ* ÝvÅ­fk‰éöý·†áïÁàí—ßb¶|Çš¨÷úr¥VkiÎç­OXÉkËC·ï½Ùl)1Ýv9jZ>¼ 55o½â–ðâ oCÒfK6À·PYÁ–ó&ÿJƒ©ƒ2þpn@ ù!И0 ´á  úÿ°ö€q×ÿøÎ¶ëe¯÷Þ‹Úéš$K§f[].g¹Y®Ø€iÆl:¦š ¡›lÃ7tò%$oHB I¾ ¤ò ikõŸ™Ý;dÈ÷ëîvgggÞ¼yóÞÌ{Ÿ'•Ȥh2¨Ã̘° ‘±’r´™M$<¥ŒòŽÎŽæ9MÙtM•ÏoòëüÁ€Vm‰ë±ïO…Žƒ¼©P@TÊÀ,|/9W•5£°? Õ#ËÞºÖ²þ†ç¶^óݹ£r“ÎÌ/ž{á?© ºy4¹ÀËA­û¡‹ú–7†M N:Ñó«Ï}éê1ýå»»©âOîîX3ºT¡u\Wì5… º©ÐË{¡›oðÇü¬|åÚÝûž9s¿\¯P»ÔöÐÇ4C/"Ú‘fŽhS äR²m6iˆrÙShš0ímM ùtª*áó¾˜0¬Á >Ÿ,M ëwœg\¼ïȆsnmUĞHMslÃ>'¸;âæ°~|h9m~~&j2º¹BË+{6>xÁ·šìÉWÇnØèìÕZôfýøªjKTeUª¬Ñ ›ì»ÑÐ;ZlÚ|xÙ6™NoX)Ñ©õM6G4DqfÁ0Ïç†lЛPA¢´QH‘7/Ý( ~#Bw{íg£Ý!1à - |Ÿ¢ þ‘Ï mWEcíY-!Fgª@Iø™õ´«˜†ÜJT S¾·P‡6RÍï éÕ쪥_îÓéÖ¸/6ÚßVçÈn¼u­œ ;–iW¨iQÛ ê@<åô´\¸¦9·ñ޵ 2äXV|µ÷пKï¯v:v³ÜXûÙƒ=V€[TîX~`}{ïUÛºÙ–È º‘Ô2£Ak¶ÈøýÆÔÐùcÅ;Ïë“0GÈqOmØ©äoÒŒ“¯yRQ ÌÓ¹f*I¡_$ªˆ4Š„LéµÕêBN”6²θ\r}G´Æ$݆ýVÆiD†.Í”ÈGi(ƒ Sä¾ý«sg=°UEË gÔ´*Œ•Õ0e£Ù¨× ±Hç¶?°X‰È°¢£%½¢(QíytÓ†c†ØëÍ‘_i ÕEµF¡ÕI'¯fíN£R¾¤¿Gzvö¿wÜì!'ô_öS\‰ÞaZ DÍê5€ k$r|£Ï›ÚAÔ:q‚i'“j£]ÅTÛã÷÷ÖµÚnRM¾yÙðeÏA«nüÂÇñƒr¹t®G'Udu3¤r)O|‰–œª–BÛÿ¡{8iÇ–D|°¿{n{kSC]M|$1‚’ûý*Oœ ‰‹ úO¦€3jVÃèlÁB@-›fe(Ê•¬¤k'F @"R"}!2»ÚMCø8âÞŽ¡T´X°¢!W_W]•LDB.‡É¨V*䄘â¤6âŸÌ; þ r(96C-È8‹žS_¥¨?pG “Ë)ê=ɹU£;»ºwŽVÁÏî.ø9¹ŸGëÉà%ß¿®¿ÿºï_²ïèóûÖ^¾0ZxùÚ5Ðçøö lT‘îEY… &¥cAV¤-»¡8Q¸VpÍk#É]Þ9æ…tïgÇÉÿ²¥gYçÂ:Õùá(ÔgkP¼MДQA¢„Q8ê\ȇéè ¾„sˆhØë6èµ*àÃNG$j°r|.¼éŠ<—¥SŽfŠ4,¼rMÆ_X\ÿQvSø·±ê7‚›jzR6S¤!˜“p*dV'«—RÍ=ãzQýÒ=óÇ—.ˆ=§ÒÔû^æ…çvv…Ý™¨ÙϺ¬X‡._‡pȪ§BÔ×`?š‰þ¼À2È[ÞHje5Ī8ųLI] ÃuÃaÓª‰fÐ,-õmfÜ»É\±OžÆfõ“;´.óݳ·5m¿g|ÉÕ[;jÔÎ-ÝÕó3NWǖ쪞ز¾xsÒUîqç7Ñ‹ÒKÏ›ûýþà»{¯ØÔº`4e4º;9ê»é¾ZË`®î­Ÿ¿ÈËUôñiÛTš®¥{-aÂIK=ôF¤Oobpöæ>4š8€ŠÝFƒW@eÃŽÂÞj=HQ8Ù1uàAŸmË'ÿb©fŒäŠ?üXÂò—€m j¯ÍÍ¿ šýŽmT˜óôD&?sM©+àG¼eHDà ˜„A`p®ã±$FîýI)²ËŠE|8§<ÍÌŒ/jeŠã,ea½—ÚXâ/Löõ¯9¼>Ý½ç‰ Íg¬ñvë›uZŠ ®ó·$Ç’7n"¯¥^f¦mã†kF':¿ÝÏ»×Zµz_M¨cynÝV:F^LgÛSSLïK£ìÌ{¦„CTbŠ!7RÔ`ž=Ò«AÔì­äŸû«z®ÜÒÖqþ#6=|N¡·m]5?ç/ø/Wã‚lfA£ÛÛ8’Ê,lòPv-W»æÆ5Kï9·«ãü‡'–¿2®Dù_¬/¶Ãí‹k3 ÜÁ|ª›JSCºë‰>y'6!Ïk@öQ ÌÛøXˆ`ă‹ÎÈpöYêa¯•X<~îYð±>jž|Ú™ »Ï­«>¡UÛŒJêY³Íï¿ Ùz+!ÞÆX´ÕÐî(´Z¤$f‘‚Ò¤b¬ª+qD³pê¥dm׈Æ|M{m{4ì©öVs Ü8‰¨ˆ:Ò•™Dñ„ —ÇÉQý¬³)Ò½ö«;['Î..nÚtCqñ››ÏÚкókkkûRö‰µk×ÛSóká u¦x["Ñš0ê¢mê̺kŠÛîŽD­Ÿ·gE&³bϼõ‡¢‘»·¯Y—yÎí¯Þ²eu¼7ë&{³sC>aƒÄš›õ kèÖ©zêÿÁ5ÁMÔ¢MJ˜k$AnBf?-žõ`´HÐDW(˜ˆfÉVðˆYe“T¡~÷ŸXרßÍÛûèÚ5G.îí½øÈšµî÷™§i´>5ÒèvCn®mòÁ«ß¼²³óÊ7¯¾æGèóGל¼®í†Ìõ:\×´„·à’3øP[¶H¡ †…ø!޳!—׬wzuKqPuX(òkž6]¸ê;ÿ8qâ)ð‰»ÕwŽIbÛœ'¶<õðQXË p}€zø(š;’RI¸â™”_¸â‘NvÑÀ©A-YËÚ¿áV¸æ<%ÔÙŠêÜ©#ÐêÓW)¬Êøï­>$Èejª@·ÓÎi@ Ù"ߣ…ó{ Êvø-*zÎ÷$ã_RÆsØ×ÖÓåó#PNÀzºP=»¼˜1Jê(l°ùËKj©Îf,¨ÎfTç¹ïHv‚/”~ƇÏwþ}GhS7zþ<ñ9R aðC J;„g?¦D\IüìùïH"Ye_J"ÕÕú¼$hj¨í¨ëˆE¼5¾xH̆êîsü8?`Ý=¨î ~Aàhì/7×C°s%ì®é G8Œ‘ ¦72N1Ó¦óPžF‡ÜP¼ñ¥‰‰nX¼ø†&&^º±øiuÿD>?ÞŸLöçóýÕäáG§ˆ††Ä£NÝ?<|ÿÔ£Wÿ×–x|Ë]}ð‰×êjظùLÔÕ}·JÀÏ)Á¡XŸ4 ˆüX¡ÌJPh<œsû_ýˆW>ES÷¯ïQ·'ë¶*@£ýÆÑ\ƒ}®BùÛcp%TसÇ(Ú c^N+“>V%«@3­JJкêTI¼¤V(YXÃÒ-½ySclîòzþÝä’0H†ªOìÖ[ÀÞÚy)ÛÒÑ@SO¦ÍÖìÑô︅iÉ®½jAÏ…Û7eøŸ©5‰ÀµCæ´\•Z:¶,µt¹)TïªrHM«®[U+ÐòÕÓ°ýsˆÖBsÐŒ|Z™æ)‘ •¨€HÕÆcшǥÓ"ßIEL3õGê$ý‘-õ̰ôúñ¬½~~Ío{οwÉŠë¶ 86Æz4ºHª-ž¨·qg®ÞÐdîY­×¦k/R÷ï¼™iÉ,»°«eew¶ïÝu‡'êÅ}‹’çö5&mþ|w j~Æõ0@›Æ–7Uô—„ãDÐj¦뎡‚_Ð|`9b)F%ë uƇmæ‰NþÞd¬äªwH1üÅàðéåÿhqšÏ¥:ù5ÎÎàä¯^IBZßçp‚(æÄЩ*ò12a_B‚bÐöÚLg(!g;D`”Â鄈 J÷IL§ÖÅÈôšã¦¶ùü#‰žñ;6fzö>¹k޽֠¥ªq ߸¨Á¹s[riÕ›ÉËé×S­ügÚ–­7,ØøØ¶DƒgÜë 8#sWä6žÍqQò Ø8(­¨û1ί ) XoÊél:B š+.öeÕë~ƒŒ÷s©ÇÁ{\Ào¼¡qmo,Ö»¶±a|^ü)•Õo2ù,JµÕ§×ûmš«–_>–HŒ]¾|ùåK‰%—/ïZT­ÕV/êêZT£ÕÖ,‚-°L…>röDN!Ði%6©¯Ä·5WT•Í)®ÄµY̵c{™6¥ÕÄѹßÞÄ/[æ×¤dØùÏNÞw– “O…ÈM°ï¢¾P*¯ß8 *SÀ!ų^Â# œz7¨>Ó‚“ëM¯sln(Îq["õ޳óV§gò÷íX£×ãô«•À:îøÌ›ïK„Û›rŽR™Û¶©AÖ~3¶*æ‰:ìf=ËmÛ„Ú» ŽÕfØÞÒþêál˜Ö¾ÑÀ‘§ßó©J ƒ¤s9+/Ùì©¶|@‚ôôr",Ù`GnQ“»6“oLö,­®i«Ó¸S E܉«<õ‘H½Sá x| •'°\ZÕ¹|æI·û›Xôù¦Åy»¾j^ΣRMþ„ÛæÓKw*Œnƒ'l3Ûµ¬67œÁ¢Ä—Ûñø›ˆx!RÞã!é¾ò E±ChXh²³¼X…ÅnL/ÖTŠì\¤Ó¦oáß·8( hìŠþÆ“ cJã-·µýàeþ-KÎ=EbÞ5Y€ïŽí…B­Óa8« 3g5K Tš±òìžåÉ'v ¼TÇ$3½ü䕚ôWçoÖú|¹E®è¼õ-ÝgE3:»Öæ ;9US3W·Àyà@Í Ö“¨Ê¯ê 7FçY4zÜì·% © \iwlÿ%°ýN„ÒuŒ3ÇÉpD;‚8].Ú*Kñ G B.{ÁITàŸt¿x<ä «­œœÎ[§³¡Ãž¯sVksÀÛÕ u®ijYÓˆ…«êÜwñß­£Ü~UÉdY«z³©n0ŸZ:7îZ™oؘþÎÌbµÍo¨I‰ñ£/F`ß¾xÿÇ-i!”h‚•rDÊü…Qè¦+>6;ù·5VðÉJÎ¥àY™Q«?Oú,Ú „³þíŽÀ¹†è˜Gù—LÂþs„U¶º$˜(›\$Ñ•L¸]HENä“èð>îŽkÕ„8+ö€BÓŠÊ)v€Lú2Æ(††iXÕªk.Z¥R‹[ÅæºPǪ³K'õ'â_2ä÷‡ 6Nâù ÊÇ”Y`©ÚëI»\éžØ–„eQºv0ï9¨6»¸ªX¬ÊW2‘„5©2X•‰šoW@º^ûê†\x† gòÌŠÓ™€"ÏœÖD,÷û -xJ ­ä£ ôm‚3Ù7zCö•M+Z}*kм[Yã¾äkƒñäâpµÜ0H%¶úùuñ‘¶ˆwÎÂL´«­Ùûu­ü#–HF‹ _ΙÚK®»‘(DQÀÉôÑ%R,!Ñ1dç,”GcÊè‡Bã}?yäȵ<v=½ïߨ÷´Pï>¢‰œG¼ø9q¼Y¯Ñ»¼Êç_Ü×è=So“]Àcö`Ì{NÜ ™õžKQD¦GŒÈDŽ ÅŠ3Ži¤¿âì—à oœ:ˆ«ù»ÀÅäR¨^²OjPüjÐ`*ÅÉâ°8øe}„’UHº …®µFˆì^ö€ڤUÒ5[?¼dÔÈ\ŽÁçP1¬3/Ö©’À:³õ(²¿ˆé…Q@l3¸+Tsšþ¹…Bç TÉ’’`x÷òçNyõ–?ï½äí5´RkR¼ îËü“ÜFþXÜ—¡ÀÍSß$~NäVÓÏÞšÃqúSlØM#.À:-<ù?¨N ªóXæçÈÖWÐuºÍ5D9ølòv·Ðž4zöðÔ/á³Õ…D¸¬µVè:«×uÖ(-ë¬s`ßæ uÍAuÝ:õ¬k¢ NÃuZV¶«…¾:N±\“ÓH§¿-øw“@8½q9tZ8Hy‰¸iA®ª ÅmS¯À6œ¬Ý–ÖÎÚ-|>ûàžGùaÁíS¿Ïo-h¢H»ÀuÐ(ƾÉp\Ìi—Ã2ˆžý‹Kaà]™°dŠºp¶£OhGµãŽ©×`;æ£+V3ëìÕŒš¦ŸeÖB6 ×0aÓ ¾' kÈïÉ¢÷Ü9õ[øGÁJéøhAç6ƒ?VÒ¹ °}ó„±. çîšz>·ù¸YØ+‰ÎS¬â®fíÏ+€¸¼`«JzÜÈI6T5DBî„'Ái€‹tÉÄñžö¶„vÌGíøÊÔKÂx‹Ú…TÆV$‘1À|žºã……r…à,!6P!x\ çÜOÎ~ÈÚæD6S#Ö–ä–  0ú§þÉÜÉìFmêü@¾Ž‘5n¥êˆŸã9öwê⾂i#))k¬DÖç #zš×Â&É%2yNBBÅÊR”l…f!×(`D%%q;Ñ:C•ÈY® PŒþéŠ'ŠOg‡R±võâE£C›;7ç³µ5ÕU‘æhW¶ûýj¼÷Šú³ýýÁý@s„ª…4²`®ÅÛ9JjižTHÆ`çº!/u(GÈr…LŽÒâÐ,E %ÁJ”ìRB›,Q{·*’Uˆi©_™Ò£è®8uNWIŃłK©X¾¬¸hh`~ïÜŽ¦†\&u»ìEJ™ÂÖ² ñêÇôÖ{äD8Û´—#ׂíü5oðׂmôV~׳üNpù³à€Pÿ62F]N¾û˜Û(Û‰wÉwŸž©!ž¥^¥;àûØ'ePðÄe¤DÂ2…‚ô)þuÐyÿ,ÿÌe`.ÿÚ~õÔsükü³ûAÿýÐÔÉîç¿Ò‚$ÃþƒÞ59hMTBøÐÈM}ÊZ[SìÅ2Óך›õ;H=~b´ ·ããF0>³Þcå YT ;¹}Öa}\D=BÂx(^¬R¨†ÿ¨e'îEÿȆgHý3rÝ¿‹z„^ •áÛ…¼ Q °œ`†]A¢ðmtø³DЦ݇š™!”e€é´Ï{\…Üeʰ›>¿|!,HŠ/,Y,Tz!<u†gSh®#|I‰? -·¬w¼»ñØÆ—ùwÁ6¾vû±í?¹A~uèûßæŽgÞyç™; UVƒêVêU¨Ÿ7‡ ´¶Á–0B¾õZRX: –…¦1 Z…píš(8a R—e$ì˜ òlÙ¢R,¦†Ä¡mæ(s¹8,Æ".™~j”†”ý\¦:‹¸Ý6 §QÊ‘º/åPZˆr’§°`8éOs|ÝÝP퓸ô©ÆOj|ÕÒ¨§±Ö' „vw¢_±Ï½KmPÒ»­~³ÂpªfÙm™ZøÿšÚ¯[¦Òõ[áu¿Sðß$^¡FèAhkCÝ \#¬ß1¬Ädf\À À0 þx²!W z-†SäsàúcŸò!³Ze~Hé5×㺄u=$Ö¥F¸PY¤|¡µ" `|Ð`½É|Cœ?j6Pà•ã¯ò!“Z «ñŽñ[Öá9$úàœ=ç¿yÊN‹#)’O,C¡Ü“°}ËJ† „—@á¤Q«”e%›) ?Z…â¯E³–>Î?þÿh žy½w¾w„¾—Ð!´Ç‚KÇÉP\yaöÜ3 zRS"”Ñ@ z'$—Éÿ`C€^6PðšNLQþ«g1U—+8©„S\­Ž€3ù†gá{„ï}¾—ƒë>|/'G‚ðüë#ʸ>ðµ:”QÛˆuzdQ•è ;I=tbÊä-Öœ»1Å?ï5â—ª~5|¯új'Q™®PÁ×~ö(g€zÊfE¤y¦‘!½‚@€x}bž‚ìDy¦á$Ú4ûzñ¨ÈZ =@-ýéäùä»|=Ð ²,>õ'æ(s á"â`Ûq¹ á»´ ÒÈ…Ü@­Xr‚TÏ€ZDÐ畲àRš/U—áKÕeúRuY¾T]¶/UWäKÔUp@J24‰´—RÁéÛEñ?„³zÜÖ®H8(ÑÅQŒIÄb4ë2TmÒ™ƒÞ²w ¸=¿ù¶Õ{ß9XµãægÏ~€ÿøùñÏÉêH‘ùì=ò›ÕKö\²"M¿qøºÍÍã;WunžÙð`n¿H_ÞvΫüä•wD†/]QŸY}pX{ÝÔ'´›¹ŒH Ä`A‘ŠyÜ‚˜PðJæSvEà!„˜>΀ÁðK&WÙBPTWU5T7D«¢÷½3åsžrø¶(Qú@RŸ8ê"!UvdÓžë‡ÛÖ¶y—?ô—ë´ž„ÓÛœMr?t,?ÿÆáŽmCUÀÙà$_¬Y·¤K¿ôðÖ9Uä„T«XýÜC×N4ÆûÖ7­|àúóS²uûúݾlw¸®ßÊtýY߸L¢f)yßþ£ëCÅ5sµ9Açè ~ÁžMÝF„áÚ4@ ¬€!ç%I¸"_ƒ6 9Èã!•³2 ï™à/P9EI h¢kNS4ˆ®Î¦9©ÚHc´Ñí4êPQa iÇ «*·SÌÓøÂ8Z}"À[áL$qQô»g®ôWžÕ9xÍ–BaË5ƒg­ øWžÙ9xíÖBaëµ'¾³Ûss:±޼כ辰ޜǛë†;šÑ­æŽ_¸ÓsÃ( Vý²½}}{—ÕÏþýº9’ñ /F.‹Dr®ôiŽfÜžLÄ,ðJ#Î)y #šˆsP6 Šp–¬VA h%é+¡Wà2TE£NGöÊKÞ-â•ÅàtIÄ3õñ¦D“?‰¤èQ¤930½D9ŒJSIXpFú®n»åŒæž~uSñ¦­sú<½iÙ¾¤§q¸Î_È$5j‡æÎÚ¥ýst2wíÜêŽuíÞåŒÄUåæz.u÷wø§Þ»¨aóáÛ_¾f¨¥±uÁE£1½EÏYN¨#‹¯´×¹jŸ×³qƸ;Æ?M °1ÈS8߀"QP É@úŒXÉ‘áhXL’.ÄVáö¢}P¼ûÜ,¤˜íÊÂê‹æûúýðžÆaJw&ÌÌ9ŠæU{ÄkZ;ºfbèû×ZQ¥ÑmTT¬1Ù]÷Ù•ë­Hªõ›Tzá‚°–ÔB.f·V´÷Âá¸gëšaI'‚^¤¶zý˜wçïtL¶*É]?ùî\½Ï¦öÛ¬Ò2¬FR¿ír\a ;íþO±… _gÞos’¶òÏCCe'\þ[ÉkÀP˜Ì“BLäe/èÞ?òaˆ¢xQÀŒ™±ùçsÑúÓ,øÿ¯É÷ø}!Å8˜ß¡"þ_ÿ<Ä1O©Å@‹¶ž‘(ÁVµW ¢­¬“± ²ZY‘‡œ ÿ¢µ]†öíÚ¨ßNÞŒ<¹É 'œ 5x“¯R©`C&ïè¼ãG‹ôQ-ùy”‹qxÍz”šb^…k¹ÈœJÎR(„ˆa ¢LK{Ò†â“:Ùb %~JL"ÙÌûûùó¿òƒC¦‹ÁSÊ͘\û…f„¤±·ðøý òÙ¶ Ò+I<Y0”›%¡¦-nöØ ma¬DÍÙ@PP ')fL¤ž0o¨I§)cÃŽÐcXhÚÍ,K@3±\´/—‚ö6‰!9šË2 öiA›·Ó…6Ndpæùá8!oÙã”=½° ïѯ¬·jÏ‚úpG³+£]UW粩LšŸ3‡ð6‹KʇR}†È٬ߨC²H¡ Êó`Žä%8ž â[…ÒÑ%© £› Ô§ ܧå¸K,súè˜-=3Љ]ŸUÎ…mêÏ¡TEéÿ#I£"Ig0ÜçQt&þé÷5MÕNt/>`q‹ôŒ"zê­‚ðÃô„òº‘}’p5ÄYÇB€•–¦±£– ÈÐ9))%È1„YÀ"¸"µ£¶JI–”²ðŽq2£0`Gpy¨‘ZQS]•ðy6“íý³ ø’—òÊ”Á8ÕdS?›ÓȇŒn½üV[ª¯®®§¡Öftée·ÛSóëR= 5Ÿ"VSVÈVv˜³y5uc]1WÀÅÙ=šÚesã® ‹7ê×*ÔR‘ÕJà¼å2M$J¯LÌ_—ϵ´äòëæ'H× ÷¾Í.Ì¥ÿeÞ >–üP Y¨  ”&6Vø@JPbœ.•’ ¬8e9¥Så„ÏÉüR¤dã1F‹ô¬RóOÎk•ë-.Ã}«÷ øî^¼|ùâ»}û$çß礀Qk¥ääϢݫóùl6Ÿ_ÝÅmû!ÓC') Í€RRî˜V–;í˜~ae¶/® )º#h_(RO7¦zFöµëíŸ;¦‚³ÁóGÓ¯ŠcúãhDsÙµN¿þãÙCê°½Ï&á›vÃñ¼\ÏÆBa h<O‘åf'Y1žR–PŠ&-Îç-;‰åœfƒâé® Ç? Ž¡!½gñ²Ò’2­ŠÿL®Æ“†Tœ§‡Ø7à˜&@Ÿ¨™ëK» ÀÁš¹øCR¶4|ˆî,³’ ‘<¢Ù1aœ¤'º§¹›%RÀ¾²ôì4­cÿ}‚üg©!ðå²Å÷@¾4ÍÖÀôò²6‹5ÿ|jÝ í±=È_O¾4uÿÌ\CVBÈ5ÄÌ %"_2‡SNG]Èd Õ9œ©°ùwŽº ÉD?Ðg Ó77õ1+cž""D1ФB€ÆÙ#8ƒ>?ŸŽwâŒÈ•Î&Bîâfñx¤:  è†í2‰‰pJHÁQ_âÂÉÈ+Þ˜‹V=ñé-·|úÄêÕðóð§ÅdR©V6w\ÄHKæÔÖóÍm¼yùŠ›7ärn^±üæ9òð¡OÿôÐuŸ[?±TYãælv¹®Îwf×·èŸÝ¸ñ--;ظéÁÍÍ;DûU¦>bc°ÿIâ€àýB#$â`ÿ2'ˆ™ Â|n1 ²ò Mð¾.vr 8û €ˆ„}»MÏɤ–H‚$TÐysšCiXJ• Å•HÉy9„bqyûD7´Âúæ)ÔŠ=I$Õ+®[=ïÊ×vï~íªù@êžøQÕè®^Êò™‹bb¬…´˜{þXðÎ]¯]?<|ýk»(ˉ÷zwV!^èœúˆqÒça"EŒä(ØÅ,¢BkðžöJCƒÌà ô,OI±£Áéo£\¾:¯NFRє۩Q¡“w3u*ލÈÚLê+²ú’½ûßh¯©QëÔÍm2RWËŠÖDÏeÏŸ½ã…Ëz{/{aÇÅ\pñ%\@}4¶@UïÑXlK²VŸue× ÏÑ®½%úíÞÿü9;žÛßuâåŠD¿€Ø û>ö½šØVPËá,€CE€€@év*ð%Q’,€³Ò°·4­:‚²T¥5&*†½ †¿ª‰ê°××ûb‡%J–KO&'$!|8°yd¨iC5ßRPkÕsÚ.`¤Ñ…{—Ünñ2:wÛ¼…u#»%¨_O\¸<•$_¿_Úš¦AëY‹²ÌdÆlîqÕô¹µ—çˆþS2zs¶ëë‰ë '±?)•© ´, )¤ƹ å„B®X x7ËdXQÓ®Ri5j¿¨<¦ zhš4&¯§>UW yj¼5úHЋÜUæ8@©‰S¯zfJìQà×#╲ pi¤æC~ÿ~DsáÖµÅk×çZÎþêø‚sj>ÔÏ¯Ž­Ìä–´øœÙ!~5­ùì#ú—/¾xÛgÏ›Íc#»ß´ýÙóëã?8±«! ®«úvÏÛÜéáÿt;–™>bÝN_RŽñ< Âð—Äö\ûâ–-/];2ríK[¶¼xíȯk†·¶´l©©ÙÖÒ²u¸†<³|[ü,wKpm„–è%ÆèsèË ýa&üÑ® ÊâÄ&ââââ6â~âIââ#a.Ëầ  ¢L€15€ºªDÊJÆ´@Ê™N*3èM:ý˜˜¬Àl7™ÇÀæv·Í>F¸·Ë½H…ól(ማ‘Ä[\òžRPRôÿPÕÅBþùgŸ>þõÇy辯~åÎ[o¹áÐÕW¸tïž ÎÛyö¶­'Ö®^¾tñÂáAäš]hnÈÄÿ|KÔg}òŽùßü^ù¬äK|g*¾ÿ\¯li‹:œ±˜Ó},êD_œÑ›b—ØêÒ—HéËä…¥o7•J¯)]ñ—¾P甾x¤ôü¸\ÅöÒ“àÓÒ7þŽÒÝ£¥ÖЭÎ(¼²âg§3AŸç‰¿oƒï|AëÅOç¬ûÎòs3+ÄO$·L}Bÿ‘i†ÿ!׬:9Ï %Eb¸ .×±™WituÆ’j+ìJ ÞyE›»(š©X´>bjwè,„e˜á¤21ÁžQ˹R.As èP ”'-Ó!5 íþ†½Æ°7\Bª(¶ ‘¡œˆnUŒÆ+º]Ñüø7îTUÂâ•:8MM"ÕŸ¶sAn傾 ÿÏMa@5N¬]ß°êî­à«oþŠ–iäç89¥95ÔÞß¶ÛVÛ<±œ© ¹&z¢ê®sïEqFù§è0­`îE"Ò%Zϸ@bTÿ&Ú4Qgƒ#JˆùŸO4N$MÒ_@4­’fÁ/O³>4«F4srêšd}½}×¹U úÍŒD³uf÷îhxóWŒL-ßåàä&H2*<ƒd=dÈ'òç;fõĹ-‚(©÷MA JØÖÑr¿f©üuš*iPÙ}¼€Î()  0ÛÌú§‘ùrpô;#‡véê#×ïJ,ä J},ݱx{×]wîÏwÿ¬>·§Ço\a‹‡ƒFÔ¢Uû}ᆈañòxOk£ão_7Y;:4˜¸tUÌݳ0AI•Ò3lZVéÉDÁ’¹}œ»Ú5¹ŒúuÀ–K¤ì’ìòó;ø£X¯<<¥¥`ÄŠ‚Ö û›°¢ñg+úkÒ}‹*úMˆJ”³|ofÇ…û¸Çú°7XfôÐIýeOîîÀ_L÷FqWþpSÔ°då¢= wtáÐprð²ÕÙñ¨¹3lõ‘,ÌíkmšÕÃÉïÁ†­+ç¹Óã‰øÍ¡'ó·fš¿5_Àßæ2kNÁßþSñ79ƒ¿OëÎ=OŸ5zý…낃œNaÍìZ¼g猱®Y08˜Äc}ÙêÇëmUÍ3 ªPUhõ(ð cÍ¿Bž[9ÖÆc=—ÿ:x…ú5á$6ãÕâSk³Ã;ÔH†DöÖ´£C!U;Ã’¢ï9v¨D¹ÌH”Ýe¢¢L † {Wêa³pZ!aPP ´¡ ɱJÂÉ ”°šÑyÀeœ$äY•Yµ¾³}ˆ7êýÎçæLjÙ£Ür£c°¥~u”úõ ·Üî š¡q?Q[ÉÙyD„8§ ×£õ¶ŸO¹üÈ ˜†ö pLí8²p)d‹IÍ$Ÿ®‹ ˆ}bìRÐï´[Lœv)"Ø,Œ^ÁpáÈÿþoû2Û~Ëõ‡ùÿ#ìo- ÿNí‚´¶!bð>žjŽ“-‚´,hœÄç0 1u3oÑðÞ€˜ FÈèZØ{a%æ“L&33 &Mõw·>y§/kîÕGÊn6»æÊ… ¯\›ýÖò"ÚV-.§Y¼1õ-m„¦òk÷÷Í¿tM¾aÍþùË7o^¾rãFØ’<ä—؇ ±å’Œp´ ìî;]ºN’:Dþ;9=ŸJúÃI¿!‰ý©lH‰Ú®²{Öh°ªs¨³Êß±ºÅÛÕÑdʳf•ÖLº‘ªÎÁÎ*_ëŠæÎsÃÌÝ•FesX¬&kõ¼9©Åm!µÕo\¦PqÎâw:ô¦êÁÎØ`s(æ‡ 5@þ™‚ü£#,£¹ïµa‡–°«„D–ú“Ðîý†Û-É™ “\jPlg=–;øß<ζ›uüsîÞH¤×Úµ¶ÉòUŸ‚õ¿ë·#œ£Y›ÂPx#˜öŠ û¢^dp yJy€(=æá4øí·^Öq’‹(³V¢S~ão¿¬3K.m—˜¸côÀR©òm¹^~L%çð—ꔿÓÿ@ý¬§þ Ûð_° ÂV0+)ÁçW@ÒGN¯ ‡ðþx9~yE"püEžÈê’)R‰^9,u›¡wóÿ ð²Y÷˜­ÊdJÚר°ìÑÁ÷ôÃ÷hPNoö=‡&%ät²XrÕ )`¾<Ûõû´j‘32:;MtÞ4Ù/—òo€ÌädRPÇÿ/ÛÞ­vªøù‡ûÔV8~ &e¾I±»?'ÆÕ7ù(}ïgËØÝÏ {à-ú òjÖõyÏ@Á Þ:~œuñC8¶̧Ϡìð¿ü—è;á“Sgã™0I\‹8j€«’sâ夳„TA,=Nþœ¹J#¢Zk5Öˆ²œ<ÌŒ¿#Má"ÁÕ Á«€«´N+7*Œ,MÈ€ #r—Ž@ýØ•I8 ¥"Qõ® g{éƒÃÈÉÞåôëÿÆ\j·ýIc˜”¨õÊ÷µúI$Ϧ~Âü”ü€ý l“E@ŸæåèÍZµ\¯ÐŸòÍ¥o`*šÒ\_ƽöj4mxü€3f8ÁþÆaû±Îð{—åÇ:Ý Ó+Éß³×CÝ?H¼ œÙØ ¡I†;íIµ¹TB¯‚‚­—4\"&¦‹”ÏØé𴘦$§8b˜ àÒÇ,Ys4írÖ‡Íæp½Ó•Žšg­Ü[¾'|â5§šyƒü¥äp„‚DºP‡N©…¤9åDñe©Ëâ³j£Áëq9lVCÐŒ$ÜIླH÷ìvžé€0QÑ.“)\Ï÷Ì8²ÄRz+ù8³_àl´&ëPÌ*Ø„iˆ~ìœf.Ò‚,¤§3ù j@pZì2cƺ˜¿¾âœ\:zV§ëë {U PmW<áê<“ÙŸè[—w$ÝZ­;éȯëK ™´—΃XaE3RqŠX “ÁGâÌ\%ê0ÖÅ ¬GDÚÛÛ¨÷sc‹j9«®ÖЮZ¿¹2ÓeM6ù‡nìôªàÀK´Ù 1T‡eàfø¾Ý¥÷iNSa1àûÎMïBçFƒMƒ—;°Ûà¯uQšj5+Sqþú@S/s¥«.d fµð]œÊÛyã¿)iíBõ­âUdÏÔ Š[õ NÆH™Y€v6Ëñ6|±OÜí,V¨<ÇÄèy³7 e\Ú» Ô=Ê÷…øÜõ§©m;S J 0)djƒLØ'ÔÅwTEýB¶ÿª£ žÿîÑø w qÒ«è^²‡Õý‡ânW÷`ãYŸaÁ§¹Û?„Û/b‘X„Pþ“ÑóÇõ©qõÊЧ8 ¨W¼÷É{x)Dø·¾"¼gšNá=f¼¥¼¢b9–;ìáÌèñFÅPÉag?¿›ÿîÌô;D»0œœ^€Û:e0Ú6|¹GN£¸?4Þô*ÙY|J€€J˜YÜ—GèN«à‹àÿ_¸‹8.üWà»Zaÿ»%4&î4ASx¡„mGƒÀ[^¸[ ‹?HrS¢ç˜1¯&(YÔô!¤à¶W\Ÿ…ºvÌŸ "1©SéŒ`ô9O뚎P眔þ<%Ç(UZ³ÑÕ¯®[ÖçÂ…š*huÚxR83‚íZ‹ÛÕ_+`@ Ynœàû#(Hxí°’¸q×g¹­3†RÈk¬7!½–Ú—ñ¨ÛhÖª” §>õäÝܼÇk&‚úçr±.h%øìÙWâY Š„|Ë’Çáïy¤…ïÀB¨ûM\·›ØˆòÇãñGìÕ’¤(r9âZb©Â†"z¿ß$7μW°M_Fˆœå»ò+Èt>ÏèG› Œˆ/ÈŠäGxjYÈípdà/òqW}o¼j¾-»,Þ“vø¨£”?|tAn(ms:l顜ɔ«q¹jr&¡‡Eú,˜÷8‡d  Ú-‡0’‡³ž;P γ¾±òFÁTº9¹O¼ T™&)PúzÙGÀòG&{[n¹… Mƒùp¾wÂù^Â?\N‹X!éC#1„O~ÅZ€—ó‚TÚOvòð)ÿðOð*ðmþ7áqàùï“L=O˜­Q‹òøhhŒO3d ‹shéÔJ¹ Zô&`bÑ&´á¡Õéþt½ß't˜3à½5O€Pàx0@=®Þ8oY&ÓÖ7\•DÔKV ÷åó‚ XOw“í¬cØHI„«΢ô—fIXþóñÆD™ôÁµ¬=x`Ýëð³`^mŠï@ûIà1Âw&<=g笈õ¢VcR¼c)‘®Ë€Û-K ui.ê¶8-þÖ±\ÄÉÄõ‰ú9xoÆ}¸íúdý_b^Æu‹G|/ó4Ù/ùä?þ^v¯Þ®óÇ»RŽÃÀh×ûÂu¾Dwý“@«M4E…™?^¢X0qjø7žŠmZ I¼´”TX${Ð6“º¹cu!úŽF0ŒðßXåõâS]( Ãæ7ÞAð‹6ˆ²î'd!kmœÝ}ƒ;;/îk÷,Ù9bPÙ=‡Ü™y ÷×¢ô+Òp$»´Щnl9À„¢¹%¿Fqm3â³ßòNÚ1õu¨ŸBºÉX’(èµ¥0³ Ùc5£×*€À; ó3.´ÝÁ@ "Õëô:P®µèÿ6\nwØe7mÛ~›Üî´ÉnÜ´I£Ôä_ÈïN}òUAR0PØv8âk'z$¶­—(µ§å—^‡¤v3òfÐãż_c—ݼí¹Í«õ:ŽÿÁž=xlž¦{H€º û$ø@>†äê¼jüƒÆ™ÌÁu~0ŽÐIè ÅÚŒéÓ¡8 žµ%MÜ(lêÀ¼¨ÅïoY”vÇãnzƒ+7XW7˜wíV%‘Ýè¦Íäû ϯk ɲþ•}ä?M•1 ±c4ø}XõçVý½:TV«IýlÇ£«Ö Ý»t¢¾·Æ|whÁAúîé»è3 µ‹vÍ]²ÙQß“œ»kQ-ìï]“§^¯ ¤RÖ Öa:ño€ká:÷ ? ,‡° _™zP¤“P}z?|Š¡@TÀuÛ5ùgêž©×ÐZ¸ë5ÂO:§^üô>@1€P"KÉ¥ EFìÇÛèqp5‹t/ÛC’£!G º,œ«r‚gJºt¸"wzq‹Ïײ8M¿­ªŠí´¯­Ê#ÿ-â+üR>õQƒä'Êlæó:M:N%“Òj!W™ÁñΠ«!jlV š/i1R©+YŒË õ~s¯L8´m‘J Â°É @„bd&eC<¯¶ªU6u6žS™¥²¢TjR5$TV•ÝšKä¿§]cWÊ5Rß<ŸTA’ ©wžWªUXܳB+ñõúáuFëïõiuWâ½{ø+)ÍÔ/áš%RÄâ‚a YÄ}?ä eÈý£.²™’(2k@Îw©Y.€¶ø r¿ß D©9n.{Ubþ »™AÁÞD«IùÛ=–ªÖhu‡ÁÐY-$Íæd!ZÓi0tTG[«,àb«Åb­­ý‡~ÖÔ¾îŸSeó;~[Õ?ün8{ÕÁf3TWã¿„`ÿyÈ1v”P#<,#DÆ(b8Kd!¡èo´m —×¾ÒÍB;…€[9½W¯E[²(a_ÓÒ¥(?XfãdݽǮÅ´Çi¾Œ;Ì·ò‡ñ»ø‰oê6È¡‚_¥d!Ù ‚æ0€7…r)Ú@ JY,íõÂYE=zþOþûÙ•íòüßøOøîÇZW=ûÊOø¿šâÍ‘û¬[·àþHsÜ„çI="ñ1Gñ\‚ßá\‚6k%š«U–FmL+±!ŠÞŽ— ÝÉÍcH… èHeæÕ0ý‰œY"Ø3”Ÿg ‡aöéO»™%ßÀmæ/“È¡MÚ8<Õ Û…ºé>W7ý)ÝÅþ‹ù.á}üŽ؈°E'­bêy3º8‚ôA5ÆñÇ›ÓÓé>!G£L̓×Dµà&–Dè½b!¸ÒÓ|ñá9ü=¥ àÿq¶2,Eit>'úŒ¢cÙlŠýWûOl»ýêý—]3xÓ=†ËáÏ­Kn¿êÒË®¼æ>ݵÌ# ÷U»’餫8´pÿøµ>éÀgÍ ô&8~¯C^™s\`•¡ñÊÒØÌ§\Å~Ö+Ñ€U²FP˼Ú,©Â,ôʳ«ZÏP§k_ùìÿ„áL±æÈ}ˆ…î‹4ÇL¥üµNö}¸v+wÁ+/ÈÀ)VnÁÌ›^²‡™…À£Cëä;ÿzè…ò g’‹®ÿ͉ QKœ_P; ØŽV§Ʋá0âƒb4HÅ"ÀR\"Á&—Aû,cØ0EŸ®\±` ‡…jõ—ÕlÐiÔR–‚ Џ*¢`¤éì´ zu)‘†ã—ÆGoà|“Y*õ7'ºæ››&6³ÃbÑiͬ3±©)1·Îö®Ù§ TYe:¯4èœÎŽK:á"•~‡L.ã©PÀ‘êŒðï¬Sqµ¤T¥•qjH ;]~Á'î>f€²ˆ®™N ¤B¾pt@“ô"Hr!EBe+@ Šœ˜.Lœ\¶;E1ê,Òí¡œOnHùð[~@„Cþx nƒ]T)dÂ|²2>Ú¡ŒÙ’-‹h†|Ÿ‚*/ kònµqRZ2yl5§¨²ÔgÒæÚEmaoÓÂoPrˆÕÚXƒ+7” X’!…J¡ntx9ÆÝ²¼¹jAKèL¿_a5ª`WBtžx›}cSÆ áê ÐûhPF~# u¹.å£ë9?—¥ÕüàÎG46¿^çµh4¯%Z`ÞçûÀ“7ë½VµÚêÕë}èÓ‡¬' Ž‹yP’xQ@&6ƒñ6 Y6²@Ø­·Ñ­z ¯Èy*wW Ì ~¡˜É[Öb& Æbá0kð¶,Éå·x½-‹s¹%-^þ¦|KK.×Ò"ÚKUpÞ¼Ã*!%ê ÕÈÛ@÷&šwñJ[£øì¨%ý&ÌA¯ÎÏBÅ{¤f…¬´åÃCî¬V¯÷®5j+µ5ÅCMQ#«äß²¹ì “OªU:³ËQÓô~ ¤Éc&â+Â,•GÁ˜Ðz¢S"ˆ?Š¥˜fI2b`ÄeH]ÙÌ.Ôà%„Ÿ \”B©Æfñ㌃D4ö§³9÷©HÈðãÌ‘G˦“Y©`¯D¥Tpz›>N†L63j]}%Ößp5ò ü̵pö*e…|¾ T)Õv¯ŽñÌY’¯[Ü „ó^¤ËoÙ· ±CÄP¡ßÒa'i†ÆHfP͇ª( ¸¡)†ÇlÔ'Ò ~ÐÄ(`‘Cy·Õ‚’gXBÖɨÓB)efi…”ªpM ‚©äk{àªlm5þ9I›«¾'á«Òßõ'‡W£vk4v£ÒG硾h÷z›gÓKZƒ:Íï&WíÐrO+åJGm ­óWŽÎƒ?Â~è‰T¡†‚4׉øY€Ä{¸8°I" ãÝê ½Ïèç0ƒ!_gAlú9¤Áq¹»Ì™P¨1j¼³Îç¡óä]èÌŽš¶Ú¹ÌÙøg„w’„<åG(– ®µ>ìžNÂwBeŒC_¡F&•0Ú:FЄ߯÷é¯Ì„Þ[ç‰ÌBP‘ðz Þ2¹5V•Äœ &rZëhrpôÎz‹ëN«YçRÁ.¿içL®˜?æ_±7ÌiGn«RÁ?éR ÃõÊ„$åG3b6G › Ç4¢‡Úg"LÞ@Ð+E8·MÕ îB{ê–[tŒÄ˜ö¹Jm—ÔæV ¦pF§?ª=W!¥¬fþ|xS FÙ+ ±³ ‡ë.值ôñ:<{„ýP(¡e4† ‰ f¹à£eÔf./–Ágñ€EZ .JL—Ä‘ V3 ^ÔjÔJ¨º€ ²Ÿ%…ð£?- Ó(àþ£4ÂF&xcn¯OÉÍs²Ž‘-[ì*…Jª¥Î6ºv õ—œ¦U»Öº{Ûwr©|È€‹¦þ {ù(ôè\]2û\=†ÎÕ…£n“YÌ—Aù¹}œ™JåJS5eÖì~âZV©Q>ÔßÛÓÿ‚ãñ¾íÔï0ï*”ƒšE*Û)Ô#¾>P—Ôa¯D V8P¿ðÐ-œ†í¥dLBiºîúÃ7sF¶›–fY£æ*&ö©â"¹^1júßpŠ=œz•‚ƒBLA½F¹X‡ˆ¡œ,ÄЛ) !©¡¼Nª5ûÌ'ÿCmÉBoö™à¬ß·^.»˜GV¸½Ù×µvd…ƒIS0åt 1@NÄœy!áî5ŽÚ Ñ¬u8Rè(.üp–Ú’uŽkä°ÍÎÊ6£m,¼–ÀUez湘Øffj–ül$h Û.½èRÓÌF›x6ÀY¹]@}¸\r!ÿ¶ØZ‡Øz0&¶Ö)¶ŽóÞ@6jÑ)ìÉPO6ÒbT. 0#á“'x¹­¡øqzúV± hinÈ¡&€Š‚¤„ ‰c3èÒ6Ñ`®C®ÚÂ\ZÜÈCÖ¨š WQ7»ãÕZ•ÙâÐykLžÇäú±€’/‘ò=ñt<–Š*¹JÉ%ÂöQ–Ó(¤JµDqµLB1¬Æít¹9¥Š“1”„õ9,V5Ë0­×åò•¬‚aá\Õ­X¥û!?ÞEKë1´"-Ñ!»A¶^$ÄaP”’Â8[¥Óßé› £xÌ0‰ÕÄh…R˜[: Öi­n7³|½flßh®Øâ{‚Yà $ÆHS¨íœ%iGóÚn à×ßE÷€Y?¶×ÐwŠô@›rñ—ÚŸy¬˜úÁŒý´‹M0.¦ò`Žh%.|ŒäZ(Íü€f°n°ÂÑãÎC0R€Ô1¶daÿaŠ-¤U¡d ,±²\jÃ8ëè̂ł9‘ ˆÖ–Æ|"—ÈV'a;b6³W¦{9q}…b±”)²‹·ª*f…¢¸rìGïY„â”$›‚ZIÉ º[tººUW§Ï;²%eIç/ÜÕxæ}›~ð?ùÞ¸VŸì;vÛš-ËÖ‚ˆ Š>Ÿ"7 7ãNþúÁIîÝìêkF¿ùÍÑkVgÉOÞöîÓ—ÞƒŸþÛÇQ¥é™ý­‡ž@ï» ®Y¼ß\È*D¨ÓȾŸOÃçºb,?»Õb‚cµ>–E¸ÓrÝ'~NÔ¾ à÷!ÿ>{µ“kÙ÷/—•j÷?{åÙÑÖ/¹u4k¤t`ÅwÞ½iÏ=ë®ßÔÈ]îÌ»Á¥Žšnœ§ Ÿg?|%ÂìI”f="T‘9¾ éü.èYð¿Ôþ3?"ýÜ ðäìÐÙ÷_l}¬½ïÔÙ·Q¼‹Áï{ø4êà(Æ)"s¨mÌɹ=,C–è˜TÁ/`¤}m+ŠÚ$ál$ërõZ5t­C Ti†>4ïüðÛ%‚½Ø"¬Þ‡*iï:úïwOHY·…g¿wz`ߊNÑ«.^÷±[»Nüñ_¶¬zè¿?¾ìòUõÊÙ ñ‘K´#÷üò–HKùÚ·C©N³±m¢Æ8õ,÷Ïÿó3Û~šYud°ïðj>—'vö¯Ô›t™è"úÊÝ]HPÈ0ÈÙCv’4…T‚bÆ5süÒ1B*Å¥X ˜˜Î޶H8êƒÌ<ç°bž†vö" `®v`ëÓ”( ¨+XŸÆ˜l[ß]ÛŸ6ïØùïh¯³yZ×µÖ c…Y%7Ê6Ô¹µ~Ãh»ÅÐÔ5é¹vKóh¶¹7s_ .‡<ç&öbä´jh´ã០Þñ9¡?¢A%×SÕÁàB@H V‚Cy4Å@{hfñ@•þóãd–5„띑JOv.ßƒÌ ]wSêÙÀç¹áÐðá‰`C6aöKX“E©tëjÓtö îÄœqù‰M ´B…˜ 1É(Ž!5ž}‡)CÙÞGô”;[€”É"Xy3”WnJÓN02€º>LUìy¡k+ü &IgÀ‹† hTå9ç‘9˜q .`_ÊÇbÄy,´@}ÆpÝR"V°EŸ§mhCiæÑƒ¥ŽË۴ꪦÏY]àÂÅ×ܱâ“÷¥[_ªÇ&n-àé»tÒ_ŸKÛü4‹„zÈØÞ(¹õSmD!®¶COlÛ‹Ëþ§ñŽÙ_…êü¬(ØmF%]J´£¬h[¼H׸9e¯VÅ 2ì]¼-5ÊË9‹>Œ|+9öÅåØ{“n²ÆH}áx•«çì65(³Òª?Ã5ƒÃü””Ê-š¨š*“šì#ÿ 3Îþ…Ô±úYõì×Áƒ¦˜þ€N¡Eék;ûwr>K™k‰.®’2‘¡€ñ…7hÛ~ Èbu«}õ]ÁÒtÙGל‘´5®ðÄíÊôèž6¼Ÿ§áûí£›‰4q1Ÿq«4 FB.ÔIŸø')ÿÓ$?>áÃ.wWŠº_u0b•´G¼ 7¼ªC„浈âsÄ+CuÐ+ $rþÃK)¾gò¼#P·!1ÍúÀLÜÔÞìéØXlî·Û)­Ñ¤¹5Ñfv!ëôäÌ`Ý'ZîïÚx¸ÄýÈ(¹ÁÊui#«¾ò: À%Ü?¼ *Ýa¿¶™g¿B£óÙuÿ?÷s†ØXV(Ü¥@µñ®†YJ“H±12§@o‘7´í Äü]j µ.0Q<ÄN,ô¸mV+C‘#D„ºz„ .–‡E°~×ûà¿I¾Þ{(d7zãR§½©wUýþ»¢îѵ[jvÞ»% dîBrÓ^ 3${^›5=›ðÐøû`pÞÅ’èòX«‘U’7SÔïO¸ãÆÙƒ$å¸d¥·IZ°PR©ül1Og)¹N}æÓ¾²‹R¼ÿºÑJ­r7ÚÎü}äĦzZ. )jtúUYh6ò½Œ³Èdþ¾I]9NAHššDÅ ³Í•Vù¼fS8äûâ»ÉcöDC2ÜÜŠ$‚Ü79\õa€~ƒ º=d™38G}F#nÀƒBtð嘱(ßq!…t¨3€Åfxu£ÜÂüc^Pe«#YÈȹ÷ÔöÍ×âÌæn}ãØ±×o¸õõc»Ÿíj¼qõÀEƒÑ3=»ûÃáþÝ=ÝüŸÜ7i§!juÛ'¾¨¤»ÜÒ›[wýø™¿·]ú™;>siüsÇNø'zGîï¿ÀïŒl—Ý|—^°My%ø”v±{NTÁ>õBjÓ4´OÅ1¨ÉxÕItà™MGÃð÷6‹ý©93u®Ùd• ‰FÜÓ4ÈãÜûL¢1¨•A‰·SkÖŸ\ î?üÌÅõÅŸ¿ì¶Ûê‡óÖ­;oßãòK›©åý÷­Ýøà¶ïúZVÕo» ¾ûÕg;%§à÷ua„I§Ãï‹NIvVºDÐRÜàiŠ·Z1t;á${Z¬2/~L•n"æŠ`ªzp‚{HïºñW4†ûwvÕAÛŸ¼9÷m$¸éö®Ímž«Ý:¸¯Ð¥ï·Þ<Ñ~Ɇ³«–s9là~‹ùËþæÑìF”EY:ûõ$|愺†*²\€B¾"-AØšÐL@§d[E÷ÀX9¹…ò¦©XW düL ²fî  Ú†bªÛVlêúÆE[ïßUÌÌìšÉM–¦:k­·|8¼aßeÍ;½¨ÞÚ o™â­ñÚÁæ”ÁhWIžmï/m?Ñ¿úÈD}v‡2¯ŸK¨œúQ#Êjç·©D lTúB¸1ËÀjTDh’Îù ¸;ca»³èXw¶#ÂúP{rÄ n 19ùâõ¿}|}~ÍÕCÙ ™g3Qðƒ†‰ÆM× ÛÔ˜ØTÞˆežÍnÈ]³6?ýøo4kúkÖ=yÓŒ[&¿.”âÞT ¼k|ÓmÓ™Ìôm›Æï:8 ‘Tè:¹Ì=sÓ“ë®ù‡Öðïw1ô­$ПÈázÌÓ¸ë”N>“2âs4ŒU ;| ç’·ùFK9l± "ñcˆ…C. ¢ÏÌs¼ç šœœü‚!2EññšˆÞ&™§²[%A 0~”ÎDþ8»ªá…ç‹Ûnyòk[WÞ¾³Hí™¶haïÌTgDnËF´~Êjb%~Ú®7þâû‡ÿûûOíªÛÿÙCÔ¡37¶N6:³«¯þôË;£ýE¿„‚¶Œþz¤·ÿ÷Ϥ¡®²BæÅµkßÊAÓÑS“`Nœá"1ì‡2D‘}:šö³^¼yå|‚9sŒ¼‚;Ãúƒ7ª¾ÕèQ+=¦õžöþñ\í@̯HX('ëJèI‚#HŠJyíÜX¸Óçï GóX­Õ¨¿2 aHE<¿µîóÓ—Mè µ:â»¶á6mлª* QÔT'VÞ&–5B·Ç(7£º7)•7ù(\úVo|¸NÛ~ý&÷ÌoÈ®3G-ÿ| t¾6û&Ýr7¾ÂÝðÆÖ¨Üó ˜xóšú®¯Àߟ%JåfT Gíè\H‚=sT©½µ:p0gEð©k¦h8”Æ»áñ{e®H½**Çç·ø€÷«íÜ6ß<•Xqóóë/ûåäVµE¦±YÔ‘BO¦eªÑeoZ×yZg:NÜ”µrß••6^Õ·ÿéËJÛ÷ÐÐ £&à1GÛWe}µ®}ƸXߨøâ?…ï“Cïƒ:>¸ñÑ:­$>2&Бq•ÀbÐSƒWsDúà!AZÔ¡îCo€ºÉQq 0˜tÏw­o²»šW·f{ëóM#³¨·$·ì=Ô2ýü-+«?¶‰û‰MƒœÎ?ÀgÞçªíKdWµGÍž€Æ ¤57ŒÔÙK—=½¿ïÊ%9¨µfMà“[à;ý ¾S3q¸¬ÍÒ$ Ç¡Ê÷öbXj±c°Ðî >ÔçO:wÂ^I‚Úú´ ÆÃW—Ž.ýSW4‰úýÈ/A½8¤uÕ:‹¿1ÚãÇùªÐ­¥¾Ô6Ƚ8œOùÂeM“7ÿËÊåÇlN­V›J³ÂZ[Î÷n)¹"]ëŠû³wÞ¡²ªãްUA]"ù·|ÛLiß'WïûìÁf”j?£Q(dƈ[í\™©ÊÙX6~´f§ú0/7sP/ @9ÐHô—{Ü€¡k”R‰”BßcM)ä$)”…`FtÖ¥Ò*h$Ñd4À²Ÿºì¢ƒŽ:+1Wä£cÏ|iwžúÕ§l.ð£G·üí¡ÿý¹-OâÑ›/ÛÃî{kßdãô=»šJûî[3|°ýõ Ý|Œãžù|§è‚ÛÓåÈBWÝimvš®†o³ÿì_™_@žH÷œf¡_'žÿ„´ýeô¤ ŠÆpÌÇ _S@º  >*ž?kÞP>L·Ô(†QWrãàHÔ©4•„¢É úÂ~¾æí,>×ÁwWÇã1}£|’{Ê-©ÁO¬A¡U†e¦ðDzlg«uøêGVÞ¸½eö·Èy%@î8°'·ýS{F$Éã*EãúËËüñþ¡Ä¦|föktvöUµ¨&.å¾'yŽn%ÌDgUB­´ÐX+TÔQð^RŸƒ‘ŒdX @íðx¶–>«h(PèÑ43" Ão†Œ?T‰Ü°Ø“É ¾ÔÄÀ7_õ¡w…,.*½oÛfŸ'w—]2–N8dnhí1»j¶I­$ûf¿‹Uô™‘û¯Z_¯7θtrs8çÐl5uÔçqÌÝíÂl£‡ˆM’¥B ¹YÎLâ|Ý¥·g•çù,vAx=¶=Ä[ëø(@…2iøÝ‰S<Ä-(þ½Ðç¼ HÉ$^yåÀMß¾âŠoß4p%y_û¶ÞÐìC½Û(žýú÷•¬E™}eö;°2›Å€”$‘æž§…2;½»»y{ÍRI&M¢ç£@¹Ží|•^pu’ŸàÇmtpÀ0¼ó׳<ÆÅÒÓ0Ÿ$ˆús„^{2QÈ'Z’-‘h$äóËM‰ÐB§¥¥rø¦ç%𥣠`ŒÂŠ:|ªÐlñj4^©#5–_wÝhxÙK»w¿p¬Õ=?8œX>Ð$AÀþÕÙïŒ=Ðâ¬oéN ]=•O-ß§X-×B§ÇP¿éÄÈÞ¡ñÉÇßùÄ£€~n½ÆWmñú>xtÅFr™/з^úÌÅ»ÚU‡xré 8öCñ=æb2šäˤR¾Â§C—>ñ¡Þ§®”¯Òþ—  ú²¹ãlÞ­§æ»õwsï= >~ýË—5»¶¶Ù’zø£¯Fû®&äŸ:Ѷ¶ÙuéÑÁY€³_"aÏþà†~‹&êàœÐµÀbú’¿y¬fjéÅ/!y>·…ð¢:u«‘…– h—#¦•A¦…_u5Ï´ðû©iøØ.'ÊbÔëY6`ð)ÌbæŽ)äU ¾Ùåò,nðà¥ðÇTÝ`e·2æûÃe·ÿù§Üûoþ‰Pº ±ï% nIÓ»^û—¾d¾û"×ÇÕoƒ/¾Èuº£ûöG»PîÂ>î!É«ðýŠ_ì# Å†›ß~û˜*•ß‚¡#¦…®º–šärŽ”ñªÔ†TéÂÖù&Q `©²_Ìç1µj™ØI»ÜÎÇænä^”ìÏ%òÄ~”ýEâ“fäçÄù^/ðK3$úÖXjó¡ƒEÀ»¥qê/Ê‘@SxéÑÄÜ`hÿ…ÃÐwÆQ\Úà ßq°1…ˆj:og€ £Ün¸öÅCõǯ¬ ÷ŸKþßi‚þñB“ůæ·Dn=Ü@zUëê¢ãâK®¡³ˆU®ýfÆYÇ9 à~«‘ÓL©û¯‡;a¹…¼Dd„é˽ù¦~ hç[1¤dF¦h5!QÜYŒ|ÐÆJ;áó8&ƒV­R*ä2FÈaöUGÔ¡Rå»jôžÔÓÞ H%´Ü/t‰p' i¥[Àc.D~ÃÝŽ7&gïLp˜Áo¬8p>ûÓ(xÕmýE­Å\âÖpËý1ÀŸ¹×((æ =Ÿ¹Lá A Ö0¸MÝQÑ-zBodF .„4 ÿÈÉCªä_m³_$·¥ƒ÷ÏöZÈÞÙ×ÕÁõ¢_ý;¤/ ßú n7ð±aª–ï€l×YnsBÇ!yáÙ$ˆGùTy… ¤É´ÔŽüVøFŠÁÈQïÒªj^_ŽòØe¸„œ6&ê…e(ZHšŸ²3}\úi~ÿuÞv¿®÷#¸žá!Tº~âõð©B¥x‡¾|àzrxöóøDCö*²û.É?ξ-YFŸ"|ðé¾Í«­ È$á)—Ù-Gð…ÕW9%FSÂ<’­PðÙÐ-| -B³åqÌxdhÿŽÆÑñó Äæ3]ÁŠÖüñ¨?ȱä°:¥%A‰å÷ómçîMÂJuÑÿ’ßÎýîw­÷ÍLܼµX:øè¶ìp{ƒM%ñؽ<:¼;ßÜØ0U¸ê†0î{­ý­—¿pÙÞ/ë×û2®²Ã$iç¾ÓTÏíÌ„üý‡&Æ÷w8ù3¿#¶*H[‘$^hKšôzH†¶@Œi[uEÂThëŸëî*O,èý©1©„ÄqZ‡¨ÉÏ?ÒJaM¾Ô tÒ¡æ‡x ÂÞ†º‚úIWÅ®Må†DŸ/&çBZ¡f¿Yüø†áëÖš÷Ü»>Õל·*i·D{6$“ùš‘¢Çží¤O=Ë]ÓÜ×qÕW.ÛûÒÉaT¢XÇõg‘å‡'Æöù >ièhø‚€Ì€Šidd£%ÿ ¹è2TŠ"5]Bgá`ÇÒ{ 3èôÍDŠ„<Ï +²†| ï# ou‡ØN¡yO– Égé!õ" Ô»ê?€û¥¦Û7ßPײ÷ éþÖ¢ždû6'£S…üx“ב롽lšý~5ýÒÎV~½éP…~€hƒô»…ÞOt€>8­)¹, w˜'[8ÐEÕb­܃RE³(¸·.ÈÀW¹\6†šX!凵…¥:-C(­ù³Š¯‚¹ùü«2¸ŽT&ŸZz2 ©B.ù°e¨áK¯‚`‰´m¥ÖæH4ˆPÌ L3c~‰\³ÅeóÀúp u J³ÑÌ´×”Ö¶xü¥É†bw&huêûbÓÛvÖ>z¤·|ä…Kƒío÷¾‘䪜SæÓê"Î|˜Ú ¦4¬ÙaNŽê¼t4áô9-ë²¥°¶ëº—/ßýâ C2Yoë¡5 Ón·AÙ^‚²¨tömè¯"lp(k-€¡Ð6a¤(J!}}(h-t”d‚Eh{Ž\[5àkÆÁFPå€ÃÚkö ßžt$Z9:Þ4ø–)‡Z …«PÊï¦NY|´¹é[E!²|jö›`&ДK™*"¤‹>õþ&‹e²c¬óš¯\¾÷+'‡k`ôƒ×µÎ˜<ŸE{`|Ëøî“H諈,´à×”•u>èeñqŸþìBðNÄBOS}æ@™ Â}±·ÁÜÝɲ¼­”Šg~ZŸ˜‡²Ç*©ÜL•ë1ˆ®>Ï5_•¸=l×øT¤XS;¾kß®ñÚ¶#Ï\´ÿÙ£÷=éÌ»S«OL…ÖmÚ½5TOLJ[‚öº±b~¼Ù÷¯™5°»£&_ì]}à¶5›Ÿ<ÜÝuøÉ-÷ü¢d­m]–-®ë 3J­œº§sf¬Í©oX¾«§qÛ²T¨k>»¿–»–¼š¾…ÈŸPÒ´Ð šAÁ_ hsT®H…+“|©«OÀGAùÒˆ(h%i;ïOCyh£Å­ë†×š¡W-5O\b$R 7§” ¥çwŸÂ ±¦PÈ„Ìê ççm+ éPrA8¡®po·+x<·îø¸¿ÑÃIG¤9fúý/c+N¬Ë/O;enVãr–[©°†v;·uMœ˜ÎËåWºtúDo¡å¹3ÿ9q˶Fƒq[¯œ@=s}›QÂ}!¾Ç‡‘4 ÃPákøÅÿ0Oó`\OäÇXøØ¹ziBLÑ{þAV‰¨žxõÇ%èÓ…!5—ƒ>‹º³·J°0CÍ"D$2D¬Ì”@èKbå_5Žšs‹}5„ c\ñØš‘#ã‰þÞÞ~.³áîíÛïÞjÜ’¢OÍþª¶Ü°ùä(÷ÎÆÕ«7’—ÌÞ0vãæÔOh&ÖåR³µÈ&:̽$QQ#¬DXQV¡<—ÓB‘Ø#CÉ­ =30G¡cI–oØ‹~D·„—îL–u(c Úƒ(ñ1l¥^ˆÔ­DÊÐÓãðÚaÔûÀäîž*,»z}¡¸éšÛïé}³s&µò¶íM¹—è€Ù²Êß5g×ܰjí§OX?ààÞ“ì.ÖÍþ<¼vÿñå3Ó<–´½¿C_O¤ˆËyNWC«ø"⃠?ÙN_o’1±ZZ³%q— (Œ¥ jÔ%4† û  U¥=¡ŸJª“àdØ:ǶG¬6õóÚˆëaCf£G èß4ÚŸW9ly.2~íššÁ”Sæ4hl¶äNÄîv³_£X8O\dÑp-bâ–™FƒiË | ÑÁ~öoÔÿ@:äˆkˬ úD¹š¨Ål„Œ‡|âacÅŒN¼¯I²’Ú$H ×ünùœ®¾™xÞýÉÉç“D( &C}$M%€(y= Þ"5¸ÊdãÑΩpç…%ˆHïøŒJnvÇ\}j_},4ÔÛlÔ&ÿÔ?µùB±T ö4™6é¥*ý-R¿û—`Æð¸DÚv‡Í²5¿]£Kôë[ï(ÍjÒ™MŸ>½¼ŒdS©g38©ƒM-¼‡,UC½M„‰±¥¬rCÍ‘R*(ˆÝc\sIB–ÅIBŽsÜ· ÷¡¢Fj2‘B´ŒFù<¢€˜GdaÏ“FÄòÉ‘F3]Ã1†€Ûž1SùÍwnÚôÐÁrùàC›Fï)åõíêöŸùJºÁ«4ôÖ­[¹’{‰:l˹,Ííé:¾«ClS×XW,lúØ*®ª‡Þò¶xNh!ƒü ´ç!„ˆ:Ô“ }qƒT@Ý‘RF€Ç+P|.¸*úš•42 Câð‹Å&ù3ü!fÉ/ÿ!£­âèr‚Ð(Ï!àÜóóŒ†RS §“áºH/6DQœsA¹Üw =“e¾‘.¯òÆö¤¸øäÉ µcY§Ì¥×z]7ü`ž£Díš³ò›òÜzš*UíÀ¿.ð—Ñí<ä·VbWY2Ñ^Èk¨ä>¸»½uª#ò“ÞDÁbP³kê»âËv–KSí© ;”ö›"ƒÕe5²ÓHFY¸GÈïÑ*¢ô ý4¥Êã¬|.hê™L:¹Ä&íJ±Ï;úp:{ÌVªJi4oë…ÌËáyóJRùÉçW®_j ôzä„L>³p*!—‹31&«:— … ßÆCè”’3‘ðÑm½¡® ð¹äøGÌ®6ÃÀÿ²!cÚ¶àf£ëÁ‚ÏÝüKGö˜R'•”Û5™®u-Ùþ°‘ökµ›Å¯áþjw€[©ÛzÎd¯s8ë`C¤§Þ§ÓovTÖzwtpþõaAùÏ \B…"Q`Il‡c 5ÓiBçü,«Ãõ¾‚7C…ßFÊÃp—@`(p—OêÕ‡¯úÓ'Áݳ=:öSßxeÆ™c_þÌŸ_ÙÎæŒê¹'¨ŸÂßB9}1h!ÙñÉÊí¸-;:³ØZ¯S@÷,‡0R)ª©JÙY"ǧëÖP³ïƒ µþ;ƒ»»}ÞÒ:Öœ²YÜÊ&ßð†]7¼þ±Þš­÷íà^7º¨·^á>£ó—ûÇj^i²› ì´=åÓ÷^þàÔÀÑéV%È™ëüðùk¸G©øüõáÛ;œPì9V¾7ʣ›€jÀÐà\¾BgÞ:7® W::õÐϤ|«Ù„qEÍ‘;ÎH…vuóÞÁã£üAàG¢"Nó÷wwy_ýz®T·ÖG#Z­O汦R9×àáU™Çîß»mbå³zí?¹8ùwê2ÑðYe¶w]ñΧ,ŒÎ`Vº|Jƒk?tÄiO˺ҡ±Ê¾R±4lòj¾ÿ ú޾³ÿ ¢ D{¹”F¹J)@Ñœ«tÎ$%muÖ‘©6ŠDsBÊQHı®¤…ùŒ£J¾‘ª¼ìÿ4#Ƶ7¬Iuíûø@ͦõ«c}j“ÜdÔ¥ëÛ‚‰rÔ¸e"8ê¾ãb³Ñ¶(ÉW©›r­O‘drhWyÕÕã1}¬=»\+×kÝV-B¯îêQ«¢`\cq¨÷ñ6`+´K¾íà qø´õBjß,\ûfYTû Ä¢H&šq;ÍFV«”až_ûFñfJfÎS$"*|\wÿEeZmaÉ1&Ö¾º¾~yKÖâf{c›wîÉo}æºe퇟>°a7Ë­±Ôôk[<¾Ó2Ë{·wùì^»ym¶ÖvÿÖ5[ž½~èù½ÛƒMBröMîÉãÔ;Ð6ë‡;r PCçÈAIÉ'%¤§¦¡ðJQ ä»å¨‡¡&$ê)V c•Œd”³š—+ …à ë€R‰#–¼u€äk뢠Y¢aˆAhð*šó¯RFBDHÅÌâ'BÅhT«iƒ„žgréE ¶JŒŽ.(—F׌®Y51<40²l¤»³Ô_îom)Öçs5ÙD<àƒÛ’EØÁ‘<ðC;°Ùmñ!o±"‚ód ”˜ün@*jwèsƒPš†¥‘;í˲ùRgï躶¬ë:’­õ劶‹N­éj±[7¹öª™ÙΛSGO­Ûû䥽ª=dÏÖ-[àÿ·±~K6Þªñ»2Zc[!9ê¿d,e·®«Xk­«ÿ’ñ”JaÔšŒ Zã(¬(?’nôš†’äÝ#ð­'f3á°'õ:kV34ÃhCBÀð}„º îý ’áHΙK:¯×èdMÑ@$…AÓª’H«›äú¨q—‘ûÕ–;6dºŽûÚŽ½k=MJ·ÅšlllßÞq·míyE¯-œƒ²BAVgæ¾§j]´ê¡#}z_Ê>Í <÷—×4ÖŒ5ú–½:0ñ Æ(qÒ5ô)¢†h'†ˆO•]:À€ºI#ˆ,Ú „î4)“&¤DÆ;?MP d€£>Å$Ž' l$ǻلpö‡S>|‚µzÂdÙÒÙ‘Ïõ÷v u56äÚóíQä¶)Ð_™†Î³N ¸Ç³€ÕªÈÁ˜’ i⥀|«ùî­ÃǦ¡7wßôȾ¼ÁºåôÖvto ºÙ¨ÐQtk¬Êÿí™NB×.7RôÚkºÈƒö”™Íº=>Ïõ£•(vœ”¼ÿMµE©cCM‘ðp)šŠçJK!=ެ¬ÙW¬_ÈTÂÛܬ‡¼6K€—³(Îÿ+úÎM[W–WrÓxг$0.*Þu[pÛ§èvT]CHÐÿæ´}3Þ³._¿m8S¾ô³»vöÒrzhk}~MOÌhû¢Ë,gäÞ†e‰`)m—2rsú>“á¿B¥±L¤=ë "‘¾Æ€ä¹®ícnïÀ–c«6ú¢––=÷®]qõ†>§oÍe™Õ…„×Ò`/ŒuÔÙ¬ucy{ƒÅ—Jì8óT놡²ËQ\ÛXZÕ`³Æ1v÷0e†{Ñ/Ô0B5Å×âZÅZÜ$F͘³í ipAø\5®Ïþ¤ öžYj¨X‹Tî …L¬ ÕãÆ*ÛTè̲ȅ"‹³gu,¸‘ÛCžÖk¸ƒàK®öíË¢ý)£Ì«ÑøÍA?2ÉõØVœ}üǼ«´Õ­W6wãz{î!’ƒ{ÔK$QêtB#æ‡#Æ^À§îå‡á0Ê… tú} û’þ$œë†pUÜ0usû‰Üʆ)°Õ[ÜzÆà–°7Nw]7%U1lÌ?dÎX¡}}­3oÑ|H>õþ×56U((°ý2…Qø>Ôå-Þ¼Š¦ß·šôñdõ |—+¸‡è1hOÔÄMe+$D”•7¬)IJ™˜Ÿ¤¤´êB颌j¡[F&ƒºCÌñÂÕ9±¹qØ^®œô.;Y6¢ÔÒÐQì€Q_›‹Æ">:)Ÿ—cz^* auºŠXW ø4ënï_‘=º" ÚûêG ßlŵ]WNÒ*RnМ‚”YÞ¼-MþÉž±°5_ÀK½e¶ô»s# ÂÏþ^m…>Ž@É~¹Q²6Ôé-Þ² ¼5±.Ÿš-,3²ñ ÇŽ$I˜ oœ¡UD¾Ì‘²ºœ¤0’ i‘´T"$K Ô(^ŠÅ£´,t@#²¸TZ9ýÃ0X8véa“e]*™¯IÖ§ê£a– 妄¦*ª€\ײ™Íu¡*²ýÄn_gtÞ‹â <ÂpÁŽDðTÇå«LˆÛâ`Ÿ-eb3.·Ï}òyKUl¡¨²(µ¬H®¾ "VßQ=›ñô¡\eÖÁ½ƒr.‡ˆ• f)%ÛÛj²F¦Hv“ÏÇ@QGhe!M¶‡êII¢¨" ”PK^
«áŽ´O÷5Ô5FL^Ò'µ,Ó´¢ ’¦<^¼oWs‡ÈÏP—!íay}ïp{‚sgªØRL9Ž÷ ÿa+õG‡b‹VÔ“²‰|(×s¦ê\«•p Ê×äÀ µ&6½çÇ8¨ÖÔòý\ÏEËÛ¶Õ 7g,n¶Ï;µëÒÖu÷ï+‘{¯Ü1bL(íSS;q,#Ü’°Ôm¼ùÜS’¯QCÖ#ñOá´^¤uHFŠb2~@Sø^%\­ä5"ÑBÞªÎÚL|¾õ\êˆý\#­ Gæp%Ÿ`WT¬µ¥G—Sp!Ü™sO æÆ# §K§ê ©Rº*>ð -]Mórºù8bd4#ÿË*ä'Þ;(¬jÆg½/ž°£h¢ÔdM¦²®¡‹B?6æ“fä%.oDÒë’–>QŸêÜq{Îb>Ct7A¦ÙªÐÊéâÔ¡ÖÕ IõB*RIy"ȳ?ãž—Èàž°AîYñ¼ÓB24¬a+ž•I‘kN¯¦ø<ð ²ËhB6„HÁ¢û“e¥EÀ5:¹5AåVð¡w*@›q—Ë@øøÚ_º}²6n•E,ߺü%îv¥?ô•ßúäP­Ì lü:EÿÜ©ÿÜçŒöŸ?|æ»|‚4|²PtÕ;ãq¡Ovœþ)€–ÀƲñ“W§òΘÅE€¡¸ ’Á(Éäœ#¨@#W £¢Òe•wÂp‡£>Ô2™©þ¸&Ö¸ òÞ ›ò9p"YhÞÔI\ÔÕ}ñhrŸÅ¦aÝhÃ׬¾f4"ñû¼žr;u«ÅH;÷¦3S׎pçSJ¸ÝM†Æ™[&$W5·–š/çõÂ{—.5D3 ÿ‘„в~&yeÑI€hä;›ˆ¥pFÉPËZ’!TÝŒƒ×¸Ú û”˜€–7ù¤H ãýJÏÏÑØšñ`KÖ/×Ö×\©>ø‰•kîÞÙöç~ç~—šŒ€t8sæ ½Lrw‘ß/^Ô¶f9\ëN;eæ ßPS¿ùc]—Ö}1ɽ©Ñ&ƒ·ŽX °ñ‹"æÀŸÈ¿âs×ýeE ^“ÐCÎ×Q$«g¾:î²#žRH$¸°ˆOÌU^ÈP+)n¸ªñÂWµ^øªÎ _Õ-Ôž0;ÄñçXõÃGá¼cÞ“‡“p£{\€Vݸ®Zà pÄØÅøñ£M]MQ„ÓÞ´~Üݧw™ãÙæ¨/íRG[º›£}Fçñ‚<}ŒúÙsñX]2Õ±¡èº u¦hcx­ÅáqX¢uÞ|9›*¯ëÏB·Ê ¤¨Wåqî„$ }&‘)'¡Î’óIý2 AÂEB£Š} ô§a: zVøÂp4øXœqŽþA½ ðŸã`ŒãÜgÑ¿÷üé)ªL®}èëßøÆìíäîo|×D@¹w1üN´PjžgãÍ%Øñ)q(cn.‰ˆ¥„ŠÑ0-J‡ Z*@ØWp…+V^2ÿ\ÔûÀ¿ööÖÄéS³¿Î·âì·®;ýä¥ëj³5¸o%|&;|¦z&  qKõ3ŸB‡ÈB"?Rˆ±!_ÀB Ýq~º—Xk#œñ}†w 4¯ta÷ïßáÓ¼>ýlÌý©~WLó‚ÊnÏsô©3¯-ÈîúÿÌ& ŸäUè±ò<ƒ:Ĉú+ãÚ¾cAé kbF¡«vUÕr¶è5ÜVðݨ÷þÙ@ûçn=ùuÞþyœ+¡ß*š<$Q„2ðÔ;DŒ(ýeEÚãP ,;±ñ7Kˆ‰¡À…ǤÐV–âuhøÖæbÑHȨ—htÆ¥m‘4¨ÎʵÌkSAñ_ãq‘T!Ç`'16ÌVa]G*Eã&ò;ÿóÁáÜš+Ûå”\¯héuuìÊ®:2¸ù‘ƒåeŸ~¿qto§;ØÐéM4hûŽ¿t`Õ=WoòmÖz]Iâ“;Ú¶ç•à÷“W„®ýü¶£ß>Ùÿª¢0~ ¿{ß`¬p›¸g$Eú.¢8YÖêà& ƒ†0Ëä¦ ùǪÒår1¿Y©Qh¢`¯X ç@<Z‚2jÎ7‡˜›2YF­Ãˆ†ú:S0a ¡YeEE"ó²šqÊ"•5ª…Ò¾ÙJ]=”‹ìCƒ®àÍÑÁ}½ö¤Ñcª‘ëì¿]çuéLj9ÍJÞõuë·´·/ã2œ÷Y‘’Èn0ª»Göž +ÞÉé…oÀ®39——j7ƨð™Ÿa^0A|ÒÉMdˆòŸ“+ ­{D.gˆë€¬úʤxª+…\#%xªÈe$¦ M‹tQ` ,ý„8¥,áQ‘”KL)§á(ø2h×Uÿ–sM@UM‘I I_ÆÍb’+Mç#9³ ,@Ñ_LlŽ{œÿ߯=­õÐíj…\ÝI»µO,MhDUÉ6 •˶Ìv›LŽil…4~Ò8H¤‰/ž¶áÞLOá£>¥ Åk4rÜ¿\ü ¾$5"hk‹ˆ‰¼=ÓP‡Ÿ"Sò]<¡·P7nN)ÎZ<’Ï" ¥ÃiÛj1›ŒP_«”2†‚ ŠoÖ"ô&×P"ýUÔ¥+½ËI}¢%éÕ>s‰+hXÕЛ`Écw57é ÞS»öjÄâš«¶»Âìwè»ÝøGa%Њ „TH‹‰»üÈDRÇ÷Y¯ðêØWk‰Ÿ ´¯‘OáORñ§I~@Lº‘2ÄuˆTÒ…\ªœãRT(’_03ú«tLÅ+ò%¦–Ó•Y*´M¤ÃU“—˜Ém‡|Jd3àAO­·vŽgÕʳ‹ºy/É´à?ÆöwºžQØR@Ò®xÆÕ¹ÿÜ\K_µ¨ù·„ðBzÿ£DøkD#1Dü®¬Ô̘7ñ4p›*dë /©REÕ&U¹\i¦P¨ªI ž#7Úõh¾lÇGŸŽ)ªg‘p ¡¯YRN¨ç(ïv¹b ¯­½þ„+ì ù½È.DÔ˜µÕÔ¯[‚ø¸¾ÅÒv§§üü™Wàä=·ØF1!'8îóTùÇÆ´÷ÄNm$ö‚Ñ—²Ù“~ƒÁŸ´ÛR>#÷œÑŸ¶Y“>ƒÁ—´ÚÒ~£øµN==ÿ[1Êoô/ØÜq{Òk0xÑü€Ñ@ëÍûÙ Fîæ Ü3£àÓ¼Ð6²:HŸ6øÍ0W¶0e¡ÑºG/ºW6Â?$•[kRPi¾Å“Ÿ-k€JEŒ0‰H®Óª•©TSTc„BÁ*Ä–r ØNç'q> u×ÂYlµ¹ ÙHõ×  ¸Ss«\ÀÜr!—ʤrÙÌ…N'æfcß Õ£#ˇ‡— ôwu”Z°i`Š„Ììbë@ª¡øš'ßG²п§z]ÁrëŽOø= PíêpsÌ ÖGvwÛ&±&Dz 'ë÷°f 6#¼ê6lëêèá¾Èq¢‚ËOÜ0WÈ®Ôè}¢§.qa¦…`^@*A¥ÌüÛa}`˜ç]ÙŠ#Ur™›´È}+ÓeÀÈɪ[eüC&Þ¡ø;"ï% #a4(ªQÁZ%£PÍ na8¢’È“Ñ*)Нk9O˜ƒKÌÅ)ÄÍ÷cÞ™›/Öµò“ÉóÌ-çÄihÁhhfjn¥'!ŽqD_oOWS‘·%ó5<¯ +ߚ𱋕0æ,‰ÎÁ8`a× ºp®Ç<9¦ÉÍ †‰ˆ ÓƒEX6LrßӹéV¥R®nfÜÚÇ_ü³ø±/]èCÐ4b-v«Lν-ètæUÈD'häs±•VPaúI!þ4ÉÈ)ðçÖ©ùÏ¥èKŠ?ñ´T ´TÍÿø­p3Î %þ/VA,P„«Tzš¬^<Ïå‚‘ÓÌ0ÇKhùÔÜbKÏAlàinÂW:›;[±!Ÿƒ¦®)ij{VË&}X›-°à¨ŠÅFrŸã¸SÜ€+xS~ý +üMšL;#­ 3ØùÔc‰æ„Gûð%®€a°¦=Â’«VF£ê‹g\!öÅsp€‹å¿ÿ©§±…Ç&H­¿Gm Ú‰Ä2Ê{d µÚ(, µÏ¢Š™»EWná«’W#!û¶Ù÷ V½ŽP­F5]mìè€F£gV«šÿIôUL¡×Pï@K•væ#¯ž¼{þK›O´²º;áj’y«-2¦.h-Äòíóׂ3àTB«Y³ÔªçX©\^ð@¸-9µÔbK/ÁÇCEÈÿÛ¶¬éë)¶ÛZ›íãÿå¨þÔ‡þaoЇ¿oÌzoÕF"'þäœùŸôA;ü›ÍÆ›ôðOŸá#n›ä'è¹g$×Ð*¢vò6²&(9‚D¢PûFTcUuA‰3X¾J¦ê@±ù4h´‘U0¨éA‡RE 'Z,OOj*=_Ì$Î3Ñ'ÊäÐßf_àDô*…sMTá–H˜q²`r¹yÞ< %C(Ï·¡TŠÓ1?ºÑ܈’à3©HH쫬VIiHç’Z› Úîó ¸WßU™ý’Î9“}œÛòë+%]×õ”7uV]w¡óò>ýœÿ¹*3½ºÄ«R‰‡Žu¸×ª {>†üÈBþX îæ‰ªm”:ë çdÞ¶Â!Yä©: YCQê¨ÙÔÚ¹ UBŽëzGÝÁêóI½”_b8ßt?XP©U;*‹|„éèõšÎ7]‡?;Z„G¶¨uÁåö³õwX†`?l‚eçñOã£ýÈèho+ëks©D4ð{\U¼´¬4œ‡—ÄË‹Ù+<Ÿ»ñóâMp›¢ƒÃ¨•5Ë ×÷^{’»]0èäJµl¨p¼÷š“kŒ®{pÙçuð¿ä½ìyÏùÕVó¡X‹ˆs6|!+’ÄŽ³¢ž¤ÞFçGà×e 4)ãÅåx•%$$ê(‡àFH‰”D-Ïb èª äxÐ2å·Ò¢:ÿ(ë?âúÖ¸¾ó#®ïþˆë{?ÒúÐ!_0T†ƒoh‚f27¼’bhO@>Ϥã…D!èw9¬fêP1n`XóAJMKHH”[m™P¿F”¦P÷²‰ByÇ`¼xüW/¨ï¯¯qÛCÊôÊ×/[yÃt¼Þ¹9»ñÚa_õ–ÉÛîËùõþέ »'›$…éÕvÝ¡Y>TÜvó¹ÿT>ÿ€Ä©u7ççj/¾Oýpâ)þ\Ck44*¬ø UuEèégÃ0baÙ ’OÑÉT—*춪Aø€¶ú>ƒÓ‰+he¸û@Ùìv"re܇ 'i:S†ñç§W›& S¹àL©Ö:”Ëd9Åe$^ÎCg“—<‹€1펞}[¢†´CgM?°òPR?¤-±’Æ´«ºBÑ•Zn§ØÕD  æ«*p7ûyÇX+pÞJwÍhè­‚¬4yX8„Ç‚Ó1Ê„<oáÅF#>Œ7™æœ@pGï³ú•µ®ÔŸ^ïžæÂôuÃùÉeín3í³C~U8±¡'Ñ™µ{GP:kÎwë†O~eïå_¹¦ÓiðMZ,Tm(ÃõÕ}}'Æ/Tð¯‡ò±èÜ`¯€Ã‘N‘RFÏãp n‘SEØH¾çÆù<°m%t€CòX0gö%â¹l¼1Ñè …"‹¡<*h‘Í£‚vøÄczˆ’ÄyP=e†²%u[œØ^V òÙ8˜Ã Š ¤Ä|LRK5&©ã†!ÉûhCà$·Ä¼¬àðâjY”v²Ai6|±b¼n¨Ö¶{}®)êC9¥Ñ©éÙñ»/êh¼è‰=;>Uþ™Vgq˜“c‡úï}Äásð²]×¾|ùî/Ý0Ô×ÅËÏæ³MÔeðë Ï~EY_=o `èFÊ ª$@ÁTЀH”GDÒS",¥‚Ig¢„æÅ6^–u õ­Íõí íµ¹†XÀz5Ue>›-ó_™©”ðÌõD w|% @ $[Rïõº,õC©Òê&glôð˜ÚªR*$ÑØ&F#kÝÒi¹ä_.ÞýHûÏ5v«M%ìqR2 ë÷Ô“‡‚­¡òŠ\qs€æ·Ö6mK;Wº}%‚øëëšý6Ø’.ÄAœ›û°äeŒKÖHœ.Ða'Ü¡|® 'àŠÄõ]‹S_åà&Ï7ú|™µš Ȭ-ëbÑ\6ÚkŒø†Å‰µ¸„åÓjßhüøt¥`_Š‹-L§ þûtiKuyÊûÍÝÍJ½{+*Î/®9Ôú½ÅʉDš´Ž—›H7aáèÂÑ ¤$&2Ôä ŽZzÁUo=P%ÇTôƒ•t•µ¤®€ˆD–*ã˃Dk Ë7”}žqѾͩèšB~¼ÙëÈ÷@£À×1W>Bb{•äûà?_l¬§Øó±´¨*µ‚ JhV"`n ÉH¦æp,óú;]ØH\q…±RI;5Ì®`€y •|Êz± ±‘Å—à{Ÿ>ܾìºg¶æ÷ìÜëÕ»,Ù–åõõ«ÛcÌìS¬EM—/º2ß_c³»©·‡Ž=»åÚoïÔ…JÙu(Ò×¹£Wn¹v=~qËé@óPbÛ~„M-ÒÁ¯øE+¦>©sø´íÿo,…X”ÉD4Ëz\“^§R€Áë$Ñ÷F>S1@ü™ç_»~!Ÿ—$AS£Ò¼;éÝ1n‘ù7éꛓ¢ %à—Àm@ˆ…†BƒcîÈ*ð<É +šÓ ­Î?혎v@´t ÔÕfRɸӎ)m MQ•ÔÛ(‹ÊÑ‚òÜ\€ooSíŽHÍÊD  ÝxcÛ=[s´ÆÄ’¬c×`®¯!mv³½Ñé­Û²]WìšpþÈ$…ƆÓl5+t^¹Õ6R_ËâLëÁÇölß̦xYÛà–.Ö¬U2_ª€Oº·uf4]Áu³Jƒ»_cO”zºlcR<|›ü&_9ïásÏþ†ß°ˆz1¡ ˜é¹ ^Ìó@ë á@Мߋ™[13ÕgÒ°•9JžÑØX?Û™\>µr÷î+G‡ ݹ¨Å¥ï‹­ß4¾m_ǪîpÁ¯Ío¹kÓþ(ƒYm´›²ù–GÊ ƒ< nkHÜ×7²ßkÈ[Üõ[R]õø=]gÿ&Y ßsŒø‰€1Ó dŠ1 —-R¹R4lu€d¢Ð½Bn]0, T„×r™T††%$ äá2Ð:žÂµF‚œû1¦ôœÒR™¤¨ŒkÑÄùæO–µËú»:ÅÃé/ЉÏÁvÕàuˆZr“4æ¶ÕKë©«¤ Ã:Lf³B yϺ¼¾4’5<†ªï;wŸè ®\³&Ú%[(Õœîéì[±eËàPW{¢)áÕê­šÆÂ½ûWX—;ÅÔ ذµÇUÒ¦šR½û£Ú ’yðË 66”n©Í6Á/ª²l¨ïJl¬/’$¶ ߦÞßÍF4+N#¼tQÞ¹Psi 2ì*À óìÎsíï/D£á`”¯ìÂÅqLd%zÏ#ùÎCwæ5¨ß Æ"+ßøêõß;ÙÓsâ[WïúÄtjö½Û{ºÙX[vÙLÙélÝÜ—ëNµþúèíäÿq6Õgt¾#«n¿S„LÌ m~¡¡Æ [~ÉÐÐåãIS0çrd‚FdspH~ mŽ21Hü;Ïf–´9¬@Nvu’29€¬ëB6…wè¥îüY‹x rØLµa!— –JÄ´Qãd¼™m¨¨X±.„é+ãÃæN–Ýím¸.t°}°¥©6—ˆE×٨RePV¡€$ꇳ”¹Áw`²Ù&Ñþ@Âügyºìu5µ/K­¹c[=¶>.Ê„,wÕ¤’6L.•èºæKÛ÷­p5)ÝVK´Ð·½m:YSØvdçcáÒxf¹7çc»®~áÀöçåâ!ØZ®UðÀ œYsïEe£?ÀO¬n+8ÒX÷¥"Ÿ8uêcP¦tž}—ú=õ?DŠh+·:Û €¶ ÙÜ•L >&#î^@„ü6‹V­3"RR¾s¥µùxówŽ^ûr©GWð8óµEûÎhjx×óõu6ÃÜxCnð†wíyñøÀPq¯MmÔ›Õ’ÄðžŽCËãH¢sPoPoQ¸¿’åXÌmTUúY Éõ¨Ž 5Í×8íPFA”Öb)ôˆ%`áñÖÌséôPb¡C ÙÞ¼ ‰€ÿjßPö@ÚRCr÷½iÏ)>«”Òræi¹Ïþ&wUcq±ROyc»J¯gTµÙbÒq$ú6Ö;óÅ®Ìn@~Ëa䦭f•E>epÌ6»2]Åœ³¸±?žvÄúœ:U“ˆ…z¨î?Ïþ•â¨ÿ%Z }’/g[ë‚ ’Ðb hšB=ÁœXíîliJ'=.«9ÈèTJ× ” Ïã¥i¥ºWs*0  ·†_Ù,¼1‘ÍÔ •r–í*“\fTϘó¥Á\&k!>êgj ;ƒÝÛ:Ú·u}¡†Gu¾XÎ"Uv£1î²Æ,êㆀM«ó¤Ý׳A‹%Èó¤=:­-`˜’=æŸ6ÇêÖ÷Æb]SùÌöüÀ/Í£œ«‹¸zwÄ㵤ü9Íî%ê5øMÄxYö´ #‹­Q%¤€Æ'´xkdÞ©à¡–U|sÍkàû¬áw®G½EþeÁÌÐPqkŠãŠ×O!ähê­ÙV¬Ë'Éï¾ÿ ðhm áC“—ðÏwþ‚·0NìaËÑ 7ŠU°ÚªèœÑ ­’¹ ‚|s2 ú8YúP"d½ hã"í ÿ@æÞÂ1<‹!`òKÍ•0Ð\ÁÏXʇ lƒ_sñÕÕáž©£<öì T„ ¯á&…{½kêW‹^BÚ€”ª2iHpO x‘¬¾Èû“ØI$qB8_×$à’XpNâB0š<¤´õ¡çCË¥ÁmÉ©…ÓæÓèø…/ƒLRXù…TpU(5¿µ%Hwte²….veSI+Ò{2Žé8JŸhxÉc‰B£äå1ù€&N¢Д”>E˜‰êËmBñíPÆR¸,öð4•îebmAø½œcŽVzx!ŒøOuôBV«P‘BÎ<ð×Ò¾k0áoÉÄÛí§²™Þm‰DèS³#ŸŸý:ÙâiZÑ®w™ ໳7:]•Y¯ôð|}÷0ù®ƒë8­¬êÍâ¯7¿A´UlÍÙªÎЧC>?*ßAE_ ì»ë¸“nhv¹dÎìxíÆ“«ãô©÷÷VUšÂ¯„öÄퟫ»Ü¡ƒÚܹ‰l'¤@ò€\ÃáHW w·šã‹h ‚šUœ4Þ ¸ŽˆÅ€EÔœ$\@ ~“êm®ãÈߺٷŸz¸|Å—·Î¬ìA*Ùb‰å;ókK~[ÝDó'ÀõÆ( –y‹>ÅuÎø îUç—ožzäŠ9²`ûºÆìpƒ—äé»÷ìÛ åZœX]V"¹fÑ’<м˜eRšâ ìPË: ŠåãÚ1»x‡‡±nGQ-Ô•ãDÜoG|†®¾›“nb¨$ÁÿÅPùøeYê Ës«ÂFñq4¶$§/w[{'ÖgŸ8Í}*XJ;œOÇÉœñX,]ά߸i¬¦+iü×çfþs–tG Dø^Üóø½®~/£ŠœØöê÷âŸ|Nf/qsNlkyìøn!vÑ»±ç1ÎV×Ú=÷K½ÿ#ð¼=å3œë®âa>u‰øªÐsBÃ Ñ ¤LPü Yõþ”5zùX" ðÑ¢BN¢,êŠEk«€É Ý E¾xô€¥Æ¢ó)-E¾Ö9†‹& ·EB{µD”¦ ”ˆ'”ÖêÓœJˆxIQˆí¶9urÏÅöÔúÃnkÏŠõÙñk'Ó¡eÐpÍ¥SV3ã×hcŽLâSÁÖŒƒ[¾,¿*ŒNyºÙ€qËXMwÒ”ßpÓäbÁø¬%Õ‘D:gz,ŸMa ‰“PÏü Ò;E<#èT?€oݽ¨S«.Pj»p“ZD2­Ð@wòÁRûÄzP{d‰(<ÃP ¼-5ùeÂP¡)EŠHE¾H$ 3%|"É– ê<”-ò?±•œS+^¤VR1M«Ü?–CjGÐÆ¾ù„“ÈÌ–>wœu‚—)G¸‡%?Àt»·¬¢‘H†FÈbûh* ÎçÞ»¢vEácð­8æcD H†NÏÕ¶Ã&©&PÈÇ—)W“aQÀ\¤ß‘j:L$âI1LžJe]Ç‘úEL6Gˆ ó#ãäIAïâr(Rþˆ1=e§Ž²–·§áãS|› Öâ³¢‚Urç–#H¨ $O¤1Þ×™Y¼ÎüäJgØÀÕ’ÔO®¢Þ¢äÕìwKõà7g<3™v5X8_—J¡Ø§D ¿‡°>b†^hÂS$KÊ¡WÏ`S^úÿÐö&ðqwþhW÷ôÜWÏÕsß÷=ÍŒnŽ‘dÝ’e[²lù-ø6ÈÌcƒá 6$B Kdsì&$»/Éî’Ø]ö¿y›Í± Yœ—€ÝþWUwÏŒÆ2Ï{/ìé®ê®®ów~¿bv¶û¾lðµ;f f?dü9qNAùöóœkÜhš Ž¢øåʧO5áœÊËä©y .œ£\XYš@…å5°áËÔÃüÕ‰SïáÔ} ¤í³¾ÂJ¶à ¦ú îVàäþ¾ÔrÇôÒSË==ËO-MßÑBN]¸ÇÝ:S,­ku»[וŠ3­nÊó£¯ýäÕÙl~âÌKWì}éÌxsv#§¯žL&'WO%S‡QŸ–/¾Mï‚sÅGô7œ¥<´pfFž‹´ £(ÑE©.^Ç W 6}°¢ØBn÷Ã-ÔßçïëêhnŠGƒÖ 'ø#q¤jçÀuŠ!(s¾ÎžTß)€úUóúJÁd.UÖåçîØ–wtî¸ç;7Þøí{;ùíý qå‚öb‘ØúðÁƒŸØšt&ŠŽ`Ö¥ÑY¢%Ÿ¯µŒßôÙ¹îSטÎf§ž8Õ½ñ³7‘·¼Üº¥ î©L^·!ÚpÝDeÏP0RÙÒú2zÍÅß’;áb³+«ô´J*d} ¤ß¨½ü7jßÿµ—ýFíÿ߈ØÏV~£ch¸ÏPà8†Á^DÞ(¤P8±1Ÿsbɾª>š±\„cTmR1ì­¡Œµ±Œî<ÇôžcýÏq~€ç¸?Às’ñ9å<,$rs4#j¥ª‘i ä¹ö‘É ´bذ}ÉQ v'ŽLõã1ÔµÚ‡7x¸XÊ®9«Ñ)üØÌ<œÓÃäö÷þ´òü…c{”{™üæÎš*«TèÜe($<k3—´f$Uâ¬êÜcÅ;óO$ÍB8ñ¢‚¯%eÌÑêô‹ISuú!¢,<ýpR¼B W„8kcû«–hqìVù’šµŽ)!ÑØ5#j¥øQ‹ÀOöÂQ3®þÅ—ŒÜeºàRè.ê­•]ra¹aaÿÀxHŽáù8Ï*q’ºþ‰ ÂÏÔ·_ð³¢Ð Á¾Õ|øÖÜY_(„,¾U Uû¸øìm[óÓlq;O“ûÖݹ1Úñ–(Ô&%lS·©ï,~­Ú'ªXyØE®áíJcÀ­¸5wÖòù¼Ôw¤õÎ$&²·¸=:„Ž•YÛ¼í¶¹8GÞ†Í&cÛî;×]¸ÿ9½/`¹ÒO4•3j¨"†jÄ'¡æEÉX‡sá0»ßk·ÂÒ¨(È ýFäÛFà8<Z= ûÒ;`äÀ'¶'w,XcÌß½Ùe{XårfSå¨azý°ƒ{„z+=µ¿{ëýMÈÎ}9 ŒFýÙHÑÝQíÛÛ÷q¡}ÙrJ- ùH‘‘’žÒT&([­V¿ÕçgA¯Ì$pµEì,Tú˜¼ ˆ>&0Y°»UŸ°u½ùwL̶eGrû'p/¼³~ý´!ZN‘×ëM€IsÏ;BšÈý[»÷O¥¡ÒýßÜo:Ü…ËãmÁµwŽã8ñݲnyÆA’²°þŒöì ,E`énÄ5'•!nœ_ÈVMÌ5êóZÐ_®f]Y­\l¬!Æþ®R³Ž–.PU& ù‚™€_a]áEvKó¥ê«âWBÕƒüº;éa'›¶ÝµÐÚìK²A¼x³£±±}ýnäp+=nåü-s›¦úì”­r²cèšÙ&u°Ü‹Ö¶Ùزy¹ëú-«¡U|iÒÁÀýé§O!â[W T$)9qìdux…¯TÀ­…VFECpÝUÏ%[5ÂLP4›o¸Ù„7N §(¨úïV”)û«·Ê•\ªzùjLð¸{ÑŠøBA©9º—œ¹„Þ\ééÛ?ë 9åNF£P{Yoå@õ·RíqÖÝsE'â§5(gæ¨ßã=ɼîIëf ±Ï|ØpCY©„}áÂÜ6‚'´º1i§k“ ÈïQ$Žtœ_97õŒ„˜ ‡@9B©óðÅ/ò¡(HòoņÖ8 ˬW­Ð0ðëB±ñý}‡G­D¿Aøû@Å+qÏ®«ÙcÏkçf”÷~øk×]ëÐÞwñyî òßá÷fˆ6â^þ+Ma@‘v¸ŒòP•"Y•ç¹ó´ñr•e’景㟒‰Ü6bkŽ@6%Ö¨/6WÖå²Åæl[®ÍŠG“ru¼©Ê¤°°¼ÄÛ½ŠÍ…K¸Ó œà_ì¬%fóGýľ®™¼Ê¿¹¡œ£Ù"3™Ï%a§´6ÛîIKXãÚÌ–fïnõö›Y•ÃÝížGFÚƒ•¨¤¯ D2®kÇr¯ûâÈçi-á%N#Y¿‰7£4#;†‡“h®*3¨û1 ™'´…{¸„&vWKaç eP-•Ì×ÊâHºbŠ4/àCH‹2 ã³òL]-ºe&cMÎ ÊôÍçÎEü˜sˤm>5xÓmà ”lŽ›MgIM¢©šƒøÎà7­&Ä-e%TmI”¡È^QLn€d_UFÀì—+Å(ŒÛ.ÈÁõ°íõ"0~…56€µ_"?=Š?T‘ŽÙ‚Z´í²‰dÖ9vh8@…k0ìçŸo”—HBvñmÉçá™ë"ÂÄrYé x~J¢qß+ƒ»”RJR ‚šSɱŸR Šš-¤\¥¶c¨qN¾õ\¶zÜð»Ãž0|™‹1G{ˆÑ°Í ôØGT‹‘ìhPÑLG| p5x†»å–‡m>Úè[3½Pœ»eSæÊ+ÁKýÇ ÜÌfê-îpš»æin’eG=MAò{< NŸÿi1sÿÀú4‡‡/ž“l…ßÚN4—sy”Í+¨, Jé•ZŒØN´%xIL[»] ö‰öê–ØŽrþI]¸¿à‰n)¢žB%® %D¦"­›{ƒ£§_Ú·ï¯O{7µF¦Â‡“PÀå~GgFK]ó.WÇ|Wiq$C&æ?!“²Ã[vozâêÁÁ«ŸØÔ}të0+•ð#Èÿ@½Cýžh%6ẳbÓ†2/ü“Ä2oX<â}ÿ’ØMGužŠñüµ»ð`Š&ãÑ<ŠáY™W?0’¢;饿)¶GþÙ%K79ÖÝýê¾u·m+0$P©^ ø$2©Þ–h÷efûbŒ'fµ•MýX®’Qýïº÷WOl(í{lïš/íl>Òuä¶&oï™oµçvêÀÐÚ„ZÊ ÝXGëe8¦Â^f•ðÜÂØ¼r©ýIŸž‚’kŽˆ ‚§§=üŸ`éÇŽìcŒU¡°1—m‹ #x/;ŸNÏg9ÚdÇrŸìâEúw˜Î[v…Y9B´î•^‡åC‹ã¾D‡—6A¡á=ÂO‘êB"i\ÏôïþΦ¯Wd*›öãÒí3$½[kUiì'á¯WAáu¿æI…Ž–j•ŸÕÆþÆaï¦ÖÇëS¨mv8ŸÚX»™“™Ô[É o¸Ûà¬q‘á$¯wP°o^¥ƒ„…€z…–&…ÎQW;ÇïGƒ¡H.í ß«Á—ŸGþÕã•?©bdr£òayˆýä¿íNîÀùN#ø•½àt–ì‹: À÷Ô£ÏÒY¶©)$#Tƒ\p/%ý Åë+éaȵïp/þ“7¤ÚɸTJ³Sñ¼–¹ÛÀ2y…›åÎËOw¨L~Bä’ùæ’ÙyV_çóõŠ|€<À#oQ]éF¡Êhaì^­h½G¸¬`ÌPé ˜Ðq#pÆ\"×8)FËwGYŸVç•™ÓÃÑ‘=½n.¨gÀî ùe–ó¿ê%[c‘œ$ò×à8¡ÑD”ÊÍN@v@¤PÔŸ¯á§Ë°l&8bÑt*Úkb̶hФ Ô󵕚%J±Tm©éáȘü±AÃ=öˆ¥Ø3–j.ÚCGôœ–DãÛåJn ¶| ÷y&¨0Íʶ&½ð ›¯r…º×eÛwŽ$Ü=a·Iª¾äÂUZk$¢Î æßú"æßŠH£s`Œbä5 hlû%íéÔ âIÙDÒŒ Kˆu¼=«ñpù Áo‹ÔZ—gáºùÌÁNóÔZ»ë(¸?>`ð%íÛ£Åf9q%"à‚c’âž >Û_$zÀ›e<)¨0” [-µèÔH”ï3YI Ê“H ¨¹À‡î®”GžÕ¼ykcy݇|>ó!ŸoúÏ·|Èç[?äóêù(‹F(*ÈÚµ*+¤nѲhj)u¶—zZzà¼É§“ˆïE®«±¼‰‚xï†õ²øJ¶7ÐTC%©9·‰û—÷/f¡Ðâ×Cq\ª’u|lݶSÓA»¶}ª¢ñö- | NÒÎB¹¸ÐæþÌî¶|¬»Æw횉»Û­9[ŠærƒÜJ Ì&“]áJK¤©iº]¤„{ïX(p²öp•Ç` }á#ÆdS•Ç€U´6Òh«&/Kõ…‚˜À¼hðªg:`Vg:Ø’\ÃZnËÉéàuWÇÛ1ÓAtûÒ®Ô®ç>2B}aòÞÛÜ\ŽÕvo€¶cÓ6¼u´ï —ÄÒ\ÖàŒ4âÈÐsŸùÅ·ÿ≟øÅíwüâ3s¿/,Þ9;{çbAüóWl¬èvæ#,É;ÝÅK>tÏŸ_XZzáÏ÷|ìÏgwízáOÛÿì‰þþÏî?ð܉¾¾Ïíwñ¥]îfôg3:ãû/¾M½[µ§I¨kOÃ}Œ&ç‰ÕäpÇûÝG¯ÃѨ?šŽËÚܲ*AcŒ½ÀGÜú×÷GŠ^mjëWÜô½[oýô«Ošîý:cb iÍŽn§³g÷h¶'ÊTï¶Ù>}¦Ôî,ìψî÷ûAÊÌ8\MAsræšññc“1 B¹̽õ®J^'ˆ¹òú6 çp*%ñ)%sPd¶É}L® ™\1_7Òb. E‰¹0ý=Ý]‘hÀˆ–=Ê̘ò+£¸ó—ö®E}“h“Gs›úBxw[°oßH,5¹¿§iÄ¿“µƒ£¯Üxäµ;§ÆîúþõÍ›ãlÛö‘–‘ˆW¢3X”r«: v‚ƨoæ$Ž o͉‘áµ&>SIèµ×39Ò7›à| .¾Ä=AŽ`ÎÀf”‹PvBé¢EBÒRµŒ„Roý™Æ‡à¢è EÆŸlÅAt´ÊÅr. aÁ´qÖ•VœdÍÅÓ¸D®-žgdÀa·¦ÙqŒ±¦vWZm8JÓ¥ð€¯=Õáó¶"`¤V¯¯#eç¶xò ›³‡}A¨B½8{g«·BgX$íKFeÓP7—·5a³%`EþïúƒqÆdm^¼O©¹W¨}PÞr#€Åšö"=PÈʈí$(dEà2˜A÷†¢!ÖÏQ,V^Œ˜0"jÈrÂ"V»ú]Äõ_o#à~ŸŽ;ÿ WLû‚ÚaËsÜ+ä_ëŒÜ?™YpÀÐh£,xËdåîdµà[ïþ ¯OÝðïaÛh ·rÜxyšÀÊà M3„°ßóà*pí…½ó=¤ à\“—q~k(?ò[‘…±øÄ¢¤Á"óù­È0ÝpUÈÚ _&iUÈ?5×XÞ“¹´µÊúžx¿ ׆Ò(«8J%B…pÁ‚‹OôÀeWµ²>§õ-®ãÞE!uÛ†åØj鬑Íb:+÷2øªRüõÛÆÛÒÇê ”çi+‚êyy(¢íâÛÒkázòY¢X^öÔ; R „,§"1`TW¯ËÅëø]iN³öേø$¨µ4ÀQÝÌÊ´ðÈ%_+EÈå—­àÇ)¥Õ „P^PôŠ—›þRI¼4ðßëˆKÊŸ·Xˆ†½Y_–1•bRšEmöXœîk1.&à“^kÐr àÅó!€’–n¾n>ߺç¾#{âùÓ÷ö÷lˆ¦SÉ5Í.k² üBÁܘ 0¾Æ²z«¼™8Irýçßh)€—jpF™ZJnAùÈÜ“ÒßÃ1ì#ÖƒGùÞgM@A»H¹b PÊ%€PÆ-£Å„·U«ÞÆw$«Ü‡µ•+” ¹e„TÆKm[äÔ¥’X <¹¨s† W÷­úB¥ëèQh*ô\þQ„ð$A­¹ìƒÊ•Ú3ò ^þjœÁŽž$®òÚs0ù`¥ã£ýë+ë{{:Úš›2©haÓc\›>Ч]׆Ÿ@+õð%z:¯@] }“EÙîß)RbNJqä;n#ÇÍŸšMtŸxéêkŸjiW¹X6ÚÔÛtä:[ˆÎç :Åp\.ІpDžÌ Ëήä|FÁîPËúº{ˉ5ˆo»Ò¼•1AUÿæ«j\Ûçßú*OöU›•„„(]\#[ çc ±†'6‚“œl ºE TÃA(ÐKz‘ÍFœ“Âm奷ýÂmÙj·/sGÒ='ÎÙ8û\EÕ<”…(#²³#M—íSkäÔŠ(|¾-Í+kµ ”÷²uQC›V©KU…àÅKj–»>x%lÉ~IfàÔ´JðüsNN´µ®Ÿ™Ø8¹q|¬§»uMÛ´Å£A“޽¼ nVp[C“Ô€E¤Ãpƒ;úÛV™Îtlhq„Ç–'³´‚¦£±EZ!½ðu¸ï™‹g6ŸÍµì¼kÃÐb”#_yﮮɰw,ëÏ9,±6ðSZ >Êm'ÿCͲ¬ t[V3™õ†É,5bÙu=Sâ4MÇHm‹¥<øFº¶-ª´ä®„Œj‰˜3ÈãöJ~M«‰"œÏ¾ AæL¤DX13%‘K(ùnlçÅ4L|ؽkyV3ÁÄ’½´°~…+ ¹ºzå’gö”T.Eõj€@5µ¢<E]ow[ksS"ù™€Â”0 €wØK×À€ R‚’%¯@7— ©vÀë,RÓƒáî ßHré³LyÍuź"=Óë§{"ê@çö;¶~wñcKå fÞä$†'Í£Mßí›Jh¢åµÖ–£Ô7yo˜JÕß‚È.­Æ–”'æå#Âc)ïÙÿè°?Zlââ(â“qúWØiõ$z·àX#÷¤ä‹ôI¢•è·ñnq‹-T'Pö“29:ƒV»¡ÀÙÃN¼¾HRÝW—b§BÖj>?Q0`3‡Õ FblM„rFÒ»?le$"æÅÊ(ˆ¦$ôüªŠ>´ V•JHén‚–RRdÅÿ>Àà!×XYN(à¡õwñ؆gnoëîjëmïm)†Â¾Pˆ ‡ükäfÃõ¥8+ !…Úk•üXöh¥¼Ø˜7¹BžïÄfÏlÍOe xÊhÝ®“ÕŸ.úùHt*ðNnC*ÙÕEšª¾Ö`-àâ çu¢ÌjP~áÅ\˜TqŠÖ&(«ª.¶K~×qêq à1~:˜+­$­Ñ’ U*NR ª7(x*4\Wó×í WCb‚Û®‚ÄÛ®ºO‰<š²µHhEû-©PyL”hÅý­UåŽ÷©(D ñ7û†g y4@Àmdï%ºÜ34 úˆ>B­¦ÖŠOƒ2ÿ ¨Ê¬hQí1ˆQêýŸ‚g•cÝÌàÀƹ™…u “k×ú˜p 3 øuÖ÷ì ÔóÎGV÷Äl&¼†ÍC„š,¡ªFr«Êb4xBM¡Më½)×Öi][´Ö•ëäÈ‹m§Õrg“¥õtÿ57šœ77«R§ÀéÞ±ï8ÿ'WÚÀZÝä6…Ñ`T¶æÁ¾öÅOíõ®o6W¡®Ü½auÕ±;£ ²½-|„tV¡gÙ5• s>gñÄÐìÃå ï¥Zn×…:Sd!ê|l}7}1@üQÐbȉ@"«Ík±WeX‹µ`Ée%õZ %­VÁé¡Á§HCh\óÊBª)_™g—Y­n¹*d $þ~Àê+„_¨Ýð+*ý}½(-BÜ@«aW‹ÿMžË&ëjiÎæ¾Wà¦ì¦›×z[ÜR2a ¶FLàÚ&µ¾pýÜð‘±hdÝÍ›m‰XÌ‚£vöårOv]˜ûG*¢AðÓ•u·nÍ+7¸ô†ÄP¡3ÝÙ6„s‰¾ŒvZ¡Sa½˜Ü·n!¿æ™@p>‘äeú¢ƒè,·¹°³TŒu•ò9¼õ¯¸4d\é :JÍ‘Jè-ÈôÈžTw¯Jb-áe1>5ý¸=?šûNßD\)Ãó¹Io:³qéIxp[ †H®3ÖYéÛyÕÎ>÷æ/Üõí{¿²ÍK?òÞWO۾΃3Mµ£:– …»wL¶ÞXjÚxíÉ(ÊㄾÊç,.ø Ûª· 2 L4³Ï$­SQ3Q,y;@É­P¼MD€\X§(À¹½ duuc««£vÔ®ÎñÎLo] ïz-¯(Ž©Âñ^¶œµVÎôŸ—ü@σ®](†®VŽÏ 0Fdˆ‚¨.d¸La_<íˆêï>»¾mâÊÑàÔTûî ǵݶiúúõIÉ4ŽŸÚi6"Zè ñõ[šSäëï¾ ž, Túµä¯¡ÿ?#ù ìÿ6Pâ)QO·‰ýŸr* d46OˆwÀÊ;Â8„Wé|¤™y’Ä™ª\¦{¿ÒÖÆÒ¦õìä‡x6R…ñ‘£ Ó6®'¡Q«„KAùh¸Ö÷®Æø·Î*ñþDýð]5ššêØ•á’)t•ÏÆÌ¹&¯ ½ïx6&†+ôT—$âpÍsÔ[p•Zk«Åzö¢&$`£Ö‚×0¢ñ#‚2À'ãè|Dˆ±á^öFäMÜçrò˜ë°ï½w~;¬dU*Véf?õI“Ÿz룿wަR£Nü^ ”nXçdå±@¾RÈ9(BÓ”œFÖ‚”ƒ½¿ l ¿— €o‹s;ˆÐ$llÍrƒ6ÐÍý³¼ð-;÷Ü*ã} üžbÎö·­[ËgþäS¬5 <›õ"{k÷Å·éßPï}ı‘ø!Ì«=,IJzÚIB†â­u,PI<€Tá+”( ªyr©L*—íV¢]Z*H@îGdÄfûtjaÆ)´bæ`÷åë*•X¸ìæÊáµÓ€X¿nzãÚc#•þîΖb>—N…±Q§Q)yK_C ËûDºpfe>új°Øa8¾ ê2Çöo<8ÁQH6­¶0d‹î†=“…Ù`“÷ǶíÙÛ´ël‡{féhÇš+§­·üïÏM¶ ·äÜö *3ûµ£ï —žK‹§&ý]©‰V׺u·†;jN ¨{¯ùâ~WÈ¢¸å:›Ë¦Vm@NÔùŦ¡Œ%4´§¿åðæ.‰gëv—ßå4LMŒOU&ɉ+Qrwu8:z¹¹k%gz£h,{à^‡sÌBøˆ&ä‹RÎQ%QÌK•$üü‰ž¡œÝð™ÒZ¹Édô$º›‹³å@¤ocÞ•‰…& ÑížšD΃_==2Ü~T©3[Í‘±ƒCkEi…axo(ëXá© áÆ‘›BÒŽè‡[/ê Ñîw¶»2˜»Â:zû·ŽqËà¶cߺ}ôÏá¡Ý½÷ßß»{( Þ¾â…S#Ô[#§^Ø[94¿ðéøÄ!üÎ1øN¸!z8/’å ) d…b¸Å· n# Ùïc˜õ!ÉŘF7$­¨ŽôØ.püðK7 ÝüÒaîÖ]äÏý= ù_ÿë# =~²žF-Am_èG ú¯3¿A-ĵpžR°=*"]KÂFÈSè(â7¿©ª«"TxûCB³Oü‡¡(n?hâ~î½pœzë5´¹ñ¹ð€p\|‡ Po%b¨\ñ‰4 ·›ª·OŽ"?qÊ ·£y1šC=cC…YÖŸáÙD½ ¼°ù\]p9 µtl_v|r~¡5NòQfã5Þ{°ov`¤c5>Þú¬ÆdCPÝOðP(ܦA5FPá厖£ÉJ ÏÍÜVð ø±”˜e/þj^ÍÞÙD¹ê ­Çdí`E8Aš¼ŽãþÝžhóU½¹m ; Ÿå.Ö]¶ÁþŒ]¯ ºšÞy‡\ï%§…»Šu«L-\Ñjã&=· Í$@@iœü>³a/ëÕ*”°…²ÿ]Ç,â°Øœ:=8xúåe„aAõ}öá§yê‘¿z¼sðìMƒƒ7=xèÅ ~äÅCÇNÞ¼|ÍÍ7ãy^!îôQÿJ$PzÅçºcŸ†[²¤u¢|úú*<óæí›‡N½¼Œ&ïÖëvR]=úè§?ýè£]8.Ê»Ç䵌ó&ÐIÑ“±Ñ¸œüæ9:ÜZ Ò½€c»2Û‡µ+Á/ 8YlÕ„‹íÀ ¨f P"ʄԂ³©7nñ÷<´¸øÐž¢¿m,©ä]”B§ù²Ã¬¢¥öTW mDJ+-±ÍÚy Ñ@GÊîiêñû{ònj!µ¦#kfÛF·u÷™N¥¦ôwl*ZÌùòDNïöøMðxôšÝéˆWïëÐùÔŽ¨wàÂßú ©° §J¾x‹O«õµÀ)ÑÞ–èà¼eÐÙ,¡P<ðÕÔs”ï0AƒŸ†¿gùăR8zÕAîÝ›ÎØÿ•ÀÛ>þø£Wñ`ö¹½{?·ÜýÎÁïîßI]øëMìê:øð&ø÷wìçåÏuDA’¦3/RÒõòçÿ9!Hh$4ûRsÒg6éĈ_ÁÇ#ÛúÖQ›®¸²ciû­“$wOÎ6%.ŒQ<Ô¤fܽÃ3¹©ë×%ßšìØ•gP(Ðá÷¦ .¿¦<@óðÝ4º Ð󈮅œP)"„æk­É¤öÖRº'Ó ØSŽjÒˆ §Vk]ä쮕¿ÁרŸ¦]¹€©´xfúÂÝä¡éù|òBï»Î¦ Ùlrºòa‹%œw5ü^õó¶„`Á\Èb å`Á”¹QUG]É:PUÜ÷¿½ØN½$9KÄ‘•Œ4¡}€âܱ~?‡hTHvÑ0ñXÐ “^ª¯)Þì¥âW§ë¿ÿ·ä7デլi æ¼Ú;oàzÀ+A¯‚±ëÑß6ÖÄE%g?‘S3öÖòpbÓ1u`f!óÙ/¿71èLÛßÉnñs& éßŨ.y!SŒ_÷ZBkfLxÝ›°GGÈÍ¡òVnøò›Zžá–¨·æ£Þ '_Ý®÷iÉë_ÃXÿ¼§¤‰îrZ'œÂQÍÞG¤äö# F…Ú™ÿ›&Òæp8”ÆF$P7D»YX7U5naäjØ( f¿SYìq½{hþ¹ñ-j³Lcf•ö@ÖkÖ¬ÿ¤Îø÷ l­1iþ¦";´¹°ñ–©±…+4´Â °˜µ¶hÁÕܽ— >óßOPî¤T˜äÊ2Ó(:•Òðl踫ÒWÁO¢0AbIbÀî>qt|€b(¢´¬êê(š²‘@FÊÍ2sM¼©ÏKGBQ¡.u¨ª‹‘ï%×Ç“ãí·9œîJG¬¬SÕî™ßÓ²îÔæ\nt!,¸{;#íÙˆQoQ¶6îà̳)òîéf·jQª³Å}æˆÇ`uYMÌV{Ògˆìéî]_òjvÐKЕ,Y£†ÙTªÄFvanŠÿ¡þ‰úÑDÄÊa ×WƒsóHGpl >ÀLõ›ƒ¡Ž$2 ”šªÒ]ÕØë¦ÚAM¢åýùnGWoŸ/7Ùæm^zp率–š½mSY__o—ÃLÙôìWgç^šMgL¤ÿJm1¦÷=Ñ3pl}.·þØ@ω£{ÒF‹šÚdòÙµÅ\®¨µûL›`›—a·ÿO5Þ¼†PoΓ$“ÿÃ?G^GEο!Ú°‚ðƒ¾SÛOªfxßO.ÉVyê;?öF·k*«=7”Áö7Ý,øMhc6»1Ä™‘kBÀÿ#>“í(#KŒØ©:?0‰÷Xþñwøuuµ€8 7ä ü{ˆÈ•ÓðöA`üî_6+7(9µl5ìÜ©·“¯skî³´ôþ4|ÿ×àû‘/jÏYÞÅ»tÂ3Èì‘’ D|íV¬¯¥–Éçð7x ¬°ÁƒaË(õ祅y’nPó¡À€•"ŠïK$¼A‚ µ’(Ñl4D<ê÷=F·Å_ÂøäúK} TÍãQCÑ: Ìcûú<ÍݽÝÁ]½{—Ü-3œ;-a‡>žMÝÙ<”ÌŽY ÃíéÉŽÝ­g*½QÜLiv„‹D“Ôqò_¥ÁŸsãYÞÛãbâÔ9uTØn5tŠçö¿v5OP­ä\Ùñÿ è1`Û×ïiîêí .õr?™Ì%ìÔųëfgw» îÉið ß6Ãpê‹/m¹]nôÛSIî×á-q“˫ֹY-Ö á¼5Á1WÃC¼KàçÜó"êK lüU¡0!X(bpî¬/|©o%Œ]+EÊTÚñÑuÜ/€cÝGw”¾³evnnv‹äͱ“[Kä륭'Gr; x8€“°*b†_6n€LÀëÖ Q¿\µRI²nÅ2Ñ\êå@]éc} 9Éýæ‡?FÀÅ¥}¢¥@¾ÎÛÈa:/iÊ·›ÃÒ|³º*lé± |Àk9b¹ÕÚÀ@9oþ—ìäâÀôÃr¿‘ö]È?t꾓ògÔêÉIG ¶r ¬]Éí"béHê ZfÕ  Œ1LñÈZ+o#nŠ˜i`jŠ/!‚üÞ5–3дÜ<;yËŽ–âŽ;g7I´jâVKÐç3ÆúÒ6g¾_"—ɦŒºÜ·âþ–ÅÓã§·—Zã³&•^«—Wq8•Ÿ,:ñ7ý~÷»§ì¤QÖ:lç:|‚ …‡Îu´ßIqfUÃÿÃÈs¯]| þŸZAîçp'ûWàãûð¼Ô ŸGlÙ„f ™×§°}˜Ñ“ zN´¾öÚ5ø:MÔðiXÇHôžåÝ1<ä6ç'#2Fj'p¶5ºF"±Lˆ€V‚Ѭ÷Øþ¤7ƒO»»¶ô¸Z<**$gµñЫ’;v®‰Ò²«üTª|µ 6çÃ6¸ˆPÙÏûhh€xæ¹A´¼Á½¹hB‚‹ ÑØG#¢³¬ê£‘²§ü”6ìüÜËV§É§}õ‹^ò¤ÜÅF^}•N©ÕÿlÑs¿ôëäN–{ÑÀüR§æ~†z;a;ܰ4á*Ûá<"Q<*@.Oìž!ÐéÉCÈàľðŸ¸ÁW%æOÁÊ€ø"ÜOàœF6îrÜÊÀSiú¿„ý—˜@F0E!`šŠÏRdbè®Ó«l}±„P¬üJ€ïFºéWYJ³úq•ÃvÿÓ$ù9KÊž¶|–$?ŸÍ©zLò›±Ûs,è3Ë/äÁ"³pÏÅb`Ôa¸GÉ×efî«x¬omTÁ6Z…S'©b‰Ã¿ŒaLq %Bƒ ëcyHq ‹Ü ⿵±zÐB‘Oïd=Zîû°KÒ>¥»Ù–1ƒö y®bîa9ß+Çï…ãkÑÂ] ¡ÏPØ÷3Áå¡I™ÆÓH*µ˜8Ï€,\!%=k[ú< ¹×µËÒÓ´Üœ±%üðC‚ångtpI|ËFë¨ÔÜSO¦‹ÇûÐ{¼à³¹ÔWVóÙPc¥Å®[Çb©´xÇúu],}gavvý wüS;Z[wœ=½ØÚ¶xjtaß¾…­{÷âöMÃ=ï)Ø/EbˆXSVtÄ›3͈|sZ„kÍ,+*}ÍMßû™¥‹Ø*ÍÇ+_b“¢ÒF>hô¥lΦ°Å iv;Ò“‚¹Ó¬—Ñ]éP†$eŒ÷¸VñŒÑ›´%[î¨Ùõé_z›“( ¦šÖvø|k›}…ˆ^Jµ,Q—Û¤q†sfSsDa’YÜl–Ûc ºíj¶=é ›års÷Ë^¸?ûE …åL9‰.]‚ª( #Oš ,UÓûaîÿæz£­ù£-B#M¼ Ô‡‡|þ5‡ÆGFÚ“¹âˆdößÉ×ýr-›zCoÊG ⻿ß]µ…"Qǫ ˆÊJ>P¾›@g2<~ ¤ ›ð9l‚-TÆkN|¶§ztÔÄ«ç¦ xûzMÍÉgÙ€b‹ÜltÊïÿ7Â9øgø,31ÀóðþÄ[àцÅ"s€•¿ˆö1’ßÇü wй²¶Á\ V1—‚o´—_MN-¯¾j:ùjYb©TÀÜæþCS©ÔÔ¡~ô÷J…oO7µ,ÙÛG^Ô[³KQÄ^ôyÛ¬LòòV©ºŸT@TfÝOØüô8­¤i•tí·¶ÙÜÒE©Š–(è Úg}èçÏ^%‘K$2Éñ/Jû,&î{¦´Í–6ÆÆ௤՚4¢_p“9£véõ.5w ÅüRûÈ~úz¤·o”µ %b@&u ° Æ:”ÁEFÎ×±kyºô5l5u4„ J.)ˆýr9 [MÖ  Û° ŒÎ‹õ.) 0vµÒµ‚(àÒ "ç"9¯ÛÆš :­\Š4l%ò¤ ÈÍ–U#ÜÐ#žv3¶î|ÇjS(•üÈ´­÷ð¬ÇióØì&ƒMîËëËŽ]ï±AS$gW˜èe­Éìõ Wm9Ð=J•Ò mG<¥Ñ$è8¤3æI¥–Q˜4^1œ§»é›à"‹ŸЮ€&]N(ôQhs@Á…ð UweNDA~”[¨í(%Z²V†<žÕy{˜»²á61§ÑV»êmØq» Pµ‰Ø#h’Ñ„`!cÖa &0ë ˜vhö¨5®5ùp_Îém›Ì¦Z¬Rµû wÑågt>½ÞiÑ©e—Ë öoël[ŠBåÅÉ' †ï"`׿è  ž¤ROÒÁÞæIp–?éÔNIJÚš…¸KôS-þœ`§4ŠØ4x^¯†;ªà ÖöáèHÙZRNÉdlv*É——_RçÕT+‘b;N-\ÎõuÉ¿XÕ©^b;/óˆKk#ˆÃòûÔjuõý—N¹µñ°")QO òc’’óµG6Ôå±¥âƒ]€^3098Yõœg’ñHØë¶[ zJ!#:@#Ò;Wwœ[.[Š’ÕH;+/@pcf²Í§µ¹²)ä ²U•u´¶·Ø²›}¾½åéuõ Ó.÷dwuG,®©D0Bß/Õ9À‚?EF²…‘:ì™o‰Ùã¥Z©ït‡ŒÒH2¨ØÛ›ü$ ¨´*¸dcQ¿Çå¿>W¹ìàˆ”H•Þ¨0ùu»®Ûµpô âÓgM6Î+Ù°«=ZŒgG¯Ep&ü@úßg»±8ŽâK ã#ÖXQŒ5D$HÕað¿â²ÃÀËÕΞ,¬ïöëY`VbÖ€Ó®³¨òöööV{qÛšD°K9•=*Ó»¨ÞÎÙ–˜#UkÔúÔƒÊR~Ó@ìºPXm·èð™w ì#'ì£Ê¥BáøÚ ꈡèB=Sô_, 2D# ⣅­²È±Uâ‡x-³n%©ôª5P·§þµvE¥râ’ò<ÝÝ|µ^­4ZʺˆÈL` ÄÝüB©W« —ÙîöÛÚbN«Ëêf-:3‰¶R“!<ÚJøcnWËdS÷¬GòGF.WɵŒÁp„116µN­ïuRS¬#šleŒNÐv£ñØF&uôP.ÿ³0n ZI #ƒÔ_ÕñW#B"A1RO·Àâ, /’Ë—Æ€N2º‡kñü…ˆSíŠJpHË `ÊÕzµÒµ! à!1Ö©~ ÕUzµÒÄæîok‹8Y'½IÕìhmÉã“]áˆÇïtµÎ”ºf½Ô»z~X +R¯Ò…ùHiTE@e\Q~„±¥ÔÞVênéÂ#,0ôj+æ˜ÇùìHº0×åIøøÑU`ñ/ [NÜôÄЉ&Ï·Ów,Mßs LkXæÂCê;´®íα.ÃPlÇî¥Äõ/wOüí¡™›6¤$m;2à¯mùqD!¾~a¨cóaðÆÆl[çѧö¡¤‰vx,JÔo]7»Ѧ€ÿëð®ÄxGï­2¸þG`ŸGáÙrCÙªÓ$›‘S”´±Âíò{à©"ÎF(•K )–Ê%Pí\"DÒo]ŸŒ&¾˜pc!Œ/•ŠHbBA,¢¥ðÅQ“54ÉM 䬇Â0BhI™]pw,†WIS†™ef7ÅUÒóÒéëÍ©gGe<=zl2s½tóµ\ríÄDTš7Æ“Ó1ðÝxÿƒ/˜ÁH ,¨ÊÃú±¾¿Ârÿ7HiB‘°Ž« zë\bBŒ„tèt—\|›ú%ì›MÄgËÊM(RpjˆôñåFhmt-["dpjÈ—9 äô‚Þ¤¨ k …¢f“1£žú÷£WÖlœ ¦‘d0ð+øÞ$,Ÿ‹SÄ"ˆ\Ê«À³½ É&)b€)…›E{ yAia ÁD³?W°'Z»{\Œ1à`œw‘šp_ €Ìüé ‰Ù©Šµ]é¶Z±RÜ·©Éöž^—†Uâ¢@]ÓAÆ×Ý0³ù¡l• Îv±V§Qrê\™ùÉ\+G¤pˆŸak• Î…¥\f Ë.Lvh”fâ…«”ø4 ÷$õõ¢#æˆÊúž$IÑÍFRF*€\F á^IÂnÚJر¤¬f¶BqP Ÿ$¥Û@†r¸ïÀùû>ÕêjÌ•ã€X·v|nbnx¨Ü ØmŒí ]Å˽>"{KÒÂ%¢°ðsŒø¬>c}Z×±/ÅýÖægZwÞ:aͰ&¦yfÿ¡ý3Í]ËÏØ÷ØîüX_0¡ód¼CëÚ–îà¾Õ³t}—/nÎN¶w­kf3684::<Ì=i·-°v)“+ ›IÁéÒ:*Ji-nì½~[{bÍB¾²Éi(Z²™¸>vËüð±µ)‰3k™¹5h‹{£/sáó3•éé¾õX7]&¿u9QáÕ7³œ1Ç'u ”\,N;P@6A‰·é:çÜ‹u2…r»šó…<ùõ×¾ûü?µüê«$ñÌ3hï²ÃñGñ¦9b€È—³P&[J> Ñ éhJ”¢MlH;îë¥sé¸Õ‚Üš¸Ïqp³d×:OïÓ ív„­Kñú„BÇȇ¯œŒw/Xèîºþ•›Ž|þP©ýà'ý)Ç“÷6mkÍŸ7sNõš[vw¹;æÚ»·öø¨·v›ìŒJhÑ›Åm7­»ûP_aöHgçMƒ¬ÞL8?·M*mNÍŸo‹”‹ ƒœIwŒ¥·«yïÙº‹ç$s{|í_)ƒŸ¬’9̉ç•Òh¦ûí6‚p»l>»¥ ™C¸{Õl-ˆ¹ÄG¤\ ©Ø‰‰p?ž?wÏ=åãOï»âéãå{È­»pcGÈà(÷Qôïó~19Æÿ<7·§?€dHB}ñmÉ·aû¼D’XäwKbÛUÐ$%'¨9¥ q©DÀ.¼¾o ¼×±~AD¾¤?‰ Ðs1FЗƗ‘p‰bPéJŠ18 >íuø‚Ü© Ôf»æKz§ôî{Ô›nyjn÷ãÛNß –Ó nÚÕàv4f.³õ'×ôšS•¬†„«ÁáªPRIw[O÷–ð­ ,'—ü· ùúãÖ–èh›§·j]ŒI-Qº V÷ñ ¼O€;gôÓn™ ~ãj?y\Z†*M‘è(·á˜Fñl½QxL©3(‹Ð&>é ãÆýñXS6VŒý^,˜Ãöt£ã¬ÄZ¤¼‰]Zz<™ç/ ”ç” †fYµ¹¸=¾)¨TêØ¦Ò7•ú#þÎuùh·]ö]1ËøœR¤2újÆ¥ÚX Cá³æZun™2fãŸô•çÛ“]A‹É5·r_Vz[2vWs°¥…+?H*±õ_ËZ¨Û*¸ '•@´.páÍóŽ1ÌíÄ. ôT,¨9°áÖ.•Ñ»Å*ÄûÕཥb ¸Áq[­f]¥râ’òÁÇL‰õj¥ñyø‚pºÎ[Zï]ÉÛÄãüÔÒß"ËÞÙžÜDÉå‰ú“áŽÑˆ!WjuäÕ&½™u²^«3ÚÖVñØÍ.³Q©Ö«èv7ŠÃt9ÛMúÖT¤3j–Cî^Z§¶ Ì‘ £S( =/g´’ß×ÐÇ¡»é½Žä“YÐb•IIä°“ Œ$D¶GÍâÄIl„÷®¸/F9Π‚C¼'ÒFØŒ!$"Od!o® „‚²ˆ©=ÛÚjL—L~ ”ŒßÜ&ùÁ3Ïœø^¸Òì–Ð[T:sø{'àëî%F:€‰˜*+¥B¼¿èÎaEY÷)ZÅ<-Ú_Mè¯È£½®Z„’«L„)À8¹ªêÂ)ˆaj Ïiž{ÎÞG~Qä¦qz`Ç…ß4Y‘ ç‘’ÄQê8ø¸tŠËG1 ˆÎyÌ<3&§Iš.ÀL¡¤‚ª„ÓnÐW“´¹Ð9P-)‚‡¿ð\KËsmï2ΈèP6›#NFòƒG9ñÏÿ|¤E´9Œþ ó±ýGÀiùò2üXêyT(é`÷7ê ÁËq蜣_ ô8QƒšV•–cÏ,Ek(eƒFú8÷´ÕZ0:J{ªÓÏÏŸ%j0ô1øeIþ]*ñØÇSEߘZ0÷|-¹€yúixüœëa?ÞÛ¬"úVw—Ï’zϘ¯õéõ¹§›šžÎIžüñ·ÿû°¥]äßgá˜90«÷©²Þ†X»%"k7%`âe@䲿å(›†&fEÇ€³\RL¤^â‹O`Æ¥T. K.K<…7ÂDmpD“"ç’¥‘Ç›-0¥† AÕ‘x?ª×9Ø"k×JÒ­áRÀèϸ,æWô®°¹61\ú_ª ´ŽÑ1zz7wm~Êëñ„u2½ÄÅã&SÌg’jÀ3÷4̤ 6]­U©ø¸£pNÝ.5À9e.VÌÕgΉú™#5pÌŠ™ˆÛÉÁ'¤³P¨™9«­Ë¡³  #bG{ JìÓ‘Øs_¥#¬ø?œ}€UÕÿÜ)¯·y½÷Þ¶¼}ÛËÛÞ’l6›¶Ùô^HH!!H %„„ÞC  4ƒ  Š‚‚‚ Â'*ЍŸˆ‚è$;ùßsgÞÛ·› åOIvß»3sï{Ï=åw~¤N±b‹ôÇн˜èIDíüö2S¼#ëȸuœU­4(y ³›þÃø×¯ô·×øUš>³–µØÉ˜/Âkû%,s8j¶T÷G¤1•`$zTtIJϡÎÓ49!££MJ I¾{?K_N“ç½DÿŒù>>k¨³ z5G˨’Ëœ€—„r¸8+ r’|]§JIx*ÅßÀÛQï|_²”|ºé*5` U2…±½Ôö2ݲ'<n²;$‚ê—N÷iäŽÆL¦‘wVŽ-µçæ4ú*Ü"ˆDãqÖ;ü¸Ý¼[G$F›?ÍD×-Þ'XÍ ‹–ñGP¢É뾇ºBv­Z¸æãý&‰á©11†Ï«é ¡ï$v12/Š})š?)€OÏ+kÆHü  š¢ ý'ðóŸ¾ÄZ9Eì~|ÕÌîiB÷x úgô û³Pß eÆÒÂ<Î¥Úð .‹Å(–c’ƒŠØ I_Ã'„Š·cYnˆÔn˜ W`©¢Äï/h “²ïåŸñ9é%˜Ÿ‚ùy$Bü){gÜîä¿€ù¼Rþ¶ÈØŽâõ™Šk²Gf×Ñ?–f¿,®‰¦“eé¡"Гf'pMÑH 6®iŠ©XÄ5•ÑéѨ• grUz±)Cž„kz#âg´–u°iv2—pMwâê||¾Y¨¶£R”86‰{“\(²ä,Ó¦Gå­¥)"4ðb½z(Ïç>Ã2êš¹qÔìÔ>Éܰˆ;S¥ÿ­5eüúø ­þ׉à7ÈùÈP§c;t¶ÃLXbz¨µÅ ­èJŠV\ãD<Ñ#¤ÖÈ!/þP!šžÅÆå F N§ÃbF”Ûåð8=6«Ùn±«•” ™HÂüT¦–ºÕ”ç¢èlÜÏÊÞxÿ=Gû}ͼaýÓZî¼p×Ê–"¨eeWXX»a -[ãXNÿ³IÖ•C?•/Œ# œGσð—˜J¢ë²Ù¤8•ÛÇÂ@äX»+¶žÔ‚ïÇñÌÐ=í'×k4V‡ÛX¬nªzêçÖiZQíTkœr­'ç¨hO˜ZŸÞjóÚ<æ`usuБªmXáf?Q+j… ëJ¦`>ë¨tâ9vØ8ƒ½^«Ô´û+ºl*­Ê¨0ðzS¸¹2ØšuÅ=ä;±^ù/Ù *5(ø¼•ƒÆÒbxÔó ó „cðDÝÙIg, ¦Ü¥¸ÔV=ÜØ”7­EåvZ‹¿ªT¥ÛÉ|³b¸)hñó¢ß‘_f™5øÒ·aú(zYÆQ•Ô†‚Jƒ­Ó(Äž:Š0K,±45¦D J®¨,ÉÑÖ–vô$¯H‚ÒžÔZj2ZÐfÓ‘H Äóш¢†’·‚m¨¶I¤ð%oqkV¦›Uü¹ˆ9¢3Îñ+¼KvïÖjµ õLS´5[¥WiõÌV[²1ô¡‘ï5Ÿæ¾ôœwÕFö ¨nj¬ºiƒ[Äö\‰åå‹Xwpâ·qVAåEJF‹XeQyˆÊ±¤VÉhF‰-ЉаZAþE9¢AJ¥ Ì| ÇÔ JmF v·‹¢ü>WÈÂÏsò–pì­ H‹‹vH)ɇ„ò&’JJüúsÉåðQ¥t·W%Ú3¶Dâ=|8ëâv=ùä“»_ñ˜ä„2î~R¸@¯­2ºˆù}€ÙNÛKÔâ‚&ŽûHÚŒ°¦ë#¼,ÁñóÚ¥¼PŠƒÁ2Vþ…D Ô ¼^£¤*P…¼”ã@bV$âk)‚³™ÇoÎCT&)Àøõ¦=±†j[Êõ:ž³%rétMÂö‡7bNÙr ÑžÌö@ÓP¦~‘ƒeæÛ<ÏÒUÉLMM&YE?ë±ÍgÇ¢úÌP“”£~€ÙF/ÇïÐBÄ·ù†H¬=‘o8Mâ—Ì4<#ºÏß²°®nA‹ÿ¾Ë„(*¼U=§ÉïošS ?'CÅ~œ‰õœJªû`›„¼´’ŠÊ4:É=d†ÏD©‘8æIß“0ÕLªL¯Äk<‰¤£iÞb‡Í RF?¹´!ñYוF57’ÍþÇ9CŒg]á|ÐÞj£|ÕÊcd®óX ŸœËñ 3!­9Téö7$íMØ6VÍTuï¾—§yƒ\×À2lãdðœD¨çEDÇž~®Ò/4]¬NâÁmËÅóšXlQ‰qáoÓ7q’lÙ©9X<“B:Øä6%¦’)ðùÑÑÇyK(NɤiœÈU)ªŒlx&i§E»fh0ص¶»{̓л±&á¶,OÔ¬Ð(·>ƒ®á2¶¨~»yiwv^G,âì¶)TŠeñêz­E}ñ>¢ÏïÁçÓM²™XŸ÷bÓ¦¥ÐX¦§3 ‚fB•ÇâÃï5=öÅü1§Ãìµx?GMÇ:nÈ˘H‘Ê,BW•õæU ¬¯j6zæû´Áw„ßÎ3OË«d3³‹Œ-¾tqEþcìÀâìøÆdw.×4¢ŸóÑÖìXä²`ÙqîwŒZ^ÐyðŸŒÐ2‹kÛɱÑ&R"+"ÿ±‘|S¿œ}%–¬)h!yªâ[¤DÇ2µh–£9Ç&±bAΚázo%I`QA 7Wt×­ØCè%“™Y‡Õ…™x5ÛÌœ†®Äë×JWP[ðR7 ]T$5,æ}¢$VL:„д…ÍÜ™ ±û†D<,’ð°.€¥I(áqeP)ÙaTtD[)k  ˆ,_ŤzQ®Üà4˶ÆpÈ¢¡µjEPáh¸]e²y£iã>-Þ½6”‘ô<Ú†åq˜º  ×ãÓ‡b7IÝš芌•-+ü–³¶ˆBÛF6>JÇÊ[JèË"©‹ôµL&º”ds¡aHb¬šaR ‘Ͱw/N$üî³2ÿˆ/+TôQ¸J(n¬¤5LàÕ’ÒPlRW/hGÀáò©ˆ0Ü% ¼ì¤˜ !±HiAXSÒj¸ ¹ÝrsŸÚË'“¶üü¶ï=`ô»-!ö …z‹Îp­Å¥°ÍÌš2.šþºÆe²ÉùÙÃDÏÊŒ"¯-ÒœÐRí1 õ÷6¥FjFÜå»/ÚhXåsr¿më™2£ÑŸ° QÞKêàó½“9Ètà{Âùަ‚Ì…ã€Ù½]„@%+'ð$ò~™^–F6‹Ö®³«•HCkdÎéã…Ìùè^îXVÉÅï4D4x*¼È楞®­C›cب˜9RªfŽºƒ•Ü#Ê”§aÛÊ™VoäeUÍï¹oð²Mh÷3‘ß¿Fò´ H€G›” ’AIW©ÒœŒÁ†½²„ðj²]ã– ìD:™n ‡[†3É4‡"ÙîšþLv0ïÙEöÆYø¹ '?WJ»“èïuE‚à±,§óëJÏÅï<_”¹ùêÚWRÏMq(žÙåÉf3ý5îYxî–™Ù ^¡kÅP MI’,‹âo'¤•"||øµÊ ^À ½àµ¡ì{|âFCq«Ä@#·„ø2ÍbÖ]°$ ©ïÔtÆ 5µŽÛi0krÎÆÆZkãš$·0Ú×^cü;*È}ÉJgs­F«Ñ“d5_ËyI§á±Œ’±l‘Æ‚…?å'A. Œ¤ß¤±Ø±‰/¥·(ø¸°QlD0=̆“Ï%¨@(š„¬³ü£¦M.›€£Ò6ÙSí)Kj`u£µ®¡ÑY­1œî°£6gˆuÖ0»ÿט/ôG›ÇZ|2S"¡ZMm³³2é“ ß‡1dhæ1,CŒDÊKQ‚hˆß d†Š¸ôAøLˆ0‹t-±bÊ1=`ó)Ïàí*¥M·Ap>Šú÷2´ÿ·¿Ùãi|ÄÛé3Dݹø\¾L÷v~¡î]$ôõÁ” |ÀæUî4ØUïFEÀñ˜ðÄÅÜ.üÐ@£§¾ðoÞ>~¹(^aOG{e^"àg ÿ…°hÁ<¬†}ÖÍó´;2v6þèo„³S/Õ«X;ËHuM&Ù­Ð+S‹   ¼¯ry\ʃºqñt3Êš°Öb^£5MF”çb6šþFߟ1‰Lt“˜Œ2”n‰f–nõ5#þU ‹¡úwøú²d -¥ µÛLñ×à&Ùò7xƒ‹ü£&Bžf ,¸ãé;ýé…ïû9ñœ‚øIa\V6y"i6´!ü°é¤/&ž©v ãw>}ý»;…!Cù3ÔÀuêpç´ÁNè2ÜmÁ%—ܵ±ÏGñýH\oSß2„cqW¸K/(s/½IPNj©ˆaÒ:yâývš¶àšô;*ŒŠŒÓþ J ´9ç5tί6_O?·.ëä­q/¯6ytÎT[Ÿ»ŽÔ›¢_¦·aÝp2³ ªdÂkVs"Iè5z°pçô;°Óm'Qd‘"SQU…Û©ÓPqçŠ~Vº 1ŭ׊¤ ¥:›è`ŽÉË}#hY²)fdU&½í©ky¬À)XVÁuÈxþÚ§‹Î¨æŒÑ–„L©Ð©äZ¥‚~Ù‘jè]Þ¨;…5W©-jµEuµÆ(ÜG§=QSµ’þ¤1cãÕl°ò#¼'0& Üí×o1Â?‰ëWâÒ5ãp·¶Š}¾ü:9å†Tx)WÒ“H#²F ’˜‚¾œÝŒ*?[Å\÷ÿ{=ÓL¯øl½bÒõAl5‹ÜwÓ.ª¤¤ÀB'Ü"àÉh#ŒU¿É\{ÒøÉóñR˜SìŒ?}€ÿ×[ìóŸ­"µ6ÿD‹uŸUàY(PÌAèÀÕˆ–øû|©%àùZ3þ{fÕøSw•]+§¯ˆô(«&&Á<¤ ¯çCpáÖï1«„ßÝ%¼-ÊâM'Üô"”†œªM ŸµSòc‡©¯±”1Ÿx€ÊŒÆ  äX Pà¥'î­¬èOO%3™¶m!u*Γr Â¹l{$L£x,œŽ¤]¯Ó¨(D‡”Îîû2VG?&;BÎü3¤¾K¦B\Û‰"#¶:Ú‘ ™¨gHÒà'¤>u¸ào9½À/}€`bR4îháJ8ê€Â®AÅ”Ã8…uJ‡Öã Òøw™btjûB¢Ø”’‰Ç(9´•ŸÔÏ_UåÀ ª®ª®¶&l•ŽJ#¯UËeÈJ[5N¨‡2ëã±=…ש‰^²¨˜"i–Öj(ŠAˆ/øš,[–¨ËHRk¡o>uf[ûfÅÚº[v®ñ¸ç­ÝÙÚÕØÙךu¡e·þý¡Ýž{lŽäægo¸áÙÍI‡íO÷އþ~+îÇf¼°¾ÆUbc±ª…·ÇÐ,!\ÇúÓX©†¡Ë×}n0ˆ]ù §á¾Ä{ékãÇÞë­µ1j»e«ý­ð Wùª°ß™ŸS§qÚtUñÿÅÄsÖoÇs`¥’p"ªÊZð"â²,Ä™ˆÇ“%?dBeãj&×8äâ=<±MøtÛãô,º÷¯W\ñ—{ ÿ®Z¼wî\`r_rñܹ{W1ÑUß¾t©Qø r—]úðª­Ï]=gäêçNïÛ>”J mïë?cv29û àåÇï©wW+É#@}-Éh‰( ˜°QT¶:SlR9ºŠ¾_xþ[‘ÿüø(jyÿ@ŸGqô?´IÞU<öø¾ à.p›Nâê¤4$P O°Õñ© –N¬oO"¥,ãÄ„¥èÇ4¥Ò¢Cj5o²ñVø«9ác}I³ðW–~ÏÛM<˜~õ.­Îm@›µ%óÙ/˜5žž°Ëð;ÐþP—çø­\%£4h…«¡¿z<wrmT%US¨Ò"¨Œ¨ÊŠt*FÉDRí±éG‹ÄÔ"oOËt%¢¦è$Üu‚êM"ºã3ÕÖµ2uPó5†ð`>Dýs¢K—>:?Vmžß~Ñþkfßøç[†¬ùymwÛýZ¥ÁÖÒd­1ᇛi½Þ9þã|ÝÓE2•fïy©ö´uÑCŸÞ1ëîûÚB “ý»Ù*í/ ×¥…âR/ü€mã®Â':^yX¤Ú§TžÛLu!*›ŽG©ZTËÂA-®1›X…#&P–Ž"©·$‰~qþUk:wÜ4ɶfS*ù”‚×+å¾Bî7î^¾üð¿¯m‘6q-v “½’]Z»éΧÝ~z¯ÅÚ:0ë¹°£Oë†ôÆ€×£uo}ðÏ×^÷¯–(L>»ÞbS66yE[ü欆»d|'n¿üŒ¢öÔÕ,ÞÂâ-ñ¡°øP¡×“`ÔUŠsPÅ^Ù/he—8èÄÀbÇÊË#4ÙL¦>[o‹Ç32oJ¬µÄÌ$õǾ#†xU•ˆ’ƒ¿ˆøn+’~C…pVâu´Ä#e#!3±*w)$:… ª¼9UÖú”ÔQ£Eê¨IíJ¯ yc(L¸ É”ãC¶˜;©’„H*gô·^ï‹d}¿Æ£ï ­† 7fêÂv}<&Óf v9v<µ·¯mïO/ó×åk½Ì³SDZ,»sãƒgžÇÉ9_bLÇ«Ùî½Ï~mó“fqJR\Ÿ}xþç&Hàe3>!ÈüI¿0eó'"„GK0f[gÑ%5cKó7ˆ\Þ¼õ†Ö§D.RÓµ'Pd˜½h}˜@‘™rN£‰ÎÚ:¬niÍð˼µùZÿe?ÝÛÖ·÷©íήA,Ó;Âué@Ý¥ïÏÚèˆ}z½Û¢g˜Ÿá âfxróמÝÛͪyÝX‡§ñ¼3ܸO+§sšE(€÷Ç=ÜE°?°?ÞŸ·DÅ` ÿ€Â‚—†R)QWqd5ZDßY¬Ì%N^ü«_hG$;sê52J)—)ÇNqžImw§ÅµÅ"±hH’âa…°·3Q)ý@3)¶vìxù3ˆT6qx)±uýO 98õsxÞã‘ßв`ÓPÐKîcyE2ŠµÒ„ÜDÔ<ð'p±—fø¹@}¨¥¡§uÓPåÜ5–ñï* dú |¼q(ƒªë]9 ­g¶üÝm+ëÌ·a»¼zá¥Ï_k· Ú oÂÖšoçͦª…žßm³¡Äšæ'>”b~GE©~ê¢#Øö“¡b6½)ñBT_†ÿ[+g¦Š»ð¢èt&þ²í®‹QT¬?ÖßÕÑP_‘IÄñÓ#¶x,¤1‘Š:)TªŸ`ƒ Y€,9s)?™Ž&r/[ìP¿¿mi žïÏ0—àù6S1¨=Á÷A†:Inj;EVŽ"×3exüd® <$cóŽN_zÿùM›o^:x^Nbëª+‡ê|žÚÁLr ÖOW•w·2-ŒÒ[ó“úˆec‹ð,· 𯪷[@6þ•¥¨›¤ÒL*±å ¼œ¬âÉG–Î-‰ó‹ÚÚ‹m áS5€ËI !ž¤¤YʤSÉ`ÀïCNä˜Qí]b+žÓÓðœF©ä;…ÜNl÷E‰h6Ürø¨__$|‚uU,‚Íq<`â%­`J[°gË›B¼ ›9Rä_Š êhšbïHüP6f‰,ãó/««âßèYÚ€­›¹ŸnhY¨ßýä…ןìIØ5©9»¶ª™yX=Áñ'˜üùßßÓú‡Â3¤î¥[¼¹B¥VLiEßÊTbƒG]v¢¼n[ÐÔ¨)+²Ê&bµ"ï„­­Íâé8ĬZ|Á°0ÿÀЍ¹c TË<ùªý®úÆž&áiÔÆÏ»àþµPó÷øswÝÕ+o—üvaáiæ!æ“ì Rþ‰?•t!~u8Ꮏ‡cQ#zîø±ª$z“þWUŠðÃáqŸ‹ï›„ÊA®iì ±¾T§HÔp$J ÚJ–V¾aÍ`í\²„|ˆÐZ”U{ÔF³ÓxìO V~iÎCYó/±ϼ{Üè4UÐïò^]áç:^É÷ÑaGSK‹Kø®+p¡>WkK“ãø-\ˆQòº×È<(q0ïSÕð–t’TIDZž9d“:ŽÍ hÑ "$dœÚÊ¢ú_ÔR½*_’_øÔ¼xÞ2£ýÂK®ºîgzš·Þà häzkwùGj„ÿÛDS*‡aücôூÕF‡+›µ!ÿ©Î´mÑ}ÿºqô÷Ü7f §-Þ•ûÞ¾ ºóvá6Ñz‘ébSXìú n5¢Û¨ðIDŽdR¡€Ï±Bb¡’-@µù Ñ¥£Q³¿gÛðª…Éää¼V™¬¹a[góÎÃÛºú·[:Ò½¢¹we‹‹ŽÌÝÐSíMíšÓ¿¿{P ‡õѳ«{ÇÃû–ß½»+\­·9”­#9‹¯qžˆ¿Ë ÷±^îÜ×vꪂôà| -—a!+/rîTBu –Ã:¸¸Àh»¢LÓ•8G‰@ƒlô/‰]ºd´`¨®j¨«j¯n·Ä,‰xPi™Œ¿ÂzÀMA+*^,M,R[F‡Îm¾fñüËVæ[N?´<Óßš³‡ÚFóZ¯ÓXˆvŽÕjà§à«+3±Å¹šyÍ~gn³©¥¿kÏc[Ïzኙ¼/íúyÏ’::–^º®À{“ß̽t}›p~c0˜F†÷,š½­äÉ´÷²?ÅúdzP<Ú]l[ø4ƒ°uÏh‡ _–ã:ˆ.¸¦ÿV&~;*Þ ÁMÄHL_âÚ‡êc$¡bà–Àw‹­8 ”²©­‹œ%¸ÓÚÜÔPSI9í“_n€qY¦ödÅ”‘½ÔVÔ`§B@™8ê—÷§˜x…%Î ž»šû|Œ7ïo¬ënZ3˜f¶¤ ýé®–^?ë­ñßëïÝ6œ®²ÈzCÜU3¤gï9zŽ«’/×Z•s÷>µÇ“3£.|¡u×âz“eרê€=´ÿÄgè z5Vn¤òøð)I-}Š §ýïl¾Š'x2èÕÂV§£ËqhuIqŒ/pKd—R¼ö ¥ìH‰Î¨B®ØÇ¢—«‘œRRr%djˆŠ¦%ˆÂ9EÞ«ìD{0N¾è|JÇ¢-Mù\&mµói…Ò‚Èâ\>ÇLÄ7¯HÇ ™¼奼gI{nq¬PÚ=AÓ ö÷Ÿ Foãæ´YhWÚ½AÓB¦0¥Íï…³nõNgvÞ™ýýgÎË:7ªÝYö÷zM÷Ù/^GL¬rCSÍ¢À¸Lï±ë¸ÊãoUmjÎ/±Fm >qVV;É™t}@‹‡«Â–Àâ÷ JŠ0‚I¬½0%´‚Aеu®èʲٰ Ù‚N;q‚òñÏ«-¤¼UÀ2Á˜Å  pIŠz5?þñ…Oœ‘o?çÑBõáÃHþãö9‹%· «|ð¹GWí\vË–&æùç7 û½¹®hª§ÊœÅœEüÑBH¬H$ňÿ]#¹Ùx1hÊJ€H7øs–Eá/Â'W_Mÿ‘]'Üùƒñ'éžÐ |ïÝÂãlß;Eí)¨ÌqZ(wÔ!±nà €Þ±bá/ ‰ÀtÊ¡*Ñ•xÑ÷/Çv¨œìçéš+ÐDëÑ‚‘2tv«ÉÈô:µ’J¡”RŸâ&åÀ½ (—¥±V‘¥ÁJ×…ab`]KÃ,¿ð ãÿkõÆÖ%íÕsþw6/_ÍÊÆíÔWl‹/8°|Ýá Ç<þWù¢°Çf¯;çg×xó¦aGt(juj°®þ ò•xü T5T˜`iP'@‚T24¦UÊâûV©¨¹£ƒå“i4êJ­æÕ]ÕØÑØÑÞÖÜ„oRà#XcÄèí)%"˜’JZŠ•‹u´Å„ÿ’ê‡ÀÖŠmP5$gŠa€íM^vµpÜždúüˆ=\?Ûñ„9ÞöñÖpÎrÔ1§þÛHæïËFkì±_Ä:ÍOš3žE/òVŸ2wÄ>­ÿC6r;W)°©ƒ±«aüçXçº#ášî $„•É*ºª¡ËhhH¡ã‘°2î cão:bèP(Šw”ÐH F¢ï첆ÂsÊá" ¢4©FX¬£H,bÝñ¤DV døÁ&© I³a’†61)øe£óÖú*L¿7åÂïoYCuƒ¡Ã¯ãîïUY_1WEÏ>A Fé¡oxcã„2:Cw‹p«ØÉX=u? èïJ¼¯¿… Q«I¾¿\ƒ™MâÄ¡´2Š¡e¶é"Ã.ÒÉŠîv€H- P ’ÓRKéûÑ‚Šì5k< #,U –ˆ+Ô|9ŒK[mp¨žàkÂo ÇL5þÚAÿ_þѹGq>Ñ)|ŠäO0zÎaCßôGÈèôݭ㯠¢Çý.ö?ÁÈÆßѪX°â±E©|¡šG ‹•’å–XD‰`ï‘·ÂvE!Mž,JäMˆ '¿Tþ.H-æ’lÏ ŒêdÙNþVºs‰Æ¥]‘xߪ¦Ú>%+|Âj*×6ÝwþÌîsÞ¸t'#ò=[’ïnÓø¯aðø·ÙD3ˆø˜Å©}‰q\ ÕGÍGÕ¢«œ·aFÍ)Ûd„R¨CrŽ›'¾ÀF˜ Ø&Ù|Ìžôñ¨äw‰ò!â%¹ƒm°„ŠN.1È·ÏLÛ´ä ³Hž”‰‹àuÕP +c$_þÚÏ¿LŒÇ‹×Qe—AºœvÞH >^JÄ AcD´«e“ÂòbxD »6¢“8¿EÐ I…ÞÈÛç:j¾ÖT·»ùÌ‹÷þææ‘‘;ÿ~“§ª¢Ò¥ct§^çôÇ\ýg-¨¬\tþl¹VVÓŸ}ÖìygEfuñnuºÞéÍrãïWFrPf5Yc3œžÆ5G>½þ†O]Å*´Ê•¦°‡gY¶eçý§m~`g ê{ißòƒë [Î?ï—à R§bèoÛo¨¨¿cItvGr3ìG£ðó.Þ/:Qào(¼‰«R2NxZªîN CßÒYÆòÌ»Â-÷g#‡žná*_?~º¸ãÙE¯Lȇx{×ág¸!ϤBÄÇ!—Ñ4@¯hfO¢b„ÝM¹ñÞ7¦ƒ{‘GSkŒ‘l7"«ÑµÂ²¿ùcºoZüµÏ|·:ø›°ìÏŽÿ™¶ÖÒïúÂá&·»)„ö˜£ãΚ焇ÐÜçˆJ/°¯á>Áïâ¶€ùJ^Š2öw1HÀ£®X0ñ2 ÖjÊðb•n% @¥šg*¤ù>½W»|êiýµÛöèlá:¤pÚTÿ2¼¦¶óÇèÇ£8G.ýžSrBžwÓÒk B΃äyè#eôo‰ŒÆó&'U6é(¦iÙ€Ÿ¹¹¢ÓNªÎl€”¨—^üÈÜ!<†~Žä_φî8¾U8ÂU¾z|#ém‘s¡ÏËCø™NˆsÛÇâó”£; ÇKŠ1QHSSæÇh¶˜øHëHiQè”þÆÖÕÖñVw(½ý±UGoß³¨ )¢«܋䒢E«œvuõª›6¢£BÿêJY€&ks^7²n–´Ä#h3ލèvµÅ7>@Ö‹³BIîT½´” j+5”ƒQ¦l¥>næ-¼…ý¥ò娎̕ ²šexÏ/B cf°áqGráS¯ÿñµ÷ŸÕ‰5ËþšÙõ^„m ‘qôæÏ~A«”²^U—ß{.îû¸ïÜwžŠ"4bž¥Ë%Ò9BãÙ3I"; e𤆋3â¦ñÇןús?ÀHÆÿÓÏ|ü¡=Ñ¿ªR¿d¼ñć29×…Uð.ꂪ_ëÆ"»O}:ø„“A†">ÌTD\[O ¿OX¿d[ÂïhmÂgz¡­±« ]Ž…‚ñ0„eÑÏ“ƒÇ±bèHô£°°„MÍ2¬ùúÿì;ïW7Ï—ó^Ûñ;ø×ž˜±ma—ÏìW5\|åµ=¼{x‹5m]7š/óØÔ¹,½{Ö9‹ÔôC©9g1î/]?oö-¸*VåQ¿ò“H&¢×­jŸWe¾ó·Yê–ψV¸¸Nµ¥‚h¨bá93úÏ­†9^!sØLõRs Ã<‡>·Y,M š<# ˆS¥‚y¶ uv´ô¶öÖ×FÃ^Ù¨×b5 5«Ë’‰&O‰Ä“|Ò¤4áY´ç…yaú)I¯Þ¸µîÃ-O_µH5iNLãÅóÂuM“êþj¯\øJ–z벩3³øfirˆ¼Ü <Þ†×sµ» vc;< e$1*²Ü“T°1ÊPŽ• å“›žd$Wfc>áô:­Ï`ª*3’%q6¡#Ëä‡%Ky0¯gW×~6Æ…ùϦ\䯬¾ž=ËÚμoCÅh+§iS—Þ™sþâÓ.™éí’bïдîxä¼½¿¼n–F›³¸´ø<Ãv3çÃsᢖ‰Ã3+5%ÎtZK~.†,Ïv±UüVÜx“Ž”ÖPTR~#9&d‡Œ&Ü)-°r£)Àž}ÏE'æž'üJ“iZ¶¹¹»¹ËÕØ˜OùdLšøƒØuŸ7ŽZèsØî1\s íBJ±p”½÷ÝG-,b²ØÚ“—pnÅLz1”…B,¸îNnYt’CüR4 -(@ "“U[â šPãÙ[kÖݶ~ñ*Ñ_¡Ëú3]iëø{Ä€p"ř뾱³MRb­ùùíè•’†O¸±ðükðô ³ÓWÅN±¥£[šN^²$ K¢sn¢ëî½H8±à|AO¬‰‘‚L]™9gð 9ƒ¦9é‰óxn%°É¤ó0"±s|²L=—`}iÛê#åçá_‚ t µOwB_n^_„û’a¿AÁÈXˆÉŒFë¹ óIä‚_ þ3I%+Ö:B):(EðÜdì$>H;ùE‡NP[.dX¡ÝõFZ}B#ÇþõÞiáèIÞ­sÐQovò/ˆÈ6OGhÜäö6Òÿ u{Žã)¤ýx¥¯#ØÛJ¿¢çåô±›ElÊØ‰9–µAÕÛc€îß}Ä.¡sõŸWoÐÜYÓŠŠþ—j ­‚=£Q&«ŠWyÝ“A§Q¡(U:SÐ'û‰3Ï>¨zÛ}ZŒûÔVh†âÂ&’c;5åòa%MyN†gË*K<©€ ø¾•ØþjäÚྕk>†ÖrEÎyof‰ûüVK4âO’.§ÅgõÅ# ·˜3¶ëëÜO .ˆÝL‘8E[Ut T®¦3!z;haà =¾RT¾ˆãQrá(ñ³©ëˆ\À¯C¡„3þ[EbÌi‘ÏQؘƫ /;$§Ñh™¿v )•„ë„W3LjO))¥Tú:J¥*^6ZÐû½W¤¸uÕv¼Dw®EôæN&^BK ^ÑÛ{µ³iiW¸=a’ùt:'ï³ï‹Â?.Ž7­êK˜ÌË=¼º­™ŒÝŠeâ«xì•Ô^)'#öâcJ¦–àfe¿‹ˆ³49ó 5“b Ås1V@ gs‹"?VlR‘•7ÄÜ¢ÌWa#,kXNa#ã¤N :µÅAÿÒ¾ìn]ÝíN™e^ÎoŽº¯ÿóÃ𛦭 k‹Ó9µ‚çÇ‹—Ù93° ÄË”²á@w`#Ÿ–•[p*šp>ÊÇÇ<þ_iMMê,øàPÆ"y‚±7GxÉqïÞÖ>«¶òÇ„_|üJÇœ>õïd?S¹mÿEUï3r:{Õ`ƒ^¿ñƒñ'q—£!îù‚B¼ÿ$¶¬ ´nÈ`)R,Sì=WŒ;éõz—Þ ða$9œXãø’Q_S8ÿ¹‹Æ:ÿÀ¾ç÷´Ð=ܾ¼Íëk[ÖŽî_q×ôêg¿hÛvÛ¥èÍPÛh]nžÈÛHS­'þ+ËÝDí<‚=LˆÂc3$&g(¹Ø/…¬TE—(àÜJ&ÇŤV‚¤ÑDµ\ (á†t*W•jJ7…±VZ&$QGI¤iBCó€ˆ2UmkÏå¯]þS¼/›:f¶³ñý2•‚Ë-;0oëõ‘Ö—÷nzðÌ­nXÔâÿéàÖþH¨szpù];ð$7Îꮜ툦¼…¦jãè¥K« õ,,l?t…»ª+üÁ¶5ù…moÂrÿ(×JµSçTˆ“3°z$‘ŸÁâòÖCt”ADOd°>rq³I ¼E’ì¯ÐÞájU8Ä1¡9bñ™wò e%%36¥8!ñØ1ß7Ë*+åë©å+—pxžª_4gþ«k=-ËÚŸCßÞuÑÙ;»Vôå,Qsû¬ßí;ýÑóº˜?.¾©2óèþTSÜ*[4§rˆLWcθðÒeÕK/_ù¿\0§o;莞Ý9[|>ž/¶Åµx¾2To¡Ë†Ï Ä{T:ñ ÔXù‰'—OuÜf¨LÈcÇí©‡&:®È†ï>l††Y+w^2xÅHbÞP_cJÅU»Y˜°ê’î\Y®:_͉ÊþJâïˆk/j¶™õ“•ÐS´….R'QK;]d ør÷ô~É{‚ò>m#r•í¦Lô÷DŒ”.&ÜMÙ-þxÎYU×]‡wÁ>ÕK÷Ï›sñ²Üø¢Å_€5êÂp÷ØpÕ#’ñšÄÅž]pîЉrýºêÄGìñb^…Ú¨ƒpX0Ø—eÂHªþŸÔÁx:!~È)4®@aË2)1µyb8±Çg]ýòž?ydå’oýç¦YûÏX便ÎášÑ«V×B5nìúλv?w`@t¬>¶Ú™-Äv¹µfSÀ¡kßuÿºmŸÕví·ùzà%;ñ_öu¢3UÒ嘭’f(±Y˜.‘ÓBZrŸžb‚±gïZš[‡ZXÂÀÜÖ¬¼jѼËWÕ -à]}¦u²ÈÍ.8gˆ1¿ŽõÍÖcßä  o&@íÇî/ôô`¥«+/IBXñEe¥Íå¹]±h< … ‹mŽr.<î4õ˜¨+‚žº"èGDW79Ç)†•H¡Ð‚“ìˆlBE*nrQ•Š•5%+S^tX&7‡-¼œ ÿ”Wñ¥Ô˜J;«g×y½u³«éTcH/êX¬Ø½Å|JY©uš„Ö8€E_`q•½qšÆeÍF Vu Øü'A]Äò$¨ ¦ÖŠH—{ùËG6]¿8‘[uÕbaì¾û>|tx¶1Öšb>Ýúèòêù[[{wΫ _tëø‹ÂG³š•aBH¡ÀƒúÁ–¦ ‰‰¸Èh‰øx"6¢¥´¢UDØZ ðFLvÏ_C¯n¹ûnáÝn{êG…-èÚGÅýºU¸—ٌ } â°Ú-ãL@AR**e€Ò9,ådˆÑ)bç  ,•5ʼnx‚Àh H%ËiÂÙSŒx†xúáüØX휳çÄV_2íã'³4ít¨Ý¼?µXbõá`…M`ÞyJø‹Ç[ØqçòmO\УP›¼.¯Éâ¹»ÏX0ku“ÕÆ ·áqÀ˜ ÂQÙ þ¹ƒj-4e4‹U5Rbƒ”ì˜JAŠÊ¡<Æ$»¸ÐÚÒÜÔǶ €4‚{JÌ5'av: žQĬ0)d1+ÌTÌ {-ßúÉ+¶Ê`E“mëK ßùŽëN}]¨ý¿í¡ZÃjw¤:xø[SE°ÒöÊOB üe†d´æ¿UѬž9ä ׯ*uú†ôï…žP $ÒH £è›Á°ðkÀ¢B·¤ôºÊÚò óyÐ Â&*ùó†ð<|Š‚äSrR’‡`Øô"¼ | "@%(¢7>wìâé;Œ…ÐO~a«Â´o{2á½3Ü9`ýš¾>4K:¢ Ã4ƒÈ¤ Á¼CºŠ¨øh&ý¬/äCH&W!F&ÖjŸnR‚£Ån‹“¬4pL1“"Єô4Ÿ³à¾¾'wúÃü‹ß2ã×ÑhþÖÃs #ŠíZA¨}À£gî0Øy¥pm¼R«¯ÏkcY¤uzå|,võ7¯h£â¼>Îû¥ uf€Du¨”4VjDøV¥8JÔ’e†{-$JE%¼ –öR‚’«š9 Î[÷på•…hB9ßúÉÏ`!5Ú^þvg¿u—¡64C¸ž9°pᣗz|Èæ-ͳ°6“Dj˜b,OÈ.¦Ø«q}Ÿƒ5ÑO`MÂÓ`M&u.*ºŒ8ÚØë/…ž½d«fm?…žáë„÷Å×{r¿ºñt†È<îÆëó<Ü/g"é  /sÖJǵˆ3!¯\1eÎ+ò’<óò#qÿ?¾)¿²ão‰ë }G˜…;s9‚œåux.Þ-ÊS©ì(a2‡Ú¨4 Sä)’wâÃÐä¤×7 Â{‚xç§Ž¿%>†ä/ ÷rßÅ?VR³ ƒEœI”àL¦Fô'Lò.DUd¦ÀE*QåT¸ˆØ•SÉY6¨‹Y­ñÆH¨Ê.O L[|qýȹ#‰Æ5fÇú$v9Ôž»K"Ô(ÜÎD¿…‡”ôøËD­Çí1cQ çkÓ‰Ù]̇TšA^0X±ÞØUOsJ€~pEyù8™r¬tFœûø¢6Ä-«ì7Æcñ %.\Hd%eÀĬIÐé›Ðd´á@+[@λË3;ݹ»¶îÌ–Å÷žÓß¼ëðöèPD/‹úUNuÃH+ر¢ Õ4¤gŸÞÕµ}$;«Ãb±94‰¨LX ‚ᾉJít8œÅÜTnCÂŽèôœ]ý3ÏžŸm¿{ÙàÎÙÉŠ¹Û;}£›XÞÚÓ±#ÜÒÖÚÙ&Êá(cÂkD'aWËOóɈÁD8˜«P1JáØuqß Ÿá³qüq­3ئ£ÔÂ3Ìñ½Í¢a ëuˆ¥]E¿:È3ÆTVòT½N¾þš'¬¸Ž·)•þyØñâ a;ºœ9è2 ¹ÌüTj~½br‰²°ø,7•,ÄÌj@[ÐdajØ:z)Ý ?þÐU©ôJ.?M^~Y¸æšizÑøè]w=Z?}?±,‘á~„¨D!j%o‚ØÐŸŒØˆÈ4=bƒô¨U¤O¦¿ñä7ð±=ÃTb•ܹjŸãÈ/¼ÿ±Ë¡ºÓ(N³ÿFk…Ãâô{xÔ¤6+„÷yý;z^ø…Æ¡ý,,ö‘Ã}ü%‘w“0ú/‡Ù‰ˆÉ’ó‚£_xE„{ð™;þyç™ñ›`÷J !$>³?3NêKbÚ:³±XF¦èd̆Ù8 f_£Ê1gbY{èÅs_½yžhÙ··¡\›pÛ±6¥ÁÂYëZEYÁðÄuæŸ>ýè½åú:q ¿ï•x\/.ÇKè¿ ^Bÿ•ñ¡ÉžœºóÔ7/ú`áåGÔ™–™ó–T4TÕ¹ëj*b%½ï)ð>0þ#ÂgG¶4ceì6SÓæ#Hþ8ÈáæÜï Ø,JÂQT„@Hìd´©X n>ƒH€Ã™¦³'´Ù? ê•×,Ov%¥Éu¨Ü^mÜ&Ü„õÛõX¿±ñ†e)¥Êä…é´Ëô™¹]èÂ2]wžÛ³põŸ«_—ôk=¥çK8î¢×§Ôç^õU· ㇃r sS¦ZãçÕaf‘ñØ.Ò!2vØ·°¡ª¥¨ˆ9©’-OL#;eñ v„ŸrÒðL‚žµë‡W îöÓf ÏÆüw>õ¼WÖvùvô}AÚˆ=î;ŠÓ9oæ G¹»™?RqÈEuë8†ú$ÄAT„p_ê;Áì#"ÃbD„çÓ[͘–Ch²¢Ä^zšR!üZµ¹ìµð …êÏ¿tæŒïón­ÆÍÿ¯)ç|íΫ? ‘¥ññšív3m‰¯ˆŽÀüÑæÎðv…B]^tyÐrüÍñcÿÜ0?Áò/¨z»|´üƒãËàl_ÿ`; ÿˆÓ(ŠW&*}›ÅhШPŒŽ‰ø‡Ÿ ‡™Ëa­«Þï0ýgܧ†B-d¼¸qÇJÚÄzSeË+ßJkÝ Ų¼EæO‰8>'µâ8 pÏ—ð"Y/pš ;¶Ny9_Ë4I+(ªßÝX”ŠöŸB¸—øì‚ÔÍ¢œ‡B³.ø[VÂKdñä4#_¯D%×E©†ÕH¥"®É©é¡ùâ'—É9Ùç\8)SÔ$ ž€_ôrCÅp;ÉÌ#î‡iÝݨ ÿ>ÃCžöõñ¾ŒEîÓëæ¨û™ð€ßyô—[w-©7YÖzꎶºöQtÁŒßËüÔq?_¦Z‰æº&~ãʰ4–M´l=’& K¦1â×.6ª2–›ÔoàbËÑ‚Š %Œñ€ˆ•(Sä±²ÿEÈ Ô)üôú9òkèoÂÿüðFodžÁľ_§ZãÞ‚+¨~ˆUË®Pû”tü—tnÝga!%<A/G€¢µc3DØó¨0Rf£ o«çć2w;•Æ:kO¡3‰äl!¹“Ô-ˆŽB褄\^ ²bÁ-ˆ:f3uùL[¶Í'b$Ý{ÚÖTÛÏ‘0?a¿³Ú†®™-ƪԕ2µ‚˯¼báúkµßÞ¾õ¡Í…]ßX7zvµ D*ç4Ò«·‡ÂmsѼy}ËçUÏsÆ*ümyÓ’kVçgõ õw]øô×Nú²¡\šVóøëg¤ÑüŠHlöÙsçíìý‰pXu¢KEÖ(­¤©+ ”\FËä@ûE†'êÒÚÊcw.‘2òË´¶3ÅÊË_þÞæ¯toûWº·û+ÝÛûîýeo;:*•~á“Kõêh¢Šž"J”×|9V!Zš[Ÿ½üàºZC¤95²¨±kF³±:yGVÒå F®XÛhÎΨ۵kՆţµ3j#£C×Àì¬ëN®¨q5ÕW][ÇZ[–ÍÏÅÔZkZ|íê|åü3{#Uͽ-¹Ê&³Ý¬µ®$û9‡õÇP{¡UZ?`[—UBFܤ¡O9\yÁ‡<'»„´ñÓ 4–«»kûèeËs†pSjpÙ·ñ!ô<o¯Ü´iÞ™A=Xq\î–†j£w×è:É@¯wg|†‰¡€çÄG2±-€ç²™),Êõp 9«©êPÂÕÄ1zÊXõTM› VzjùªäUA´ü’¹ÃVÕýYSÕ5²du®!×èokªÍTÌ;ãgÀŽÎÍ-ßÑUc Ÿž¬Šãþ_zâ߲#†—]2$rDƒ~2@¢à›úm9ÞÁB—ÀʉD ìÖÒthQ5αáÆîXm@g„±Áj[uå¹xµ +±²|HåÈv%'‹(Ð’ õ|5'þÃ*˜¨ìçàlSðY*Œ~¼ƒuî hÙ~×ê3tŬKxVë–¥3<­|ÞËÇk:S­K ë¯Z»òˆÕI«o˜åšú™ž²óúÈ&OY¯=%âj?fÖ‹º%ÁÕ¾þ[‚shÄgZ1´ÿKá&q Ö×Fñ^ —ᔽìixÌiê!QÇSI8Uç ³E  ýô0G æhyJ”ƒC$,G9L{Aäà µUÂ7”vlO¯ïd¿x·§cý`¼_Rü¬QÏóÓZÈ“5žémå<òw¸¬þ^¬Î² ¼ !ßê/–}€ßÌÁ”\¡RÈUë!â†TÔ(Vx9¢ðŽáq“Át3Á9kÙâT~‰ËÌ“.(òä+(_0_ÌEÜô—á©ÕWUfS¡HA¶}÷}ë5tÔ½Ä@›BFs/15¯Š-U:,Æ yÀ_ïVÙÔX3ÞÒxÏi:â^Ê›ÃFüƒgÉs|Ä4¬ué8'ü *Ý187¿àþŸqñ{ãÚg‡¦wÙã¿uôfÎך¬†ã7©ã>¥Z®:sîÓÕ¶ø 4Ã\e‹£È4ó$ÞÌBpëá†jøµ(K’X~Ƹ6ª“º¬`‰`éÔÙJ‡6MÕ[4 C³™~†Âz…Œ“¯—ª´€DË©¡¢µ'‚<'´°,n¯qŠR{JW(¨±‰+¨‰ ðKˆ$ñjÇ'וÉyi¶K…¦_Ö6ž¼hicþH r³Ý®JW™ƒ •EmÑoè{ó†K~spxÁ¡ßîÝ÷J÷B¥Íb‰5Ìïìß=¿"Ð4§jOÄÍ©Cój[‚ôeFžµÎfáïê¨O¡6l[³±»[6ëlµÑõC.ó‚«NŸa¦û\9ƒó¼ä,Â`+É­¬¼4Y@ ˆÕ¹ÅH/Mƒ,/¦ñ M¥$bò!ËÅôëB5Ú$ÜðÞ{Â'£ ™Ã¯ ß{õ'óøYÿÂï­@õº«‘BeÓp‡) µAAÊ&$J¥¡”JQ„Œˆ"¤ÐÖܘM‡xÞ‰„xƒF fYþ׿¢§:ßʈ,Cä¬,+É%ç»ÌƒLX¸ ­Žz„ß§"½½è#+b?þMo]Òqß}÷E=krënߨ»}Q—c¥¿ÕÉûB çü¹h5}çÊ^[¬µyqœ_cŒxL7Êñºðž‹áÓ- nÝÙi_˜k6s“ߢٹq+ð›PaüÇ2ÂÏÁ2½Ë÷…¹xœÕH¥¤;Løx¥y½œÔ_dÐ)ʨU3J¥û4H¥Òªºêëj«*+²@m˜€2Âá€ÏëÅðt˜á•HC“L$&z-Jq;ÐÂBÜËÅB¤Øså 8føÆ£ž‘7x»I¯ø9Þ8xð`Ì3ò+oZ÷¡ð×7odÂ÷¿*ìøEü>z ˬ˜åëp·wô÷:¢èFÉë…Vô`Ü×gB0š]èYÁˆ>¸¸¿¿_Øêö5 ¹ÏÚ!øÈR«O|,‘Ü‚jµuЏ=ýldPô Ö°*…·3)3Œ?Ñ”}2*6ì,HËh96›ÖkB«ÂŸ(°Þg0"ZfÀ ˆç‰pÅ+W­Öwêu¤þ¼IÉ1$X&‰ç¾©·¢V1DiÈý4_á~…ÿ¿·"9;ä~¾ÍqËt_/EmߺqýòeK.\0ÞÈÌÁÞu}ëº:ò9Hê´[M<žC-Ö y1ÄY ÕDJ¢h¢6ñ$êäI¥ˆ¡Žüäú·¢ª etŒD,M!¼¯?EÞóÙ–kÞñÜB¥I¯÷¢v¯/Ý1³#ØÜèíh0å•n‹Éã‹zc®Lç¬ÎL°k}ïÀžèxrtIFgK(Âûμc5ºÍbšmùqào— ëü>N®Ù$×(eô Á摪àœÇëŽû^w6|,¢¾UÐ !N¥Á6h –wŒÄUUO^¬ßbá/gŒ\éV«ÄJ nÊÈUXãU«‹¯O¡ Fd„Ì V@óW¸Tr¦KŠІðæX´pÎ왃­Í u•Ù€ÏíÄ]…D­±ìýÊʸ¾Ú›ŽL~•³áUž½ÓYa|ýýnçݺ€ ~Åwzlÿ€øîúÑ ‰ïî‚Uê’ÁSƒÙ-³¿ÂK´ßTUIã?6ãw•Ãò²‰j£ú¨aj!µ®°z–›ý´FMwØ2–¥-&^ÉÑi6T,e³êµ¬ZC©G dQCh¶´š®ùófÍìëmÐb#HÒ|M®Z’¦‰p(ˆÅ¢)g9ê9å¨èQÌ‘øÅÄOàZ”çDêV&À„ðÿH®KÔ:QªJ¥wBˆüióî¡\¥{~õœ³æåP%×ub€«\Šêº¹ªÀ3k¾ÿŽGPú·ÎÞÝüï-»ç Bßj‰~÷Û‹Â-ßs\Š.V{µ ³š];¾TëÖpë8µ®´)GTVúºÝXö¢;}.á´Öÿ¶…åïîUc3tü#Zÿ_¼þÞo¸|`ðò«ðæàåõvkÃL8·>•"¬‚Ôn|fÝ‹ÕP-å¢ …Š£‘m=ù±²µJ9x39Š͈™Ë*hÓé(JçÒ¹ŠÅë2TÆVª­ x¼”AäB‹~7=òìÃïú„ëØÞñoáÛ{Ñvý½ 1ôf_ïñQvý±›î}½)Äúz˜oH: »Žä d )«¡¶ü)H‚‹Ä{$ºñÅ:×á0`‘%4Ê‹…ŠÄv|n"û þÕ6¿ñLRÿ+G}¯ðÙW Y6gû»5}Wø‡ÛËȾ¯‰ù­Ë†þœf€ïûtrüu&†^òÚSÜ"ê¨F,pÞ%y:™BÒ㶈ñdƒ÷½‚ŸÐG°¡‘Ð<ïvé­ÜRG*ãnÉóuy9)ç &>Vt꘱·ßöÙ¾)|öꫯúí7 ·0¯ ·¼Í,عyçzKµfTH¢ÑÓï<}ËékmUš9hþ믿¹‹”¢háÅ/¹]PšZˆ®*¨ð±ªî$9™â¡Ú (LN¶ #_+M–V X=Z­– } =GØ!ÌVJ%¯„ÓÔ…ïÑ$Þ[å7ë=ö·½qÚn|Ù;³~÷IPâTÙT*42éf…žiÑú¯x#â°õ"jî®ö¶º|* »V3ÁT¢J-Y #¸€ÙPòJàR ¤V4D'šÄBx“ð2.R•c‰/Fäô?ïýàÒp$rû›Â§W ±Pª·9g;ïï»þj[WýÞ9ל9êr*gß~V×ú%K,Q}Õ¾tßCkç]½Äërn¸ôë#…•ÁµÇÑU–™ÉÈ`gƒÙÌ9k"Á®–žÛµþ»'¨›oN<¹îl:½ð’%è÷…ñC+VÏØÿ½m[_¸~¾7ì5¸#–ë?%Ò‡wÇi_2#–Ó séÑKÆ–î™ölì#rÆ…÷}ŽûU‹uÁîßX•²³é¡QG(HCX†R…^©TŽ‹ä©CjLü`ôhwgmM†1Hàr&_Æ€.qZy†uŒœ!–/A92BÕ?¨·§† ±üÂí-…-s*¬±¼76ØJô®¨_éÌÅ]¹äìBZ~x±2âØG¯lÊÚ=Nï C5ôÍ «2…}WÞ0{ìÊÕÙÅûÇϹ`o¡f˹WÏŸ}Á¦±ä¨¯h~zÆ­súÎ[Ù©E Øœì¬UwöÔÍè T÷UXG±nŒå{˜È¶8•Ågß5¢V’3(iV§ 9-ËòHKÉäZÙ¥ÑHê„^EC#5M°ú¢­æ*TÁïðåû‚;ˆ×Œ|ÉœŒ™t"›ÌÆ£p2ú}X€òÿº÷«:óÆï¹ezÑhzïE£‘¦ifÔ5겊%Y²e÷Þm\i¶é%…Å!´ÓÂ6K $@$@B6ùÉ&ÙÍî&›B²{ô½çÜ;EÍ6»ÿ}¾çOºÒ=çÜSßvÞ÷÷4:wY9¯öaåx A‡Åx è¬ä4åõ»±3¶ççУFÂÕŒÊO õûž:ºDwÇ5p6®=i\vô±;þvâÄ tºÙ~á;ö^ëÊF¢Ü t?BÌßQ&<´oÑå¥XeQ^Ñ{ép$÷iúñ+†/ïÌœ{®¾óJ”«Äà +ï›N³·rÿBÕPMÈ“5—>Y§¥YªÖMÓLð*:†D'\VyxTj˜G¹ .„úöüÕƒB”¿¨ºPI)©¼è6Õݦæ"Û¼˜æøk#©/©õ6yÍØ¹g†Åˆ·qD†!m¼áÛ­áGä¬0Ä4ÄÐÚÝO_Þ¾ôäû7Ëh¯uyÙág[qì_luë¨D%i\´ØÑ¾5?=¤²©Èg{å²KðâÉìùñáOýñ©­Ò]Fÿª‰6 訖݄,ÆßÙt'νß é˜ÑϬûò/bÌÅ¢}wÞ…ófýðR7¦:²­ADÉQ›JFSJ)MüfšÆâ õ‘Ë  •d÷:ì6«ÅlÒ€„‡…25±x… b·p=œù-åÖº‘;èÍ$ƒ$gª˜9…*ÿùÖ-—½á©’,ÏmG]o9ö|ä·ÈõRäÞG5þßÅn4KM ¹QÊtœ:~*÷Y—¥soühüŠþýì¿?µÜ9 ;IìöŸX×BõawØ©t_owW'î+î)¢Ô,Ê Ž(b)„ üãŒ|XÄ4Ŧ†šD´Êï ˆÊ+µœà³-Üb<Nì/¦™‘„( f rÆ D×­kkiuVevÑïxäÒFŸóF÷/,µvTïÎ…= «;jã=ûkͧtqhóþcmûž9ÒÚ–>éFZG“=wóËí=±ºA›ÛV®½¼­·~ÍÑîŽM!Ï¡t™UÙx&oòûNµ¿Ê_®¾"ØZeN¯»qéèþªÀ¡v"')!Kš ¹ŽЖmÉhËH#ÍñΨ¨7Ÿ#˜™9Ûß« èƒdßsóì{¢11‚SYÑ>Ê` ìxôçmìo½ã‡WÉhŸeyÙä§j°!®Ìá‹y} ɨ‘·Õu½u‹œöØ–?íªŠ ´o#þ'Þ?vë‡Oobwý§Ë—Ö‘—k­ùÙ+‘Üè5c3ÝÚ-ìU†³néÍ;ôÌ¥’Ž}_$6fú/"ÈkJP¨eÈŸU 3€ÄT±²<m=°@¹XN‰A×ÉDRÙf ~Éy¬ƒa ï$úŒáø´•2;"Ayë?¾£®]°¨B‰¤Œ¨´©™õ1gJÏ­¯ \&æ¶SZ{¾®/X‘Ü8 ¨&z —Ô.î[ÔXŸIE¼/°­Kir»yæžÔ ðÈ H+äb¾sHþÜÓ¿¿Ýì»§)7ç6~i_»ÖÒÚÎŒ˜[ªv¨46•Ó6÷Ç-]÷wmjóôõdÍb³²ÜWw…ñže=q_ÇúÖž£û¿“”K¥åú”©|Æ2 ýæG?;8o¨òìS6¥Z™6Yô–꾦äx[@eöèWÈÕåzÈÖX=ÜnTzQý Cƒ×ÛHâÿl’»˜ç)ŽJäE$!¿E>±¨5[&ä -üiü>a(o¹ý?1ƳÌógÿå^gÁØ’±u”™ä É_N Ú¢tZ•B,¢ÌÈÌ 9ìò)Küc5M¯gßpåCí—=¹sç?\ÖþPdxóåÊÁÝç®§N>x¸§çðƒ“ø¹s÷`%¡íƒÔ™ÇØVxòWfC‚í»h!G•²Çˆ Šc¹’ŒÂyøwYqöü¿ô¿~Ö-§üA⦹+áDÉð¨$8j‰¡,!Õ‹ã8'Õ‚ì(¥5n¿F ú(FWÒW|̰Ù\s®½Ì<‹^ÏÕž]ôUú.Ô’{éÜF˜¯A”e¥ƒS‘òŸÏyƒ¿Jà{Hü šêò9o›ûf<+#6bN£Ái³ô8nyôÜ¢‡èß¼ð;ô%< èÉÜ»OäJàͺp¦mOÛ\’t^ à|ß5Í,ÙÀßdk¼èÉ@ûª=Ǻb›ÃeŒÂZå::J0FßÏí¥àNžmÍš$¼¯.ãÓŒëƒ>S¸exGÜ$ýÀ’[¾º%¶‰|%â<²¤òÏo<û»kjí¥Ž|â ûk²×¦ÿŠþÄJéçÉ÷ÌÀºÊXíÇÿAŸ¸ŸÔAOäìôѨãÈZi‚c=_Gz-ÞOŒ«ØÝpɃ¢-{FÒ_¨ËÊà{Ázš-f.´ÁÊr°'î?[ÉÀ}Æß'mà|¼ ¢l# £qDZ*OÑB>^h&£§ÈuÓ'>¾ù‘г úÏáuÄŸ§·G¾wª«´ dsÓ#èÙs0’³•÷ó}@"s ô!Ÿ´z@È(Lr“„Â2ôÜç¤O0?úøþŒOån=²Ô uàÙd%CB„½RŒU­h—Èð™Á7z⊑~4íyßB˜wjÞâ‚[¡Q½‹º:ZšqÀ|ÀiÔkd çeP®®ô“Ë\@…‰ÊZê„Ág:ãµ[A:!Ho„NóOìó=ûüHRsÕ»÷{w9kê —¡N­^N›z®Þîß}`‡{é‘¡€µqÑxí%'BŽ%+ÖǶݵ!†$ŽÔ¢Èš]H¢­ìúêÞ+™«?®[sûÆÃ0"ݨԙÉþ üªôµ&%§í»VgÆö«¾zYtöÙ¾KúwÜŽŸºv-“ù*Æü…«Ét,«R#†vb<'ÿT ³ªƒCé->ÖyÞå³õŽ?[n Ë1«@›Ò‘MH‹£Àäþ~oîãç6ø–\¿ÞÕÑJY¨¶Hƒë=\L–Œ½rÿØ]{ÚXnŸÎ¤d+ÚF+G.Ép^z'‰y¾uÀ~¨¦îÎÊh$FßßÌL‚Ä<Ä*ð^ÞÅ–wc$×Ìl>›'_JÀk–£Kjå]rƒ…±7ò öÖ™·$¾à.7¹Cw¨œ\5æá\yšƒïXKîUyò#à¼^‚Ä·ÿý™uÉ-÷ïNO…kcL‡Î côÉÑ–ª©Xº«¢Ì¾üër/â¿Þ¹ü¦ÕI™§Z¡3*ªú3N©º¢36uoˆsÞsŸ#”eúOÌ/A^ïäý½0vƒ±ƒ ±XlÒµè¸ z£AU_©"­s€Š™KæXX>PU½î®íëïÜX“Ù²sËHb犮u½i“díà¦K¯j½ä+‡[’£ÛvoMŽúê‚z‰T¢Õ1ÎÈ–Žæý“µ‰‰ËzÇ®˜ÌÖTgº“#ƒD¾<”­67l;11°k¸!­ëO5u–»*Œj“N#…-´Ã€ãž¹VÊCùq€¨‹¯PŸÉ“Zvæñå ˆ‚Ï‹m¬Þ7 ÖV&±÷Ô|™¥i5B~7c@#m’x7ïÏC`ŒÉݱº  ðh見*k¢òVÉ‹m×uÑíâbèÿù‹ñj{ 8è;ìÃþE56O,3¥y¢9ååZ>›µHðŸá¬ã÷ÿê¦Ûÿõ¾Q>‘sxù˜/Ñ(Cp>€p¶îp/]ºØ²øÀÒ´2óÒí7~pï²ÜŸ¢G‡¿ôLtâªáþýaçó7,ž<õŸ·?qÏ5k2 ˈËF•Þ¬H/©³Õ´·í^´xѱg¶uíìÝp¼ïÒáÊ[áü¸€F~4ÆD°}@K‡³¿ b,¾SÈŸþ|¹‰2zÜù0òïäÖÌ8òèZ T;rŸËý}cK†éכ䌩nU7»ü'?yûìAæzO\m°*2Kj,„FïÊ]-¢™¨^êѬ{d¢øîBÀ’óáìQŽ‘s¹QâW^Ä(ð½üÚ24³í¼hâH2·…KpÞQX`SiXc ØKõz´Šƒ¾Kµ^|YÁßÂfò°š^iÄËo˜É+ù[ž2Ï $–ráƒ?~ÔšmoµuŒFËQÞ(gœK>½Ë\ßXgš8Üë´ÖµöFVršz–­Š:û¢¯¥ÚÊ|pî†Õ£Õaú³·lùÂæìQ'Ò8LJQ[Rï‚ßi$óuÔÕ[£níÄâDg•îÕgèçnxÆXÝ¡äci™—˜?5ÙÃóU0HoAZñ¸’?^€sÙ¢±= k/¼#ž’…Px7÷Ym(à&½š¼óMzßäýÉ’ÌK¹á£©ñO­K=òwÒˆT«Òw—ÙÓ}ÑŠ•¹Üô¡NÝúèåï~ãÖê-J.ؾ<¾hC‹Mª ¯åí²é?0ÿÉü™j£– ÁíHíGR¦«š–HË-!~ ð¶ä/ãBp«I  9"ŽEW*eFp´qûæFdbÞ@ YYÁÄåéÖ'ú> Us+’„kQs¿’Ï(/ž[^ÆÃèáZ©CH𱬷§}Iǘ¹6¸"Tá÷( •IB»Xá¢zVøöªbPIÌe5K‚õ,ÆDÈtŸ¹ÊW ‡£¶@ö×pÓÒ¡ÃK*ÂK.[z{ ¨Ãb©Æi=¡Iú*"‰5Ÿ]í¬ y=:µÓªc>µ,àktWÛ {õ-÷j̳Æß±¶É×†»'‰•=‘h-¢ËLe’ô¾ÚއV¯þòá.ÙnÖÈŒ>+}¿/ì¯PìJ¥Õ¨Â±öÓd– ˆ 7A³n"?ð›À>[Œ˜pž÷8"àL4p~AcŽœ_a)ã+GhøÈ’þƒ#‘Šö¥KÛ+úšF[b¦°®Á>ºióŠÏmL‡;—O.ï 7VG$ ½ÅÕŽ-ŠUöollžlù]¾ˆ³ªÞä0ùwYªÜå•Ã{ºëFš"~O8ˆF«¼*½D§QôôÇÀÞa~OÙ(§í9gÉÆY²…ÃŽÓW€láÙÂKr¤Î-Db,Xph£mÈ›Ž5ÛëÓz}ª¾ÑÖOû†ìhÓ¹ËPüNú‡©ø zƒÔXá‰GjÐßOŸ«z ògÇÖi¸/QIðNŽ#1UŒÄ©©¼È«âSœHH4Ÿ~¶ÔÛq±ÅI*o/Vÿ`øÜ$~+apR\·´œKªQ-M5©ÎªËa1õr)•DIYI`vA0Ì‚4 -Íû«3ÐkÇî9ÐÙ}ìÌÎÃg6jÂ][>³*²ªÆ6ËQk¹VFk“cÕ+ Saiò¾Úúeõv;ü§qy½}/óõž+߸ñÌ ƒ#'ÿù³{ÞzúÖMµbV¡U+Ê ÒꮘYT±úž½}gÿÞÕÚºeQ0ܳªæÁû(‚Áù"áö‡Ÿâ­—®¼0@nöXâ·-ÀÊä!Ú Ef¾…ÝTÀfSÍx§7oª(4DsÓÓ¹Êêjv§ 5ÖŒµ2?Cwå6~íã{Ù5Þ¤ÚhSÖ÷Wiø=ñÀ´M<Ì}›²b=žú–“¤‹h,P Âý3á®Ð:F a4ê?‰DHžV^VÃvGS¬ŒŽÐ7aEØšv|ñ½ýûÿéÅ{v55íºçÅÚ¿ÿ½OîjZ•^wÓ©W¶lyùá›Ö¥áùá—·lyå<Ó{øBÅ÷òß;°ååGn\ŸN¯¿ñ‘—KëÁÜ·OÿI<Ä5Sý·Ì…}725´H\†ˆi“!–s¬x3Å‹6JI´+Cr2ÌH„ÞÛÓÙîÕˆÓ«Gn*f)¥K fÐ\WQÑ‹[335®ÅfÒwl?vfoªíø+WíûÚâÅ_ÛwÕËÇÛR{ÏÝþùt½Ì¤Q;*Òþ}Ç\[{;vÇ4¶«³wk§ËÓ2YHU8ÕƒŠýVMÇ–Ó¾ñÞiê©Õ‹{{¯~ Q÷ÞøçÓ[:jŽZ£öýßí|x_C<k¾!kØ÷ðÎKÛ“ oÙ9ý'Qô¼ õW^‘Y„NŽC1…_9‰ÄôÀë貜DÌa—ZNSòÍ0q2|°éqPfùèâä'ä¼&„7œ>_MR§èA_Z;[ÿ *’à)ÁáLOȉÃ`ßQŸÇi7X’R!Ãf@â†÷X ç¡-õ[ƒ8î`înƒzs÷n=òí[—Ë¿‹±J¿§Z}Çëû¯þnû˜Ì`Ðj—´5nNËÑosFYÝ趆`67K ¨ ;(ÝóÓã羚ø®z玑¼›rtâØÈKã7¬ŒË ~‹D¼ã,ÈÏuÄGÙ‚ÑHŒϢ™)çÓˆ NxFë"!éÄ=S>X5‰Ü‚eŸ«{Ç‘úûâ/K«\?¢]çrGeÃ*›JiS¹MèÍsÑ¢\Êà‡®}êçîÉtzʃiÂ~ Ôè‡tÞD6êaÁÙapl'b¨ >å›TÌ1¼‚Ål„θ‹Á<Ö+ôdvßò=£_Í=G(ÜŠ¨s´óO¥ìáá¿<,ÁÝÔ£²!¾´èÜG¸oôžÞö˜ÐùþÒ/þÜ=½uó4Ì óö,±í4`ž[c,c( jƒ“ŽJ()$Á I„Epè4v'Ç‚ØêBrÉðW£ÍL5 wB&¹Æ`*2ø[3Ó‚ÜôÚŠº »r³$âúñ†ÚÞ©†Üª·EÌÄoçV5¬è­3Þù#W•d“Ê^Q[ÁÉÿK¤´Vû¬R©ÕWm½úÜév•·1š;að£¯5oZœRÐ-ç^VÔ njɵû hG´É«Dç^¤ÛÐ:_}*åp¦ÒuÞw`œÀ>Ä7Ã8T οZ«zŒóƒ&bñÊŒS =Ì/\",OKs"VÄY ƒÁ8ŒZ«$ô,“_%î|ƒ z ³‚ ã¾IÕZ•ÛBÀz;r›ß±»sk`ÀµÆÏ¿ãŽH6«l0`Vþw1ÁL…Cµ&è‡t£µ*?úãç~‚^¨ì0‘Œ)oÐ5 Œ[ém"óÃ<äÅàà'€Ð.oî6æ9 ï“hVA"I†­DÊž×Ö–†YÒ|˜%¹OÓ‘€)éÎ6 lW ¹œGÕW©4+1Éòð$kf}( Å”rj– µq„aöbjS%•fN;ÕÐNóE¶#¡d2¡ª¤•lãœiNJËæohVu¢»è'LJ÷ww¶e ºÛ—ð¨`ëð V‰ÂY`râ…™¢ÙF`yá­Ñ¿¦z·/êýé›'?WwFž–åégу»öïÜÚ4Ñ×ùµm}ß¿rË#³‹‡Â] [j÷cûí¯]Ù®t¦‚õû#mËSzFfÐvån«ö0,süÆ+nT[bí¡xWµÐÆ/k#hã“wÄôõ}“é¾Oïjg96SfÒH¤ÎL•¨Sð8úÜ‹’Ÿò98ôØ¿Òúk|ÀnâÌ|.Ž b¸jŒÕVœ0Íòa˜í<èPÄ…B~1 ¤©êéû¹›¹OƒvÐ@³¾Lœf¨J?x<”YbW¸º:ÈKV4Õd¼x)¤šñã»{þ ñˆ â SÝwÓ7™G’5£æý/_ß×wýKûÍKkjFM¿ySRº7·Yz"•=–ãŸþÔ1Ë¢HÕ"KÛ¦n?}çÕÿüä¾òP,^¡Ûû䯎ÿÕW.ÕVÄc¡ò½ÿðë«Wܱg‘ÚáõØ]¿~ö̯]שîÛs;¹"r—è—T/5Eí¥®Ê^¾•)±üU΋^T9lÄrnólŒ’S*\5IhM+ËÊI`e”¦lrAᬯo÷®-›Ö­é›ê›šX¾ttdÉà@OW{«§\Øtÿ -9W\C¼–Ô %±^.*ì{¼AxüDBÝêË+^­¸lÍl™.g?vIÛ–ÍûOݰmYE(ýH©Þ|_Ëî{¦rë™r;§>¿½±·a¯\¥7é·¬zë.N¥VF|K(öV‚khÁžù¼.s(U‚€¶ð÷/££P0yW!öÖoº‚²Ë;~“½LZáüúÁ¿ýâ”ÊûPé0"sî)´8÷[zf:`ÅGí„ç]NQ"Œÿ…õ‰T6Áë‚2±K«êvÐ.4º°JÁϦ[;W¥(¸1múðÃQÇ/ÿ }ûUè䡿ß4$üé÷*+î"tî)Ü5ô/gNC±»èïûí£‰ÄR;î+ðwöߘ÷©0hÇÑl$æ[T:s"Q W˜9ÛXDë¾µÄè(ÂÙ16&ûØÍT+ˆ(ÍŒ.ª¯Ö_ñº#$P˜œ•ùo—IÑ/ ;¾u¹®º®»:÷•—E +~å\¥µÊc’ˆÍžJk6·“®WV·Ž$r¯ytÿé­²ÊQn:‰äÖ*ïê<¨.>ÒZ­D_ÎM;“±„Í–ˆ%c¼ž´Öá>X‡Jª–ÊdkâJA–û­ñÚƒº¨ÜÕf*+°æ€WB+Ïkd1ŠzÜ5†EÒ›{Væ[¹çØ»ró£~ÍQ!Y¬0»«\ŒÚcQ J*¯¢¬/gƹÕ¹âÚÜ:X²ÍèŽÜDqоj‹Œ´ÌRíƒAÓ¿p&ãq»=«áÇŠ(Gî cgOíF½YÍnx7®œäX9¥DRYÞe¡ ÖO®@ÉJ¹b£ ))…L©˜ÄyÇÄ2é$¹NóQØ‹x%þ†OÖˆ~ÞFó4‚ñ·]dKøz¢•R)U[çk„ºÈ60qsîܾfÕòe#Ã}½=mÙ–¦T2«Žøìf£AçR—a·C‘:Ï£ x¼©·Âç¥z>Ì~–ëŽÈ› ÍÛ)H®­ $’Œ—eé«-NÅŠÝûËüM‘åË«o\è1Ø]51]º¾ÎÔµ²ÎÈÈúº¶µz²ªcÿò„+»ºÅÞd Ô‡tãví_92˜èŒ{Ë4fU]ôÊe¯ÖêS·©Õ"µ¤QÛ°„tìV³j£ëºaŒ‚£ñénÛ½$*2X¼6†a\õCQ…E§Œú¤žŽu­‰¥->æl[ ÒOU‹ü¿”º1Yi™ÌR1¯7hr/ŠOñ#ŠòR71ÿŠE¨] ¿Þž•©’,ËåoElÅ -Ü  —ãúö‚@åZ¸”©p[èPœ±¬˜\zÍ)„5ºªˆÙ”ŒG2U¯ÛTi®Œx¼R;¯—ÔMŸd°ƒ —d0¿HEA/©ðòzÉ–ßpä‚Ú‰a>åÕÕ¬¾aÔƒþ¼éÑ£#êhmmT=rôÑM[ž86¢ 'aÕȱ'6í;áªJ¥ªœ·íÛ›3‹†·adä˜÷ÕO­¦£hV½~¦NÂÃhõcÕ¤¬|õ‚š‰ `ýD©¡ ]{Q71ÎÔM„óòJælf-J{sÈóWÏšêë.Ù½eó¦3I´Âq¹K°oÇ‚LÙ²+lcJ©5¿ƒ0ÑVJeÊÍ”R"UJ6h·ŠžC¼h^ê-ì¨îZ"­ æ[!í*8V1¾3°f{hI5‡ŸÈ¡)ùÂMᛚúº#‡÷îÁS8—èçß5ì+üþ#ûaIvp娣À§]!a% v>!C 8@°ç,åäÄȒᡦ¡ôž—ÆÁÏ8ûË’ïÿšŸgã¾@O›‘’[‡¤Ê<#á@~ïÞ™Ú(%'å”ÒͳYŽÍá9 4‹éðLÀ «¼@#¤…Á| ¸I…HK3‡}ñ-gõü¶žÁÇTàcð³š}©dNÍÊ ²¢:$góG´(®\Äb8Ž«ß˜gJž¥¶BvkÉËññ3¡@8äát3Mò<œ ý .¥Æwôº§?Ò}ãö¶Ž#mÙvê@K_Šh{ª~™»òYKjqMt¨ÁíªŽWdœÌ€ZS³ù [Ö>vU_ϱ3;׿µ }ÕìÉIsuµS­¾@Ûdª~*ëñ·Nâj‚¹ø>w-儹h§î8c’ÐL!,Õ…NIðàQ‘ìK|&&>t4rÁr$lÔób²ˆ›,QÇŒÄcn¢êbíñöŠ +êŽjÔ”9q;avf&}åo,ƒüU¤àû53fÄAÓâ§µm?¸bªiç““wîlšZypGÛ¡S½1ÓŠMWšb½‰xkP­­h©ªj©ÐªƒYuý¶Ï¯8øX8|rÇÐêë7Ü8´ãd8üØÁŸßVÿª»~0ºcï¾íÑÁz7}•¯¾?äïªq¹jºü¡þz…¦ÿ{œ*Ͻ BMyVO·iF>†‡nJ°ç™F&–È0 }îEþ'©‘œþw?w5±£7PƒÔ8º¤ Þã›}Þœ1EΉ”ßõ8e©Ñ³ù°»‹*nbó¶©Oк㓵îãÈ.N"è…œC|•!2R RÐÅæ«JÍ[3+TBÈ=»ò¬*BnåøvìÄÿød¦9÷âš™hzÏ0|è‡kÆö4wìFG÷vtì~³Ü1[Â.ƶ˜#®ræE(ÒÒ±ÙÓÑIЏ#fsØY^î ›ÍwùÙ—Š(>¬çBO\µxøØT<>ul8ÝÑj#=éš®pyy¸ëÜøâ«&ãñÉb_ %x“ñ"u:÷"ú óG Ãvœ¡xF`*Æ ØIÎ@‹©Le¤:®¬R‹sæ%1ª*Á*Ä“ñÓrõcêòÜïµ.Wm…ÉÄÿ4æ^¤ÑòGW®|tù¹iöW¦Âh¬È¸\™ÉÊð÷-Ë™õô6Q¹ï€gÄ ;z6SK³² ÐC¬ižëpÊ|•VL–‚Í£æâŸqÈÎ;€cÝ9ž êvêuª +8‹›n3Eo¾ ÏðÍ»È7·fU)²bX>;=>ðE Ð0Œ )\×) I~Œ˜èÛŠòä°øZ¸gÁèç[yª£ëÈ= ôÃÄl@9Q‚ô?3èóÐÊ“uÊYêD¸Ja¨a’Äç‹¶hXÞ®†óq>îöQ€G/zá› /º›¾žz·6ý_ÌÕÔ¬"â÷1äD´<žê5‡’Óù„Ç´c£IáLo2$ ~øIº­ó”.-ò M¬Ñë\»7/¯¡¸v¾N¾C¤wÒ?áûH‰¡WeËš3iF„â À`-óxa> âÓˆOß>†æ¦Ä|7±wu¡›>ø£m[¸´¨â yG{[k¶%USYátXMò¤"éõÊmü|f¡¯[¸+øœ…¤¯Ç„ù|“ôõ®é_1Ç©/dË—!…2ŒäŠ ’ÉaV¥¬ÐÛ¥ ä2…œxéÐ2édÉt)•hD%¡çNq ÿ•R*rò<Õg̶ÅëZÜÓÝÕÙÒ”Œ{&¼ùiW{ðÙÞ™{’Y gsl Ç`Ó87&m&H&ôÆ<|¹¦Fù ¦¡€7b÷¯\ÁøÏ¶ ¤Ñ b‹(|ÆÙ§ŸYë\±­yÙÍë3ál{\dz¼\héo¯2EšýéÞ”¯Ì§É­M^¶× ‘ÑCý‹6öÄ#çÍ'íñŠd{Ò èLv“‹yÊY‹)E­ÓU2Cµgx¤¤¯„G®£PGÑm<Ÿ,ð^,/° %Ù2r„‰?È ¥¼ò¢«ùå'üŠã“ÅG²Üœ¯ ¥P(…zEv"µJ ™Ê"ïœUZ v¶¦Pqÿ,i`N5‡ê^yÙá}{¶n^½2”祪ÿ9/õ_<¯ýRóZ‘bžû ÏkSàµÏ“"{G¢Ñ‘½¤°ãJ³¹Á?¡È%„cïÁEö9¶™oÅŒ[ùÈôÿ%;®¹Hv ²e.÷"E}‡—- °0°hz,©ñÂæáNþÝU=M¿Å}žœŒDÿ§kX’¿}®!V 4k56¢ˆY¬nÏ XÁpÄAEQ©®&•Äøà¢je#Å ‚v¿`E|™Õ†B=]mÙPK¨¹Ò­ÇHn .i¥”&3K=2꽚¢RàD8\©y…¦J©™<ÒÝsd²~ötÃÏ%ÁŽ•µµ«:ƒÁÎUµµ+;‚?Ùx]uK Ìï•—‹êW™daV.×yvÇÍöuž¨©™8ÜÙqh"•š8Ô‘^ÝUQѵ:YÓSQѳfËÕèh¹+l Ô—qeòËõk¤\¿K¯·^ÒD7ÐiFMx;<À{©óbÇih›À+OÐ=ÌfúcRž¡Nßëe|B™ÇI™ö9e .3^l8%i·‡:¡¬ä´O_ÎЕ|{Ì–b{P¶](Û>³,tv½¾ÏLÀþÍ£40äƒ ?{cŒ8’]ÁPWÃQÓ v%ô=¡Î¤Ã‘ì ;“v{²÷÷2êwÌlé<Ã\-ޝ˜÷þÂ\ýwêL ÷X³ŒÔg¨3•X3*ÆÀÇ4»M„cΨA!Íñ¤ñP¹ÝM#³Q[^¦’KAm¶Ñ6",‚¼ú w T0SgXì~–¿C2ä…wUQx'ã:›r²ôÏ ñP”A§aà¾rë“mR;G¤×ý˜+÷ÇÙ<›¡5¹{Ïý„ýýÖ ßZ7ýGf ú褒ÔÍY¹E ÇJR„çdŒÑ8Bˆ‚¨xáƒ+Â(EL ¶B†Â~“¥¯””!*VðÁåØŒ Ê›0ŒnÒˆ…yà xdf½¥sëž Ï㶦mƒÕS}ᮆ¤i–p=êè’=ÍkVmézæ/UƒÃѶÅj³Ks;lØs³föÅåÓ:Ú.ªÇòåå˜Q½pžt²üyZïÿ~ ~?<ëý‰i]!róô¸ .Á¬"®É&v6¢1°Ú ÍtbY©ÜÞrÎ.ìÍÝÐmÀ7vãF ‚žJ¤DlbÈ®ÆpөѺ½DÝÀu—Aݾî2\wê&³± ¸‰ƒH~E`Aˆš$8¨ƒuã v75¤“©JVDl²ÔQhk«0–£d,K¡5V] ï[–¡'gAÒ*Qí0Ë@T7ãÓ5ÑH*,²‘曡Ësûó9ÌÑ“6Š òR¹œ*Z­ø7Ám 6ËÓ^/MêSÏOß"ºœ{’ôçyÒÂjj?õ©l¹Ié Äà”-Lq¬àò’K@L’Ñ'ƒÑ’ „ò  ”dË¥))CK'ÏW{€:–ŽÒhåÔèú¥ëÁ„Å£A¿Ýª+—`§ßÉkÞ BÙw-•¡F³Ò¸]G‚s…IàO >"âh°4GÃgg¾ ÄÚJ[ÂÚpX¬«tkòùò)ßÜY=5+AŸÅŒ‘fw W'V^·téu«ÕÃ;²9C8.™¸qelÅ#¸õÖ?œZ_yã8Ú“ £ÿÅG-YvÍD4:qͲ%‡FⲜ6¸å2Óªã÷Mì}óÎññ;ßÜ;qßñU¦Ën Ä`MF€žü‘_Slf/-÷²ÓèÿÝÖË9+Éø#ó;˜á °ûÍudzôÿ™õ4÷¸iÔXïHt„Cî˜'V^†\´KÊó 8ìu0‡)ªŽ:ÂÏFH¥ Ù2%Œê¸áýª–Ó'–Ihœ˜Îë¦!x+oQaÞ¢ãY{p–"øP*‹V…+>PH1ýл5  …2£õ Ðõžc­—ä}ri“ ƒ˜Iòèýð¬F~(Á¬ï¹´Ü¡—²éÓÒÖÐ%=›^©ò™Øw´’xfu¤1´oÑ_]ŠŠë^ÒJé±ØÑ GªƒxÿßéócQ-^ßFL¶j)jOV×ïuÁðûªT@±+)¬†€9êp½ÚLh»'X! Ê– 'Î,m‚†µpÄvgåõ¡€Ón19+Oïǧ«˜ØoྌøÞ¿RTm6U£Ó€ì€Sà±l ª<Ÿ÷Œ|ŸÇÞb;**‚^ ¿wû•ÄÂ'p{ÛAˆÉdk0¢ÜOc‰BÒúÉb8`4Î÷E¯¬5´¯q,à“«¿~V#N¦G‚˜ÑÔÝŸÜŒÆ÷ˆFœÈ, ï `Fóá]á»,÷WÌhî{ØŒ,Z;ôê¶`à™`ݤÿ)øåÕêÚág_ås›­›0Op>Ð’ê©vìaÙb·ÀÖiΕ,E×2Xg0d>–ê7ó—8QÈ ¼‹:’ Ël °e•\5Ê{mµùL!ÉiBøP>5ò¢š@PÜLRHeT ºÍ i•ÏRæ‹f#¦ú½“uÕ+?½¦üSÚáë¢õr³NíÆÜ¦º½Sõ™õ·,Õ|µv¤õƒÙÜC 1} nW(•ËôÍ&Aeꊞ#{V4‹ßü²¥z•F_f0Šs, wnZÜudU³øÛ´¬:¡ùƒ¬ƒ¶UÄ5ÿ*2º+øXæÕÓ Ìsì!ª‚ŠQë³êjXoDUy,XR£`B])¬ðјÉwB¶*/ìÔIçÜ"ì¶’ãg4†Š •Ó3ª$g¥Sá¡'a¦!©ô»C7l¨kØóÈ.Ímšþ±F•]_fqø-ñHc¥Wë74x—MT©oÛ¿¸'ºi¥DÙè «¾rHòú÷T­U#åeÊrøÜÖdÒÚzBŸ–y޾cç-vºQw;ña­™y èj”jÎ6(Z‡½´X„dÊR¤(ž¢óE¯ñ{µ^¿§rÖ³—bØQ>×0†XF“jÆÙsk+ɪµwl;þd¼Oi×ZÝþLléJ´;ä̽ÛW¹´ ½pvo]Çu´Ì“˜ºc_×{ë¥eþ*ÿæ­+WšCFåŠ×î”t_Gñü’yμç6¼œË ÿó¦¼Öû™²s¿GŸÍíBò܇¹ûQ-Söæ_¸ýºøª0_ïÀ|¥©¡ì@Éäáz”L.‘ËHúR¹L*ÇžÃs¹`1±{èFUeÀ|O¯ÿ–)Kò2‹fLÕ¬”̳&—asO %!WîC¶»rYúúÙ]õ×_}ÈÙZµöóÛ®ÎÏo _¶-¡cË¢¦ŠÂô%vã_-x†/ßÛ Ì° Ó‡æódNÍ”›·QϤ© xª”bÆåHJa¿²IýZ\N‹ÛêöÁôzG2ŸGE|¾3¿ƒa_ ଡµhàÝwT;vëô݉?¯EKQ5çHö%'&â=I§8÷CFŠZî?yý]_Ì=G½>”r*r›TÎTð_¡ïÿ·´ž döb¥‚ó¼à—­oØ€•Šgïa b-¡õ™jw‡ªÅoèàOá+ˆRñÊ á>­™­hým@ëAfëÅ2Û—~KaŠŸuXáh·Ô†T@ñëplÚù(~MÒë½€ô‚z ™Ÿš mÖã6¿ü…igTtªöZÑÿí´ñ:DžfÁ·kñ·†‚@½TpÊ*}Ÿœzy¥¼-ÅA»…ýírX7ÙõŠjÉ6*‰Zq!e_®—fN3Âîóv1´ûß®÷÷Ô¿SŸ„Riòýë‚v>àÇÝ…Ûy„ B@…Êåÿ*”I'ãÕ‘ ?O…4JìÛNòsLóý­Äßy”¡.’جn—Õcóä)€¦pXÃŽá6Cxn·€Þ0ž]&i8A¤áÿ¦(ÛÑŽs§k¢U¯ÎKBÊxÿ_¨ßO­d^cþDɱw ¶!l¡Äª7LÖhì©'§xµH¬ÒÇxqæX ½åN½•{-eþ”;ñBîÚþڎDz™û™Jb§ƒgÊ˾H¡é MÃOê!Á–)ddš}¢á]'°Üqu–ù 3Eðr×g×Àú3:œ[ ýt#VóÐ"6ŠQˆ-14A>ÆXËØY\„m›°]81vš,9í6DÕ$HÂÔŒ=c2¨•2 eEVLLÎE`Ò /¸iø;ýmGm•Käð_Ú[=5tÔU»D>ÿÁ¶Øš©eÁó¼c8¥V'º/Zau[UZƒäÎtžrϨ´ú­¶¹­üZí¥–3÷1¿½o"O4D ô¯r$%¦Ž IÞ=~æTÑq¢m —Ï*µå~/Ã)Q2I1¨ë8IìÅy 3î½è{kŸ^ûxîÏ=hi®ëé­¯"OîsLå׿þÂ-Ï¿õÖó·þí§Ž0Ì{°[DOËiªÔ¦žS!±ÁÈé÷Óïæž2Æ­µ•h1úÛ&tÙûÉ5b•þñ÷)•`=PWMÙ²f¹LŒS‹±¯ÙØ ª±á+¥’69è#iWWiŒÐâsô»ðïš hêýR'/»õƒEÏ“ö¿ íß íË@ûöeR‰˜äƒÍRÅ|]+iߟ΂~ø ÞãßD‹CU1î)Òò»ˆýàV 4'ÞÏ=¸áܽϓqÆ}7Œ[ãÅ:T™Iùq®XDZ q›-nÀmyÿqµX#ìýÜÕ› _ýÐ¯×Ø*2îD6Z<8@ÛzÆK¨1hç*¥¢p¥3O#–JNdîQæÈÙkتÜw¿–{U| ¥Î~Šïïôæ>úÚóäÈÂ9Ðö0ѳߧ¯}AÀ(£Þ`¾u8ŒZ-åC¶i­š© ÇAÏ—Áª"çÆv`¢ïž{˜¾6וƒ¶—Rû˜¯3o’}#å`þ¤´XŠ@LÉÀ¦}"÷Š]—ûAîסXîýk Ù™žÜû¹\ªrß¿ÅëšÜûÈÁëlýÔ gÓxl@£ýYO1ß>ùc:¦ƒÄóç3¾Í¾WÓÌú½O& ƒi—3ƒ¯–2NW:hœý;̺ߙÂ7I)§3éÒçîY J¦ò5ð~Ôâ‹DúM’‡þ šÿwä2œ¾’ŒUWU†‚> —i±\æQŒË2sÅ2q23¯X†^sŽœ¶UÇUYN«Q$]¢žPqÞ€ÚÀ…lÃÎ6ç ÕÍé§ðŸ4ê ‹A,ÍýºUÓþüó•ö½—»íÏ;BuS½Ûå6dzϺ,—÷Y-Sî`æÊ)"7QS´—^Bä&ìõwßô+Ô{T$[Aò:qÅCB²F ã,¼ø MøX”‹¼,Ð ´ÝExø¡“IúÇ{CU•þþ±^Ý1¯ÉâõZL^Ôö-Ò–÷-ï T*Êñ[MïXo¨Úìó™->O'ŒÓ÷Ò&:j ÈŒíÙlCÀicÔ^TŸì3Ø6²þìëŠCÎW˜·ÐzmA¯ÃTÓû ŸF"3o6Ø@Pœ·ò5ˆ·ðhy{Ï·kVº2J³Lg²•kb ‰º9ࢠZ›¨S~(m€nã«Å,ý#ª kAX¢®°Ÿ_ "|w„ç»:¿_ƒµ Þ2Ÿtwb½iàAÚÜ©tò:Û\Ù•ÍcÛ]Šr i×û‚¯¼b¶ÖÚß‘kmš\¼¡ Ý´ªzY6Øß‰UåªêЪ½Ét…DÒ§´êU;Ä-ˆOô}ð 7¾U’^”¦…u,^¦«îdȤ“+~EÜeü¯7EÌ'#—~çsѲn‘RÄ)D"—ñöo_zÍ5×WùFy¥ÙRYþ†ÆœÛ1Þºu8Jpÿ oзOlWÊðv¥’^8:Ðk¹_{íèÑ—;¾C3w^sÃÔà‘#%k—š^»*7-•Ì»v ~íH(:Ž#Äà¤<Ô¶´§ŸÇ"a?†VÄ«©<ßjλ¢x¢ «Ú-/YÕYëÚÖ†þ1AV¶±ZXÙÄPlˆ)]ÛÔPŒ§EX&sh¤ì¹"Aƒ/dðÝŒ˜h‘ùë¢EjËíV³É /7jyK¾›I%‹–$ ,oLà ‡T_Îhco\y¥¤±I~üã±ØG‘¤äškʼn*úè]ë‡7lù'tjÄy0ìâe>íôGôÌ{#ÖBAC‘ÿ¶ª”u46`Q"¹AÕ€Z¼M)™~Ð=0€^üsV¶!|‹ë íPÔô®®ëÞmÏ‘P”ëí¦ Ë¯Ì†¯Ãj¸Î0nsé5*µl ,VßæÍ®íª”¶Õ×£Ô¯îð×G»a3¨•[j›ÅèWÙv«5jÙ"{¥]ì\S?h´ðºøÀô!”eÒ˜O`h¦§¿’ƒèé2ìåïeÓ‡¨ÿ^†ß?4ý*y¯’ðïÌzz£(N䈎—`_¦´Ìzê4/ë…Y[’µjÁ‹{U!ƒ–pÅO¸¶·|6ZZt¶ˆ;€²h˜´À¢Îq‚ÅI¡TVG*B~o^P‰Q°O`¾A¯æÎ1Á3–ÌH<Æ-ü˜ ó˜Tt¤ÚDƱo¼¢}ÖûÐyj㨅¥>4[ôÃ}½š=B÷±Ó¤¯ðŒ%8³1Îk/ò¼Ú§@yè ²Ù8:£èÜRЃ§K|6¯bít77A¾ Ï”$dzÌD‚<ËÞUÌùC.æ—bu„$Êàsþ, [ŽŸ.ÚDpüýu–µoÀ3ì­oQå°·Ïè l’ûàÓØ«t…§±è9þ” 6Éff%•êÂ3Ô}Ô}°x„â`Kœ'ÇŸ`G,<¿Ìý¸›s5‰Í¹ƒìЭ Žé‚iºU-cP#p.îÎʼn#º›Ô«Ì†ôx7·á4Àì—_lœ¾ ÏKwòÁ"cMµÁ÷³B;mäû÷v¾›UWˆhP"Ã8[H—ë§Pÿ0˜F#¢ ¾Ä‹bÅØ-Q¯ÓòÞZZ“Δ¿YâyH€YI¯æ.@xþ÷ÉÜMf—mê¼R}Q–W–ÎLÆÏ„¬£©±ûd¼²"/דÛ%øî§èŸ££"Þþ†ŸAŠ&4X¢É\uÂßë„2¤ÌCB"Ióôk:@oDƲv€â%ééÀ’ôlq7/æ÷j>}Fè|âñŒòÿ’4–ßrŸ¥ÎNÿŒÄ¯þŒ—Þ`<ꬂˆoX`ãÅ5"¯M»¨_¶ùg¼´eç‰k!‚_g æ«Uã:S¼ïõŸÄÙŠ6>lÝ#¬£h棈 µÔÊGøÖô!hk#nkÅs-ø>¾ñ=/×:ů„/å^¥»x;/YWàJÐvG¶u®4›5Ìd U¿Àø˜uh?‹nÅí/§xŽí×fS³8Âü„œÊ#> 8¦C0·#ècÁ7—Poh³=›½8ê-ò=(X–Dâî™$œÐph¿×5OÁ¡ýúlf/š|0è8»DÆç·vC›M|›¤Ï@¹¡Ít6y±ô¸”Ï3o‘>fûK(ñÂä£×I¤c@UEâa’ÆYÜ1?)%´¾µ†ï»—â))|«)[>â(ÎgÏC§]ô:~ dï}ŸŒá¾¬¬HyA!}asŒH&‘)ðˆFäHð5nüdv ¨ƒ‚µD"Ãô|aº Oѹý<=§Oñ‘>E=ÎSÄŠyH!=ÀS6v˜P-u!ì"4—¾-P~aÚ9»èÅÚŽN³|\ä|G÷Mÿ˜9þÉn{ •Ì7h?ÛAÚYIl÷O¿Ç\}Þ»ž•ÌZ¨s¯Pg-ÑÕßcBÝ_]6¯‘ 6™ Š=\OeV z¸ž§äô#s)9¦ãôƒTN( Ï<%‡² RrLÇéGè:ö)RžyJ>]óÉ)9´ÕMßGïæ® mÁ3Ö×§_ynâyb9Ž;b¨±Bì9Mô‹’sfPÐ=PnN =¿†&è[Ž;Àãcà~b| ˜ûÖgóð|@¾ ßK™X†?€w“‘P^ãHPäåü¾ Û;,E{G ö{Ð÷ÞïèÎéŸCßGgbàqhIx½´€'A’)è|Ý/|¯‰ÌÕ]ÓïÃ÷¶ÏÅñå;$èh8ÖW‰1Î=tdŠØ|È#d^OÑk„sn"ã¾{úUèÇÖÓzA‡Å§×ÈÖ8’ °ã’ª€h­#ziI)Þ&° 4«¢©Ï¿æÃ#ôÃGæãžé_B?öÌckB1cèv "#‡†±Ž'æ¢ÒÐͲˆ Ãdl–îžiÁý©ƒþ ý©#ý99ýèÏ@Á&b!‰ €+O›‚-EðÖVÑy½}kñ%ì A!v€Å(Dø½—5H ßÊ5øAÉò!@ðÛÈ~"™Ùù]=¿±d<+›i/ÉÂ8ú„}–%ã¸wú-hû’¢½ï/{iP™ûŽÑ8A|´`°ƒFѽ€É„Ü}ÃXóû¾ŸŒõ‹Ó/áuž×fÂ]·vÁäåkÁf2ÏûOj3¡Ïýûo3¡ÏñÒ'ÐÌ•g3Ñ„På‡lü‹1š»ˆîâ~,ðØE¼t ß>p›‰‡ßûó ©¥+vÑFŽ¢‰ƒÈ³tåô¼ƒîá%Zàûæ±qà丶ydZlÀ ¾ªkÀ˜m¿€=2"ì‘‘=Þ ²Ç>^$`~.|y ¨TÆ2çØŒM¾xyy<+/™±¼ ýÙ#œO éÏ›¤?›gDºyfIÌ|–˜©a,=«Y!©Ó\ázF¡ÑnEùúÐ$ôŸ°¡ëJ?~žð4új™³&o‘9¸y–m¯I¬dMf äÅø),”‹†±€®Æ‚lõeù°Œ`Y¾„ŸyI?Aš‡~æ#؈H_*Ê#ÙHø5­,±¦/$ü—–;¿M„~€^'Ì<óôå—35]0úLü‹S''3¡¦ŽJ¸x bfíÿ¡±° A°Õh/âÞç±Õàßp¾2[©mÆM{žû)ƒŸñ-çÏ€”Úf¾ Ôtw’”gÊÃî cFZ^¡—“›Û‚-ÁþDH¯•KÎrC %¦Wpq_¦ŒTœê£º²í‘J ‡a8‘n ÎL[+°U–Éö%” –n)xÂ㦉`A¤5ÕáÍü^§ÍlâÔ•@mIp¼»¢XP:ƒ¼âN”wäMi‚©”Ôÿ_êÞ>βZžw=™}²L2“ÉÌdÏ$“d²g²'ÍÖ$ÝÒ¤MwÒºÐÚÙ¨\”Ý¡@Ë"WTPÿè§èUï½ rUøTPQh›Éÿœç}g2™lÅïþÿïBÛ™yßg=ÏyÎsÎyÎæ›et§FÐBÃ(q;FŽmh¤ÛýáCíDÔ×~Íw¯¡'¾XÖæ¦ÛB{7\Ú—»éাxMóö‘h¹Ç¦µ*sôuÑ<(zµxwíã?}÷w?9°¶Íc¨Ù{ÝCç߆ÞïŒNÄžöQî‡VGÉÚ+Fi^É ïþõÌõE‡o}è«'º£yôÐÚ6¯¡fÿuïº-öFìÏï Vš˜ˆkæâ«âåo…d‘\âcyœÊ™ aˆíÀü ÚÜÍpá!oonÊÎõ¤¥1Ç-ˆúšê`¾ "U•Å¿Duá"N¦¡ÂA’5m¹TO[œ/C£o‘èÇ5N4j9A' c¸ÝdÅ$’,%î.¯š½»\ª"PòTac @ËܵµϪ‘=µ µ õuåe¥%ŸÇ—i¶ä›lÅ©V™4ìôùrP„¿Ô.W[å`„‡¿KH Fœ~øK«#v>â”­ø75N«±ÿêÛ¶ž¸º>·nƒOzϧôÑO\2t ½¡ç3£4ðÔ¡žûFÇ/ÛrsìÏ·èŒ]qéÊ[b÷Ü>û%¼ù¾ÑW¯?ÒÙyƒîì<²¾šû 450MoSšækŸ<Ðõ…ባ[oŒýùfý±Ë î¾}$öË'tadâ’-7RóqÝýôŠ£+Óë»o‰v^2^]=~IgçQü<ÊöÊòkþx´ª5ÚÔè”×ð\{[V&-͹9‚H"5Áì²peq‘ Óê*\j¾`¯Ò¶€FljzX&ŠË¤ÚÍ/´¾%ñõ]¸Bêºfàº.°¦†ùkj ó>êäƒ~§íþj*û#V™ð>k˜­ðÔïôWƒ€á©Uæ*sªº‚A%ˆû<ü©Ìq:qÉßWÒ÷zb”ÒéX3½º'v}Ù}—}op÷-Yã±/ýÇÊXz½µ,ÆÓoÅ.í¡‡ÙKî+A%üPPýäÎÄšèÕ]±kKï?úÚÀî›3ÇbùÇ`,½›ÞV£ôÛ±Kºé‘’.ymp÷ñ¬1:qôCô½nè•Й?!û%ÐB-Ú‰ÊDZl¯<FÑœEÇ1r³³Añ^jcàc¯…~‰vÅb±ëc7ˆ÷Ä~G3Noå¯B»ôº½7©1Ñ^}öôVn#·ýc·gM´wæoñæ¦ç†Oo¥ü­$©= ñDÝ]°9óœæ 5ÎðV¢5÷ëÓ[…—ðYÍïá$%~|‡Æ0ðh³bûb]À¤`IõD~@µ783-ìæÞ_cía7å¹’3/A3MO›Œ²$ µÞÉ”ìwx†cÓæ¤86jÊ»¨Ž£pfY8 ¯ÊKg¦Å븷¥ß)m‹×)mÓl´:WŸ×ÌÜ6†?Š=LæûÎJŶg±’îþµ«ß/˜áüHNr„ÍLÎ_ù‰ ù-—Vçšøßœ•Û­ kµ×ÛŒFO8ÈðÍÆ~;¬Užîßákž&„Þ[äÅ®iR­H2½QM,¡išºˆ”º³¬æ„ÆM›«Äö¶@›k€ŸÓ«6'à›ˆÌÏd,ε‘_›û<îY5öŒb ˆøa³\a4ËÔqDZwîàíÜæéÏß7}wP±Gk„þJVzß’šLS»Jn0Ì„ÿ/ûû±c¿Œ«£_Œ­»9¶Š> mŽ]Åx¸ÈqÆ¥2a ¶,üY^ºã*îêg^ÚEª†»R²Á)ÖJ)p_;'Q^¢ ˆ áa =^œShlìLdÇFîeiÌçç²£wü%öz^ÿ‘µ¡uÕvÙ›–^áVˆ÷œÉyå³³oϱÚ[ÞØc¢E¼–Ô’^Z¦†*§’ Z¡©‘ÓhmVŽ×°Ø«mYøJ^ðÕ˜&§D~”]°°pIj4RÕhÇÃvªESÓ¨•,_Ç•ZG¹µ•dv “­Ž/_XóxLwÇ­/Q—ÌVEKK}]kK]o}¯×,(ò{óôÎbq~ê»àÜksìE¾<{j¶µ·R3åYs;†ÖW _·¥¦iß½[F”¯¿iKͪP¶&Ç’æÉ½9Vл­´`¢:¼ªÑ“îÎIΫ×1à­ ::®üúѽ_¿q¨²xçêOMÕ[í{ÜVýWÏ~®¾†ž ùƒ+/YµúP¯—­}Gì«Âsüû°ö­äõ¨ÞÇv8Û­‚êˆÐý‘´Ü8ÂG ÒÜ7.±·£¦Ñ yvy–¯èš[–'^Gba´–®Kf«*«ÓÔX×Zßê÷ú}ù¸:þ䤸*áÍáã&¢óó+ò›x?î÷¡ÉÒ󞼺¯í’{7Þ{aT2:ÍÓImë#‘á¦3ÛÒ›ºl/mÞ“NtV¬Ð› ×}çªíO]3Øtà‹ç»ýmÏ®oVn–sãœl|÷þóŠ}H«ÖÁ:¬‚uè#OD­.ˆôÀ0$I¤v-ÕREK u5Ed}1¡5s² ÄÊ¢h©jL›qMZæUÕ2i|É&Èl cQgwg´¹©¡¦ª¢¼¸° çqg9lyúôY0×DâáÄ’í£Ì Ôžœ…|!ÐsWöç ݺ¯}]W :/-¼ý3[÷ß_[gÊ0§ç…:B+Ç×^pÁ–µ#CÕ]•N÷|økõº‚}½Ãû=Öʺ¦œÈö¾ÒÎÈ‡Ñæ²5„ÂMEkë²½Ù)k€°ïù@XçDzäV¹ÍØQh)jå|E¼S}a ÂâÂ\·Õl2Ú Ï†"Œ !5fOöš1"á¬8DŸ}7óhP¼:¸•»Ÿ¹¦Ï×:Qì-ñÓýþöpn°sCu͆΂Üp»Ÿîó—ì ÔM´æ÷]óŒ¾ûÈ-nêsJò¥Þ¢ØŸ„ò¾­‘æñF·»q¼9²µ¯\ –"沈äìÛtaËÄGº‡üã0?8Ëâ…ÆO8‚¾ƒFv‚‚•%bQêófePu{-²[9ã3ïó7ž‘QˆÛ¢‹ ‰@ù‰œÈ¨vàvÄoÚgíJà¬åŠaâg >–b¬É³Þqtã[1cr~Yk¨i¼1çò j;Êò]°K 'w쪚xà’î–KN]¼ï+ísR&3tµÒ’3&s¤qæ¾ÿ‹ZÙxöøEWÚ,ÕÃÃz–Óœ}Ên»ò‰—ë8%Î1ßÐzþ½½7ìnmÝ}Coïç·Æ¦í¾Ò W©×fó–º2J}vú汯mh8úµcÇž;ÒÐpä¹c{,–†={¬Ö†½L«E8‘$þèÓ&ô$G5¦‡ñÁJøG4Ec"3RI~w•;édî–yxØÇ°{Ëh9¥Õ¯Ÿá¸“?˜§oã_<ÛÂù³ê܃ž¦™FÕ¼ôp¿ä_~1H&N᱿†@OTœO‰ñ ½ŒëJ„ð‹fÇ Œ†.`ŒÉÙ×cQ£;ÛçͺƒEy’eÜ*ª_Ьôù¢·Z Áµ½¡u—õ¯<¶¦´ºèvWéo>È.­­•Ö5p“§{Ï`Çe“u5“W ~ªëÁØóßo«4Ö×76¡@9ý€Gÿ™ ÒDö<íÒ©·+• F÷KK‰¸dt?;³F Wúò(©‹T6…›Š ó*|–tâ¥^m! Ì[ªXjf”šø½ªïèÚ17ÜY6²§5ºg¸lg *×X¾ö芬€S…Âzg «¸¸¤¨¨´H_4xA{^k¤,½éxý z¼¥þxSzY¤5¯ý‚Á¢WÌî€=T[²ÜfzG]AQ(TTPÇö=,Úï6%d$ª+ ”׫·©•jÄZå•aÓ‘ïJJñ0ûx,j¢$Ïc³¦I -Aý›ºÀile‘\zñP±bìJœ)`e9mæé½‡FK =Þ«ŠVz·”MX3~N³…%Î ½¶6t­¾mò"þeƒ;ªÖ­\á¿Õ”^î¿k•«ÖIi^}mMN~Èðdêsú÷­ð±yåÁš? x\BöEÓŠDÀÈB`ìê-eÞ#ïøÅ£z=™–HËkǹú¿¨L”Â;Ê|L®Æ’£ߊ<½“’öªSZ½Üu+ﲘóÌ]ç÷ÂãW T®ë‹f½f§7¯8¯¤¾¢1#d»{ˆþ†_SÝp˜ã‹úwEû®ØXíVºÏ7›í™öºp¨Ñ¨/á 8çV˜ó`-kÈ¥QS%0ü؆võ¦×3<˜ "»<‹òdTŽß3ì·+WCjA|§h¨—׉bcQœåEÓÃeN#5´F_õxúeö5'FDc“¯vÄa`âÙò?Üs~¯#ÚÛ]4|t¨sj¨ÞqaI‹)­º°¤|õƱ¾‚RW•YÒ¥»3®54¬ÛÇÿ ´g}Ù†][FZöŒ”{Ú¶¶UXÌîœÊú’úàÓ´²:P’ë3ØôœèRÑ]™î×ôWíºFÀ•zS—z³XêôÄM%+¢ úÓ˜ïË‹ ¢¤T~Tqj¤ÖøN†9ӯʊÇW„ÊÖ\:´âÐpQzNQÆ!犒7(Ù*+ü…•ôHznÇξöÃ5k÷T¯Y]úã Ÿn 5ÕÕ555áÌÁ{Næ¿EÒ‰ƒt²©s@\¶'îñ'P ž˜€ïðv%½TƯÜ©zYa\Ƽœaö'æ>}0ÃÑùzì1[@̤Ÿ|ÿu^|æqú‰mÙ¯¼’㼊눭uwøco*ry/ç»°×̤VŽ9q!ž–ƒÓxšˆ]¬8§c2%31û‚¨×@„Q@Wa¡l¹ïÚ´±LÍáç¨Å[;åôò/í©l°MÂWÉmë®Ès<Žú“ßÄ®'¹’e‰Xt?·¹Y²|xáfú¡üIùûP> #ž`<=ƒ|7`°‰EàƒtNvÊA9èin.™8ï¼)ö¯äþp·æøÎì S»MòqUAB¥Gw'?‹8b5ëu$ƒË€nÑ`:妒::æOO1ë®è÷e;Z66çö÷4¶s+…ïМ\dM« ù-£å]Cma[ ÛÌÖþïB-lj·Ãy¡æ¼Sò'`ž&x&yùôâ,Ê‚èþ=|/#m·PK7”øbºÇàû'¯ëƒºV>PÓ@­îNËô}. åM¾’ØP÷6á¯Áú ƽW¤¡Ôcc‘¦–ÅÀH<_&&½¹›—Ãbƈ81qd¤è5Õa•–#Ýö©«ÊòÜ•[ÓWâk¬.µdåÐÏåFúŠó›ªJÄ`Naíú¨Ï•íÊ-ˆLDó]9.†ã·Üë€Cˆcðä‰Í4ˆ’ oPtqC®¬SL/‰z(³;AdâÙ5" K þàâj87š íN.E1¼/ûŽbJÖì{­„áÆ)QƒÛíö¹}~W  ÚÌ]c}^ëeÊX8ÖVZñÇXƒ³c½Æú®ô+|'>±mæMÚ£m&C;Oy(PØ„`§‘$Íd´x–p2áÆ `‰±>I³{éRcQrIQ~ÞÜ„:fÃv\(ä^ƒ=Ê|„B€aK޲Þ߃QÝÆ¼GvÜâ!’)²½Ê̪Ú%­uFã6VŠÓ f  »ç£È“±ï‚š ^€Äß'Ñ–•ÅÓ,o–7'Ûf!(“ÏŒçüºÆ= ÏÂ<9qÃN‚žiŒ…”4i!0¢‚KFa§4ÒîeŠE³ÝnŽº«ÜáÒbþ\`êã6Z‡`\Ϫð<¤ÀSTp²ìÿ"xzrRá¹R¨åﺆã†ïÏ(¹Šeíz;öˆømÊÓ·É¿‰ß&*N•Ù'ñqâ×®üàGQò´ÑxJ(R.õ!Œd–T2êÆcî žO~C4SZR”çq9L*S9OÎZl|­ä÷ÿÿ/?>¾a-÷¦ŠðáãàTÀñ}FŸÆ·GéÛ“@6]Ù4Ì>-»m¸¹ Q|i†°Œ´ Í”¤n9ÚÚ©.-öy@JÌU¡ò86üÎÍ!À#ÿ àÝÈkœ/ÜÂ=%V¨y”f=°¬Óù\÷ôsbÅ}Ðÿ&awRµ+‚ïÀg>…oQÌ- }âHkHx.¦eÖ”¬»|.\!s¡ Ìö–c6t„™2a­-ñ~8‹Ù¨—%êàJRCJ‰£ß™ùpŸMJ£Ö„-Kòņkö)Uãh)igÆNú}¨Øq†5¬rOTýÈ7r ³ìù¥®Ñ¡¶£%^¯ÃëЭþøo‚™‹—´]±v²Ô!“lûb‰ñò«¼,ª1 aÌWA£ýJlM«‘ô(ô†¨~ÍhÂɋ̚R‘MYÁn–]ÆnK7!kO2h†\LžIðU³¨Ü0,^ -­Ýëö¯hÞ2ºb`uõŠ©¶œ“VO¡ð{Ž?Ç~‡Ý“ï±V\ÐSTí1(²ÎcÀs~è¯5‰ ³‰ë>·óU¬ùÌÈ‚ïN~¦ˆ;Nâ8QÜYˆÏôÑB1¹Ì ͹} —™/|ǽ— 8ÿì³Ô=÷˜²çšÿO÷œaæÏÂOÅë‰ã÷QZPL­N^”ýìoÐêˆð—ûÉ3o¼@«.‹ýô1Zw,öãgÞx1öýËhÑc±ï\.¬§e7Å^|÷Ù߯~ýçg~{ý&Úúǧߦž÷ž!D3ó»™lùŒ”Æô°u¤ð¹ŽÜM~ýÆ$tI¢k*×Ågdj234SiF=¯Õ¥ë´éSN»•7[ˆÅL¦ØŽÛí¥ª(¤ñ`v€wåÐÌ Wæ¸ÏÇkLÄ9ݸߖϧ;`¦[Æ=˜Ûˆ yf“7Ýpý¡ƒ›7 ô7ÔØA¾ã3·Þrüæëï¾áî«®”Ëj?ª1RÖê¬ToÒÍé‡`7ìÜ/ýÌíz÷÷oüôû¯>ûÌ—ù½·ŸüÌÉ®»âØ¡Û.ºíàóv¬[Û·¢®ÖçÍÊÌ·¥Ï¹UŠ8Yø;/çÐd;99ˆ™Õk"Í|¤ŒúËh3ÆgÄ3„Œ ÀËH”Ôac=Á2¾…2•+3­qðóTÑð’w6ó4I}ÐM™@lTÎ(eZÚ@jp ÔP¥ 6>—:Pïƒ!;$aÜê ¹}áàK³iÌ2´]žÉõŸ~3£4ñnÜŸÏ:Ž¿ü$kÖe•²9«Ï;ØÈ𵩽JÂ.ÿÖ"—UL~iÕÃËJG@Šu[ó´^sf¸7ÔÊ: ƒ`v™×û ¾Ë«J¼ÓËZ„;¼³|Çb¡6›«¦Za5‚Pä5éi…Û¹7|I9mº©`‡¢7_1SÅ_.fqˆúÊ ¼pÔ¦kñ’rž¶ëqpòÑHƒÅš¤ÅËG%Ô_9Eé"¸ÄÜ䀿ìÆ/—bÜZúÛØŽQW…½ÃžŸ.IZ9àì°‡\£ô.z{ÊÕhe)·¸Ý^á=û7Ô›šé+[¯½ÀÞUEïl¾¼ÞVäìuÈÎazGl ÞÙ{Ê‚õÇšcçUïû‹{íZçp¬ì~ÆÇ=É}+ŽqÇÃ|0&a–Óv²xµxgÁ¸´ª^h<¹r¥Ñllj»D’K7|j+_-Jsez\¥ÖHNÇÅcU„ˆdb¦„ÿ©–xX$Å8.‹Ú´TCC圬±‚h p¢ÏHíWƒT¡òFO]„W4|<ò«ŒËT3–Z>©‘höNžœ¬LŸ9]k).¤f/›„]¹{Togd¦þ§Ìž¡™ç«ðAñÒ¸­5OÄ¥«‚ÙÛŽ¯/t…‡ë.9X½ã¶Mð€–½‘ÂÒH®Þ]ÓPÝûÂJøO¼èJWÁËšúÉ+‡Bë[ž»Ž®¹uO›øgw¸ -äæcG…üÚ.nóº1CìÆôuÓ=ôúoÒ«ˆªÇ§È@«Òh‘6–ƒÛd‰ã¢Lu˘Ù\¤aDÌN%ž0eу4oŒ½ÕL× >ÿü ÔóaTžêÿÚóŒoéñž€µކP^ÀØûJû4ŠWÌ-ÐUÖ¤¤êéˆÎa~7{¡èÄkνµ{¢ëc×Ë/¬xþkýCò,ïcì!ú²äŽç}Ù¼ždÅžR>É÷•OîmôŽ YÂ4Ù¤\@ùÑ)JXÂ]% èlÎo•OÏ líãå"rhá“”6T-b†šØÙ¯a9Ækcj¤ YütŒ^»ˆó[II‰‰ÕÒÌå‡+…Îb·q&Nðå僜±å˜Ï¶<1 µ½×>ÿ[*?N¥ß>m/þŠ>~72‰¨F&ªÑ Ó’SOš{)‚3„Ÿò2_6¦um+¦mÅ쟇6!„`æÆ1æO;0ëOKIQaAГ«,‘´øñÎ @z*€ïˆˆCKÌíßÛôyA¿©ãμn´‹Þ¥Wñì{£¦4ª7DÜuYÓ¿ä_ì¾³/—Ì_éÇ^*òý}½Í‚ ²PÈ"0îÀ œ(Ï ü#&߀-ˆÑ_Tûö`BƺšêÂ`v»G™¬fñÉZ1€Ó¡0UJøß%Wé±è¾u †k3Üé:É\å*-èÛ¸º¼¤°:ÍÔRr¡£~hªsèèpQwo´Ã×sþöÚq¿"r. æ9½ÍàË- TWÒ§ƒõ%õ•9n³¥¢mk›§|dOËÈ–ŒŒ]ÊÖ÷”ÆZ“Á„Yd¥¤µ+!!àMêH3¹7jlnª¯$®ŽòºxVšJ­†“8'馈Žçtüöƒ ðõ¨L‘7–º²¢á…+ÁŸ‹Ö‹ÂrÕF•!t*.ÀD1^wö²ËE}(¤„½v |‹zeÌe÷YMT Ëx×ì\reèùïÇ.Ê6:òz§#ãà믿þø3"»éåî˜ä°Œ.½ÓMÿëUΜW^É¶ÝøüóŸý,ÍñwœÚKo®Ú[“3L¦-}ÀZ>uôQ-74Øßž#òr.Pš * Èê™1°–ÓpÚM |Ö ˆî„Ó"°4Dæ5˜áæÑñˆyE#•(·uJUÍÔÇ®;ͤdEoWªøËJ ƒ³`×/v/êþP/…gw†rGAkÑY–„Ñûc¹CwÛBõ%yÅy^§ÙkÌŒö­«¸b<è=¿ =wïZÙûñ]ÏÞYÎ^fQNOÿ­Dol …ëì™v³ù|weÐQ½ñоè®þ"ž;ÜP½ï•Ø#VÍ3è5š´F8Û1&Zo„c©­ä— T ¬1¯ £>Z*‰¥ñÔàuµ”t v ¶4Õ¶×µW‡ÑªxYhfQ?µÏqW„GqÊœ»ÌŒ‘yû’°¼óƒß”ö»n/ª.]sleÿeëB½kƒŽ«PÜPWª®=è,n 8 s,Ëœ–§hg°à]Ÿ¸j²¦nò²ŽÁ=Ýæ«MhS©mp÷t7™ÝuMí9gA)Г+O•³Ôà Rjà€qa$@9B1îàØ,ðV!ð€p©eY†«¥ c8‹²l e¡²Pvivi ß«òÚ%x 4ÈòJ2Z¢E(fŽ“—¡Ÿò:i_©7ö§çÂÍÀ’hmg]ß\ýwäUtO®ÒÇ]nk¨Üó;§sÍm‘yŒL’íäËQíÖ‰ª€ ÑR5YyÈ€2Á;9­žjD­¤EîcÜH•ø2iT½}OÃk¼H¢ ‘á É‹ÔE†ß˜¤hh°¥ÉíÛ6ošÜ8¾~õèàØÐXwgó@Ë@YIv“»IhúÇeܾf6¹S1”ä%_n𠉊!ãÙ ÃÁ)-Mn[qtm¹1·*°³lxO´uÏHÙÎ`8×Z{´/Ù ¶´¨´¨¨¤¸ú`oC£­qÜ×}`é5:]üOÌ^ϤY8Î’VÀ§ÐŽZÒ~Mt´§ð—¶¡¦C„ÉMaDÀiF;x I"/+I[…ËÑÓÙ g_g_{kSÞµÆÉ†n Ø#·Ð¹‰àÕkœd –φ‰«—„òÍ3ä’ÎCE9éEÇV ]º¦,´b¼¸,0TY诨 µ}%KÃô̧cøƒ?.]½f¼ªçðÚŠš‰Ãí};;rÓifU$\SSS½aóWýÞ^”ÇÅ$Ø•÷1BÖR^q3ŽR ·vM$$®-+jR¿Ó¶Q8¯ZÇû™* 6ƒ†pè5‚ŒVé2•y:¥Eã8mÕƒO;+(ªÎiõóêQæeý°…I-ˆ)-D£èÂ<Ì‘4ðgÇÇnbl,ê‚7íÍM Ua¼í_v©SH:GXJõK Ë OügRËîI’§„êT)óÌkç c¹R¤Mºf®Ð󤈟ôjUçșȶ“ÑèÊö¶æ¦Êb_¶åÍ–zø·6’@vÁ©À »$½ÑëxJÉÚ7$ÒÄv¢,eyY²ð¹ÄÁ` Ï9¿¼ÍåAú‰$ø™IúÁ½ ›}|!5öá|¡•TÍYYKÆÉ&ò‚‚áö €Ì¦É•½^QâÇׯCa–‡½äHyƵM€´„{ k•!Ò’]ÇñÜ. Wü.L· ƒ­ Ä‘c—i£_©TÐQà=w ¯Y¾9Á‰³vÍêUý©óR ç9 Ì)xÏåWŽþ霄èÓg“õo RO[–_Ós¬cœ»Ög_A›ÿÌûc„l&Säú¢êÙ»öÂç¯õë¿}Ko`¸Ë1ÿ1ß¶ƒ|Œ)ÒyŸÞÄq,8†Ò ¼emA+î ày ®k€¨jè”Q¥g w’änã⊄¹™&QŠÿ8mâG–m“èdŒ9=5ØòŽZÖ¥´]g‚S@O8ýÑ ðgÇWÓx˜H>u²Uk×,¬ƒH[ÙÏ]‘‚õŠFâÿK/q:–´ø -ÅÙãçº>†®"e/Äú™î‚þ‘íÍýÆ@éú7û⹨edxp ¿·½ªT+c€4c\‘Ñ`b¤JK9™c¾vôœd:eäyãF 0d<Ò¡¦$ üœLê—¨mܵdå±h%Ý]m­-xÜ××ÕV„RÕiC½‘²Òó”þeûå\ÇéuÉ«úÕýÇ™W—_Þd-­»tÓbŠZd[C)åìZO6’­t£²Ý½“ë9·uËÊ^AÖloçµòÆŽ×Æõ%¶¥Ëè Ì˜"vœN†MD ü™–ORH$Š‘v™bÑ ªK´L]" LÞÄ‹^ˆS¢n¥5LüÖ\t`±– ^Ð!‰íí8·ö€äxPÝ©œ« )sLÿ Êœ9¸§ªv–§2Ü¿ž«~çôÅɨR”=gcç€Ëª|–¿T4@œN=_¥9z . %Èò£hÚ–ÍA€[‡:!$&ˆ_S9ŽP9g…¨?p6è4ZN;Œ©–j1Õ+§…jˆ–×hÇõÉ'Њåš!’–×JL¦€Æv$7&Ím NœÑÑžnJF7ŒnìïééêìholHÖ@-…Ö…5P².½Y@uÇÌ{‹h¥ÎŒ'¯÷E©:ª³œ˼ˆ¦j} zÍ·‘­¾8GÕHZA–ßÝÒÞÄi„Öhu°É\›žj@âРf ¯‚vànyód<'¬9ª¨$š ýmm¨ jëhë@Â_Y1WMµÔÄ<59æ_FsuSªæêtAÊÿÌÇÖeÅþ>_" :sÎä&ÒO&€CÝC’#ô)eËØQäpg³ Õ]¸»* è (Øç>âÚ©2>*ÃÚ0Žð:ØZƒV0L‰é¼#‡¼£ÁDõ4õÒ˜á ÐÄ4ÊRj­µP­–¹HÓÆ½mz—h) }$€¡ÓA=A§_¬å9éмAþçd‘›JéšÝ±L³bJ³ÑÕ„^’Gº¯ƒ?;þ{ÚE^4´qÃàò£ìÛ»ç‚];·mÙ0µqjͪ‰Á‰þ¾½m­ÉÊ>ëTbqe_ _rNª¿s ¯,ª<ól2í˜ZFx.\êrJAîå í£eµ„ôÃΗZÒMF㙤õªù8÷ÉÑ:AÔ®­j;CA³Ù7ºÄ›¨5ù×6NeØ1j¬¦Z8xTðZ~—AljZVdz. f #h5ã’¢ –fõ%j‡ÍsêSVÔêðö[Ùq­DÛ”ÐOe­üO4‚¼ÌªU”¬Z¿jýÈÊþÝp’55&ëD‹£¨vhªÂlž†ôñ·‹êIÏ4$#ãæhMÏžƒ’`qÝikŠ$ô£¹ºTºVA°¹úÔ ²¤N…6VU8x`ÿˆÀË\±Òñá n÷söŸ«ô“ýWžØ¶ý‰«úû¯zbû¶Wös«·À_ùõÊ~x‹cl ƒÜ]ü—˜½Ø6wjT¡Ô>")1kÊGö¶´ì …F÷´´ì)ÿ¼#v»ÃGü“»ã` ¥åü¼ 5't8‚ᵡgõÂn ¯Ù­<)‰\<î¥>Å9h6^%aQ/Ÿrâ2˜q+kò½·|øÛŸïm{•~D+E³£¨¹àáÕÛ¿("ìæ?PÛxƒß‘œOÔ´H>Q¥}€1šA¾ ¾Ö¾÷ço}x‹øÚöÕ49`ìˆø}b&íQ™9Æ¡i¨ÒCºbšìêoŸµUœòóÉ;™gÉç-ÅfèˆyØ(¹Ú»vÅMÇ~ðÎ+®½ìPð°5˜k]¹þÑgÓ³ò,ñ¹uó¿%Ò5iÑ4:¶Áßh| è/%Ëid $jÖ‰±“–Â<´VU$ì`5Š×5ÕaþÐí5—ªo:rìš7^þñÑGŸKÏôY¾c äÀ(ŸßãÂ,¾©úP!yä)Œ¸E¥~ZGÎWÆàS2ó(^©nÿš¸Û¿7©TJM<@fn.Gs rƒùyYv›%ù0æp9:Õßóø}þiM⃠ë’GNr8ž¯’ghæïJêDƒ}¡D_̈b•€·=^/G½%Þ¢@¾;ËéPûñpÖ,ÙáÞ&}pʈ¶çUÍ´‘¿H¤Í%#x ;áÓ‹©Œ1Dd£U9o{ßšïþìõi±•\Ïô³Òg§miïdfÇináN©í£ùè:M3‡ÎÙö)k_Hô,¤‘#½$Ê):¡ÒÒ‚´ënqû¬)GTé³Ù™ï¤Ù¦¡}€ÓnñwüMò» NðøB7“·aîÏþ’`ÆÛäqÐ%0LÃñ]”¦§™€¹§º|QÍ7ó®ø;îkò;j¼Ylï+3¯‘ßb{¿"˜1)y)íñ¬=­L ¼AŒçH¼*“Tr¤ÆñŠy¿)M3â ¤i²KÁ¯<-ê¿%XÙñy襋£—g¶P véØÅÑöÖºHy©ß7»Ð›˜'­°&oI1ÚYŒq^™¿¦É`fÑ:Ö v˜mH<1~MjH¶ÔÃi­3vgU )¬Èr‡ƒNz“ä³Òïpø+„úÓ72ôÈÈ®ðÛíþŠì8yMù;ƒëZëðž|’Á­Š­ÃÓ3?TÖÖµ½ïD™šwGÄÕŠ¡2z [˜ÏjÒ|˜ÚÀ‰ÃX²sì6-?K»ƒëý#ñ;ÜGòEêzÖ竧߃}ö­™Ãäb2¢Üádñxý9Å|NU_$Þ‚Ë‘“ò¾1­!’J3ë‘}šaž*á<îékJž*á<èóÄG¸·_$ÐL²ë”†r‰¬Ãv4@g™`XÀAö9á,m‹g™ó“‚!>Ô¤p;áÏ2Àà˜ž¥Kõ›‹½J>â*ñgÜY…óÏ`,O|Çò-ò Õu§ô,¼2 YT@íû”êߌin“Z`$—`¤Àž‹¼f%Ñ yPÈâ%±‚d“Åãߎ\)¦UÃ¥TÒ“³,*6ö}“d¡•l’m…ÿ² ?ª×nøä¹Ÿj^øfÑî°‹ù'Êç3ðé´ õ0“]ÿjüw饳ÿ~Ïš cy’LóŸç'QsðŒ¡ våÖÑî‡cÏñ“1y'=ÍίëbOÂÁè%n2Å\íNe±4IYøIY|ˆs WÃúÛ)0‰ô‡9‘%Õ"tj¶ÆqV 0_ë '†R2é¦n)­Øue­v¯¶¹óNÿ$;ŠUsø›yÌ ¸zL†Cè A”ð‡á oæá”µ{«ù›§¿È{^z‰íÓ—Þ¥îÓ—nOÿáöìÌ/Èf+kCYÞ*FÂ-* OÐ:¬Ç_¬òÉm|•†6ʢŰ³qìZaφ6Ä2š­b ÷VV±I%`‹°–÷Iߘ—A%ÑÚyñ2V«±:$õ(æÉ®¤°Œˆ&½‚‡ðnˆEÀD&†çzXÈå4SWWŽçn­¸üE#°1ÀwÃ3ߦy€³072Žô‹&Ó/˜ÜFLÑ´K¢Œ.²è*«0TߣÐ/6Ž… ŒEµ8{ a,Mâó·p]T˜’$˜~eæ~€)Ë"[nz¬,ÓâŠòªâ*Ôx.¾Ýâ¸çä×TžæG0§o‘ûH|µG1ª…+‚‹¬3R%_9ÇöÅØ<1ã`ƒšOñ}[/Ûƒr!ËÁÈš§cñ£ øi8$ñÎîM=ñ÷ F_Î,iôzêŠ32Šë<ÞÆ’LiÏ}ß÷*¿½Þzü¬ÇùLÿUüÕIoª0ú¬/‰¹ ã¦DNà™¼Â¾(‡8È,=GÍiz›Á&‹TÇé$à[hìyñ'´Kúà[ £%F „fÃP2"xIsª·~ï‘Õåߪ_á¯Ì5~«|õaé?ü[škšÒÝÅYÛÚ<”xé å^²EáÌó†5ŠÇX"º FèêëÎûÓŸv|ÿ¦müã6ÂÊíƒr/Ζ£Ì®¾~ÇŸþt^ì,+°øüôûô/3ʾúüÌk°:{O䕨/O¿Ç¹Ô÷/Ã{žsãGÄK(q¡cv®tæaúa€ãó„|ô‹¥4jš çfa×+Y`œM Ar°Â“©j”®Øú|ì¤â‰3û6ÓßǾR²Üû7W¼m;|‡¹i0G¼TýN2ï[çf· ë7QêsìÇŒáJpÌð´—~ÆVÚ2³QmÄè“#ñ˜éñC0þ(1Ò°ƺù$­Š¹èwcÿvã•p }sÛDbµGÔ¦z2§¶ñš¡ÕI9³ÝGƒ±ZÖfSìzŸùWÀƨÂK:'F(ìXô8E!ò]YQ§Šps_$ðц£ºhº„ûÑѳõ·Ýûþœ>Øú™•@>JÝuú”ëB¸ªBâœçñ`vhžïÃÖ¿ÀÚvÄÜÜÑ™'€O³\¢$ª§˜á;E_çD}Y­ª‰øÙži°Z¬ZI±¿ÆžÖeegi?³ï9\OŒÅ}V ³œF]j®FàÆ‘QZ=G• Oá¼À%ÀŒcà!Ä·êm6›Óæ´ä‹–yr`SלH=~ÿ€Xà©+ÊÈ(‚ßÊ!ðÎôœÆô€ ñZi’Éõ8mÈì·9ìÜœ$.ì´bšÁy‚' RúÛÌÒäK3©?vÓzP|!ñPéugìø~z!ÂâF¡ƒ»SÚÁú-ŠÑH/ø@Ææ8æG©Î— ¡×ªGM ;ëôV{é¿xëJ22Jà€kÀφßMÿ…3‰E™%õÞ\eÖ¹Þú’ÌÿBú¥z¹ûÅk_ª”;R#;z¹Dn^û¬Sü€ŠédM¬ uô­SÓSÂ;wǾyU¥ýÒZà=3¢Œ¨J‰D£ .ÖnA¸ùaeD»ßîãËb¾!>2}ëOŲöØmÜÈ?^Øõ†o¯Î\(íçö)í`'ž“Â)̬•i†Ãf†-½ú=þSnÏé1n_ì¶ö_ÇÎîzñï„ñªç Ü/¤Kà[. G•x2ËS”…Ÿb"^£ýXÑ€˜—%s¸ŒÏ£{ ” ˆ r@z•Ô ‘ŽÍ§“…‰¢<+%eóÂŽªY²5'ïÇ©ŒÔ:‡²©}v)–«­PCšŽÏÖ^¦Z¯9Š=¤€ÚÛp-ýö ßar%-gö\,ö%uõëoØÖi.7Yt‚¥¸»¦xïà¥kJ—@>HûfTXË}MÜÍèÃ(ê&éÛ±/+:É™Äo£ä‰2‘qöêÈÆr%ëèâi)17“á¡Ý a+ýœøÖ.~çÈ‹:ÂâçÎ U~.öî̉ØÃÜ„xî]ÌÚLÎcJ¶3x“¾#ªÏ¦"¯×ár©ËΆl÷»0î:ó=¹ 2S@Q>'Ckœ û€Woœ¸ê„n‚e7°4–«âª(W"óeÊ«±Sî¼üÌZ±ÛÒ¨]ù—ÝÉa¸ôBj–««Zh5û×ì¥/ÿÕó¿àOÃ7÷Çì±oÜï¾þP™ÿê±'|æý›/;á7ï{éØagNc¬Žú‡Oå¦un>åIëTîg¾{Œ%Ï™`‘ÍNTÒÀxþ±xR)U!`d¦g7©[yÁã1Zü@ä-ù4¦HMmÅa_dü¨÷TM})·kßÊâ•!»ìIK+Ȭô?ÿ2ËjÛá¶êÚZÌwÆîâ¦ØOal¼)š;* Û,¬R˜ˆ¤Qá}Ÿ:ªÙ÷cOû½¾¤Q¥fA¼£lêAef%6 ­,Y¹·Ëóü[Ú Öœív%a!Žé1î_ÎnJôùp‹ƒ,Q`A¸Òˆw¼²OÅžž¢/xºöÂèB˜Y6-¸è`}OÀú^ñÿý8þêÆù@/ ,·¾pß(œ~ Î;œ'âËŒ`>F¿ ý} °Ké5iÀµx?É!? G+ÞÜÕPN‡Oyå©Ê\…´T\¬UÀëèh ß©Oå;Uû‡ªÙz:]2£´dÝh„è€ÙÕqSs»]¶&};¥ÍMõ¡2ZJK¾×iˆßþ xŸOœ÷co"\žäÁ¯¿dV6B)e©ƒg]jµI?°‘õ¯“Œùx¾ßb§ÖL®¸x½ë,Q¨µ4£ ”h®¤n‰¢ì '^žñy^JK‹½eye BxÌÖV³×`\±Ånàÿ,Þ†0±1˜0\y™C³»_Ça¢(°[Ž.8™v„vÆ“‘p8 èÑIK£]»TÅŠ¥S ±Šæ4Znáù——zCy¡…æ/œ?•˜ÿ¸:ÿŸÁçïVßZ@µR!•µ&veŠÛeö©ªn—j€œC²ÌøS»IÏ!«¢:¦§që?ÎÞžF ã(13ŒóǨìJ­ŒWcâ$\ª²h8Ý¢•dI+Ïæ“þØÍ ¾´Ÿs3ŒyS픦¢ÑÔVà51ŒkÏ­5–ä0ÀÝÜDi_oSs?m¤èŠAÊ ä!?݇x@gÞ7(´2<ø£B~xð×jgW!ÍÀ1ÖÜhL|_+¸8*üsõ]\Zæcòí¹Í "¦mÁ•ü¸--ƒZÐŽa×~Éf–ªÅ[X%z»›V4¯X%ð¼øÈ*_/VÏ =AKX–ßÏÎ|‹¿šôŸ1{vþ9sØÈæð9hgçpoÔ­«åµ4œÎÉ|%å\JA ÃDY”¦ôTK œÖ°ÃñnMDMÙâôžeees ëP´kٺɵƢV¦PiÔØ¾ü‚<ÌD¥ŠeT™G”Íãîe3€yœŒZëëxtá“X¶hRŠˆ€Î°›´T&:NÖ­ÇPI"'Mhè¼@ÖÊ„‹±†,è¦Î¥J´Ê0ÑÅể=MCQ¦%à+*ø|zŸ²N^˜_T™ŸW™ß6¿ÏsßV×I ó»'jêé\óZ9QŠë@Êø‚0•xy¦ŸŒdFŠhcà8uÄe)h6¯ÚÜ cÑüÚHQ¡A¿ša[Wgsc¤£¶£2TXST“•¡/0ø|iÙL×3óØ!ÃyŒ(ó¸IÙ33ðWÁ<>ÇÈ/ÈkѬäÝr~/gÔî¦zã:J ’:«(Ñá¢7LÍâ>á5xc"­I3öñz­qÈ EÖWÆ`PlºÙ<›ãMà[˜"•Ç—m,©±hžA¿ç‚çmžœX¿fUÿŠž®Ôm–ž­êÖ‡aÞW*óVæ}£:￱yž—`Þ/E­Éó^ßÏé ñ]Ö<4# †dÐ'Ìp¼êuèI‹5)M§:MÓÄ—´Î´àlgnݱhÉþ}[·tvôÚwÙþËvïܲwëÞU#›;7§Îןo9Ì÷NE?V®Ì÷!6ß»g~¥Î× ó}3ªÏB[[Ä¡µ2ÏšøxÒããaf‹Ã2gîfù=LÀuà0Mñ)*çgZ= €ŸcK6“R7ZÉ®õx-Ç/ÞÀÜJ°Uó†[£¾¼Õ£ƒãCã½ÝÑÖÊP^‹¯…å©…“œÇìÁ ,ªXlWiÓ‹lï~næ/üUäLÔ:ÙÈ4©ÎÐG=’(A%QÍú7èôSDÐkT4è4†q¢'ZN¯OÄãbd«%¹½ ú¸ÍD›R[€BDÖ R-ÖÔ\âfß¼ibýª‘¡Þî¼|•Ä™|*άb{„Ái•§‹‡{áD,§×£¶ V=8èÁå ŒÔÅ7I[‚($ö+ÑÉÝxœ„™ã$ ƒ.š¨²Wš¥%È_‚²ÀkØýT3¾lks›‹)s1mÙ´a|íê¾Þî…È¢E=‡o¹Ô\p·¨œK‡Já¿Ìf/Y ³ÿÂÓl¾4qôjx 8‘11?dM:F ltúÄ+âµð=T;>¯þÜ:cÑ@g§2§ÎuëV®\ŒÈs¤8ö„Ø"^KŠH3ù7å¶™ ¶eá§¢qÑ2« QT,c‘5Pr’· 2§jý͉ÛÂe fи<_ SL@J„©…jÄ%O–• 0[”'¢ÄcL¬ "Ûi£¤¡®¢<èwÚY$E´H“V,V*Ú2ôÇŽ”ñ茋©Ò©…¹ÜbH¦¿¶¦¸çòér g¬r|ceÍ ÿùù5¿òÞ­·¾÷å ý7¾rÔS’‘a2ë5¿­œ¼~õš6VVn¼aÍêë'+7o´g¯l M5~cú¶;©øÂù|3væÎO¾ÿÜQ›­%˜íÁã»÷=qi[Û¥OìÛÿÄ¥­­—>üðk3p5âî„þä—ª§Tžzo³_ü6Uîmrã¹ûP*:|[{Ü?Ö×t²h€MUs‡Ä{Å’»ÐïS,¹ñž‡ð«Å±“†h­PMËâŽiЪSÓanD$1Òß(!ÂÛ‰Ýf5Çÿ3¸Š­f¯9l÷Ùñ/3Ò¬ÆèQŸ òà?úƒé’+®¸‚ï§¿e¿ÔøÒ“?§Ÿ„Eï÷ô%ôŸM.E‡N"YŒ,w†â^@ÐÛbä @}²½Ñ•ù² ƒŒõ£<µ¨Èaž%–úP\ ÆXTg†IXò-^³˜2= bŒDÛM³’'-böÒSþ¬Êšozw¶ùq./ù:¶4S™û™±¾¦û•ÅÚØ÷~Î XÇ:ö®,ã¹,v?Ñ{BHã߇ýUCžTvˆÎ‡ÏÜK³?x.¾Ó<,±ˆÆJ¢ÈÜÙÎK¸VÍÙhK—‹ïµˆD /~ÇlQ5α²o¢Ž’bJ*CÅ5%5¸uß8Iù¢Û†G…¿š‹žû†Yx&k&w4Ôm»ÿ@K×'Ní¹àä]Õ›o\ã«ñºÍv£ö_ìÙsàÐÞ½I;eðê§÷úú5ÝÝ×|ýÐy_þÄêt{KÛ'tË}÷ÜsþUüd÷ÅÞåž™¹ è@‰²ôñË5ó¬÷o²ÑÍSÌÉÛ K¹-hÞ§úúľ̵ϼŠ{mï«°=¾LòNw -`Ü8ÃQÅüžÏdüpgì‹\ÞÌw±NçwÑ7væÃÓmjmE±ŸA:à”B ^Í©úÃØ0½÷1ìé2év:ö÷Þ\ÐÝÌ)|÷Á˜]øì´±F: Åz˜Îì£_‘µO¨Ø¢ˆû¹qµ-ChæÍÔÝÌêÁ,²dºÌApíñœÐV”¼^¿”Ëúm‰QþÿïÁ~[°ßˆÚ/ýè»äSQ«Š\n'ˆ"¢l[„Êê8‚ íN ––G”ñ^íñ4Îf¼Ÿ/N)‰¾FVž Ï•\z,š™™ÁѶh¨ÌçÍ(Î,v:¬f­L]œK§¬ÇñØ·y~æÅgyæX‘§9݇^¯ «~‰yF ~ÃÌÁzÎue‰ƒ¹™¯Í¼/¬…yW‘Nò‘2¥4#Õñuµœ^— ìÆ®H~"ÂUUa˜ÍסղŒŽ¢diIM§cçl6½^ÅüÚv!®«ù˜]H۪ϭŽ(•Š„ÕCæ·¦š’¦†êÎL U^\„×äèLÄÜ0ªh•)É £˜ÚÙé8›Í9…1}¤… ´:l·æI²9‡Æo}é÷2r­Fùñ`çdmZýñ±Õ×oªjÜsçÆ²Þæ°K/渾äÈuš5_,蜬ÑǾÝXXWY9R—›YÑ+¼©±dÛê7¶å«±·ãò“û޼ró€Å[îŽfÙ…KµælkÝd»×ó±ÞºšØŠò€øòõ+÷÷ä1úzÍÌû¢Ö®Öî‘hšê9X$^ D‡7› ÔDãdÌL€Š,žjìb[¯W1ÑÈäjˆ³]ÕçPÇÀ°k*Ø‹>™yUa„t¸³ª“ÁýGr³2vKºF"•´ íE—̸5©bGR=¸áÙ¥0ûñ»¹ŒÆùþ°Ó+:>»cèêÉêÆ=wO®Ü_êÌu©°5Ƹم0ÆB¾†ÊR{`,\1  u:cí£W~ýèÞ¯ß8TQü[9Û¦7¾g~üµ´ìÂ,útÈ\yɪՇz½Hǘ]A1Ð$Økøøàð¿éò_äîœþ5Éj¤úá¢ç 3’Q®Ý üÀ0ŸWôíšyhÌ!¤1»‘Æ\Ël1žîâëQÆÔ5Ø\6£cK¤³¤osUvS]¥9÷¢±úÉacÆ`é¬ç0%X÷X÷6ògeÒ2¨ÈWWrœ¥ÂNä'4qz„ˆ Ê"†vG/biG\r&IBT «¤…­`ÖÄ—yùz®”zÑHJmbq¨ ‹¯©Ä’on •ú¼9Ù Wá‚¶Ñ6ݼµ“£Š/’,Æ"†9w$дêöýmõûÙ3uoK»Þa³z #EU•»6TÔxîô5—¶.´hWóè_»a°·sÜdvd9JF.ê½ûXDçÆÖ®´äuÚóÝØ)ÀâŸ1~á»L¶êžé`Uèî9‘MÓt·ˆ)¸Ï•v&·”D;QÁ½hZ"k´Š2˜W.Žæ7 ßá¡H5îT¿/σÄ×€{u5]=—ø–ñŠÑËlü9¸ñ•ä9¹<Õ­»1¾u3›.Ù¿Í?¢óX2"Ý“mwåÖ – wîÚ¾qcÍHs¹ÕîÖwvýðÈŽ‡4÷uVf‡Æ¯]ªJÝÉe&šj/¼:Ý, ž·¿¬§2‹þûÄšþñ_ŽÍ~e×`Íæë†·Þ_éjZ±64zÛ¡áô̾’d\{RŠ-¤‹|U¥Á.`ßêj8"vPY¡ÁÉO`ö!¼€nCS,Ù;†]ÅP¹;â@¶µ3AF^—a —GÞE@«ÉD=Xœx-æüÜ­ªÄ(qî,‡ V¥‹viSVeAú©¨¢Øzͧ¡?Wœ}Ù+Ÿ\±öÞ_]}í;×iv{°nM{ïá5åUc‡; Ûê*3tv½=mgóã‡JËS ŸYnÒî~êý›oÿèä¶ÉÁKôOÀÓtèKç_ðåCMZg~–FŸ¾ívSfOÑÜxàåðÜúA^®BáiÎÙ³äXTW 4,@ŽÜ\4¸ÔJè„5…”<ÆúQ õíqã}“>NyÓã ¾€Ï‡YIq|_±2>ÚýžPÆGv(ãË œPÌ|âãã9™ã¡?Œ*…ûNf:¤ñx/Îd—‚¬SÞ%.4¾¢™¿0œ…ñ!üd~wO?M†£ƒ6L@  ƒ Ðà˜$˜íiÙÐÖº³£Ö¢¥©:\ÈÉÎp¢ÔÚ¸ßÈ €I»YÉŸ”>çøU°ÉaŸ Î.ÅÁûœëd‘èó*ú8=)^ÅÎøÎrÔêè×YŽZ°½‰ X<%¸n<Œ)ÏAI@›Ö-a•ÎúØÀÝÎä»tøŽ1Èug¿ññb,@;G躎ûk¿ó™‹¡éfP‡¦E<Œ;x€ÆÑ—@€%XÛAr­Q]U!„4¦…b¨‘¨F$óÊ9«Àr€:T&[465V‘+mÇ(‹MÊšmç1 `lM´¥[³@F—‚3É4âÊ©{ÅEÈöØçÃw´n8oè–ö&)ÜóÆ=:ãB"OeƺZ"±"媫èRºyla®]¤ªÏ¼âàÅM_¨Œj‰öñÖKe‹NbþA¤Ë=Àø_ÖCþ¿b޶PEîÆCE¼Ld>@ 줎o̲d­  žÂÊ~~ÂŽ=Yè#»+ãqÑ?¡*X ÑcÅÖì¤Òјštv´gÁ"J%âÑ;iï¡=º+áª{spî @¸š¤Ÿ/’"Ï76‡ïÈn˜î<ïú#CûúêÕ†Êhcýµ1ú;%èMRKóØáÙv1s𽋓ñÄ«âÁ{Þ-ñ{Ö¿*üR8#…Èm0âýTÕ'i$F-§Ó²1€Tœ!5˜ÖCˆI•¾ °‡ÒÓæµgÕ-%Yq@£v7ƒ* šÝ>݉£7†éÚÒ ½ý;¢NhäÕûF†zº:sÙV<ת¬`k4E§Âî5²Ï°Ló¤mšÚkÆ>ÅG§(»øNÒïW,UÕ–‰×·#ê®ák¶¼ãúú¦0þ¡¡óõÑúþä7¿©ºìµ;Ú= …´`è-]Ý·¼ó!‹íﮫ}E•sù¯?ð™­áï@þ/ þÖ!ã@p.æd…&R3Ì“•1¨G’™Û„uBñúßbUm/Ð5ê¼èƒDÇà?Y=ÅSIò'}–öSÒUiö²¯˜J ™u,=’ë*ßWh©ªÈÉúñƒãí;³o¢\ÞR»7Ôk«+¾Ø:{x¢}gë«óoîßT?+”X}κ"ü\LиĿšk…?‚¶ô|¡“¶ qôUæÑKù!¿€e°ág ¥gàŃ;4ã¹F/IGÆåã6ã )ÀóIÓ=EÄ+CÎ6ì¶c/(?ØTgÔ²’Ì{ðD ü1éhý8o„w5­UØ"[>Àu…a‘v2à:æÓ}’a4úð” Ên8¤€€ç;»Xö›=,|Zd‹)Þ$¥ZÓñVÙ<{œ)t Ç»—h·3h)£Š'J¥”'÷ŽÆºÒ,mŽ!“p-Ç îd½þȈ$¨,vP Ô=‘D³(p3€€ð€ÖoD™eH!·Xój½ÝÊxímGˆ´ÇÚå%²4_C½@G‡º;³™ú±†±pP–hPgûƒ-ù÷p>-8²]ldWŸnŠZÉ +XrHÑÌÈcY°ÊZ ³8‰—ñÔ‘{XêHž2É|u^Ú¯|TAøØL›s™©ÁŠmË¿è=ÄcÛÈ9||†–®Á<Ÿ,"OŽê0Å.Å |ä·GdéîðL ãCŸ¶¾¤ ¾]>d½gÎØ?cVQéþŽ ºV‚ƒül¶µ#ÛÚÎóX€£h‘ÞûÜ9fDÚ’ª(’s£¢*Y™Í8e Ì„EExÉ'w¤g×Æé†šÖ»„ø×4Ô4u²¿4Ö4w=ºóŸž,œBztŸbÙ~úìŒc=ñ2$d/`LXi=ç ÷GX‡ÇÌÆ P<Ÿ°`^ÄyW¦PX¾WzTØ«\aÝS‘ú»Éƒÿ’_3IJŃ¥b fˆõ’ü”uá÷ÒÃ/<î“¶™ò5³¿Jzæ N ddOLY ü‰Ï˜C‚µ£ÄxâWçFÒüΛDzˆ¿,|cùÛžÂi–³sÞÚã䌷+ªˆøÿ.üýŸã¿ù÷Šqmä=¿û(yß½ÃÅ‹Ô8µ¶î8OI¹Ëþ§ÜJ–¶÷m:ïšæü\þ™°­pùçè   JÈïþ'ÞâÎ>óö¤¸.¸dùŸ…¹ÂcØþ’Ç }Úÿ9‘OjfþÓÁBEM¿bäÛðãÏI¿Ñã°ýD¯HÉæü°|7s¡¦4X“ 8çöiâÙ<°A H¬"0.¦YPe¥I×a×¶_þ9ݾôk ¶Ú唤›0£c¼2†…BA= ,Í1wzÒ¼uÃ45Öÿ3®Û¥7Gšê6lí ªP‹•m–²5º°%33?Ú¶©¥ZþQ(­­Ö¯—ú—¿$Œ=ûMafù/Ç«v ¤‡Ûº¾%aoþ_„‡Ä_“h¬ëŒ`ÆIªM«E¨[M”½,Æ"ÞìÅ9Ë¿“­£¢ZLm+ÉKÚ²Ìñæ,ºY¤f¤cµÕLK©!ð7µ©æÆT¦6‹eâi•Çdúý‡ wÔ²àòrè¢ä 7•JeRé.Ö|ÉáÞâ{œüyá$Ž7Ý(H f‰")K2-<ÖÙÁ·8Æ{µî îc#¯y¤’Æ‹{ù®Ia¸U®½µ¥±!5V3Æð„šVÀsšÜ xÀÏâxTÌÓb^™—‚ïèèèf µŒ“&]˜’Ó³×èâ0‘ÛXv??Z¤<RoûÑï!òCX»c]qŒ2Ö±ƒ*R:´·1tÕðEÔ4ø{üÏÓ:ï¸1bWU$uÁ¾<õAÐÚV¤„Wùœîî òÅS,ߌ†‰¦Kº†²&zêz¥ÍËâ'mlæ…Ô´Ñô%¢IÚ’·{™ŽFŸ_Ÿ&Ÿß0=eO2d½abóäqÖœ!BZ´I7¿õÅî¼nï{‘‡FÿZ8X¸“\t ]l¤Q3‹ÒvBÂ+6vg–(Ϊ¡˜ÔoF¸1Õ˜³ø…Ž=k±¯9vžSp¯+“ ¼FÚhÒÍç÷¸iA¿C¯>ü nÏDRà´«#x<ˆµ¾PùóÜBh%²kyÓ²º~3©ã ³¼-Vµ^©±ÑeÇÕ ›à¢¨eZãy@¦-ÕYT æü¤€’ôÐë{÷‘mÆæþL³XT•³…5 %ðÅ•ºÖÌ0GD]]]£]#±n®;u¦; ßcrWm¿ÿáxžÜxÙjlêmÎM®£c(7ËÑØë¥¼Æ3Ÿ¿=N3F´ƒêêh“ ™+ÅßߌäNÑAòtíBD¬;£´³ëßÊ9Ÿ…uu–Ž­½w’¥ÜêŠĸEr¬ âØ\¥»Jô ª—a†y#6kLO g,ºu‡òU Ÿš¼—NÁ9C†ùZq¾ž&a%¨„ƒ‹¶á…ìg…8W²+KëïJÄØ“PˆOW ХÁÔ¾ð±ÀrZÃÀ-|O 4†^¢,K͆šÔöm[7OŽûìëQsO{)t“r»ÞFŽ“sý–5¢‚ù¤â&¦É²HlMqlf^*d[S©þÞîÎ\ëHv¤h]…›»Êâüûéãx^oãS)%M’}qâRºqÖ:p:,Åp“‹'ï÷àý‡Éãt‡©…;iP§rÐÖÂæ!—›$¨e}‘èJP¿(a·q‚ÌÁü _ywwWŽEÿAö€:”ƒ!yà ˜cåÀ( 2ÊÐH0¤áÛûpyc ù´®&µqvfzl´·;ÛÚÒ \š«á»oÄMë¯yhý4Ðú­¶…„Žð4C ʨ€¢¬¡"‹†R˜¤‚úPdÊM ­Ì&Qœ}Áã¨}¡ã0&ìÞð+,‘ZX Œ{MæLWgMjóFcfjbt¸¿¯s[×6ÏâÄÌ7&L‡¸ín’áv·©KvÛÝÖw°rŒ(°\êšÔºÉÁ~-îòGnö؃'ÈǹŸÖžßVñÕ:Ó)Û*òÙzÏ\˜ŸÃ}ò8YoL™šQ#º¬éó+û=s³ Û÷ :}å•<зî7ñüš|Ðæln«)„'°¶Ã’}m5ܺÚJÚ¨ª² |/¬ (Zi±„Ãy+G[€þs†a¯6¸n{\0Á(ñòÊ„i©Iu´µteºëSéš4'’s=þœôiárÀ1kÌ9Š™¥vm¸r.ã]§KWÄqÂÃGl>è ÄqîD,|¥p­–î ¤äȰ1Ë4& ^þÍòâ‰2>¶’Ä"ÓCsfˆÚ¶X¬à$»4ž›4Å£¬¶ÛX†¦«ãX$ŽÆÛùEN|¨]¼žîþr¦S?Ãg©:Ö‰äÎk¢OÓÐÞ¿âeäž{üûz¨6ª ¶>1-~à¹ËÓ5TúE2R§Nåšß,õÿM`‘¹çæ¦ýšò|Áá«-ºõ­´þãOz0{ü +’7K) )G\ôÁc%ŒÐHx”ZoX\¥5ò¦ÙPÆDHeySOµõ8e¸,ŽØ²%­Ïªðfj/(:£Â¥3<ö›QØRxÜqfÛúRÙ¾\ÄOŠø7v¾tø»JÎŒÿ3Ýoä¯)9 ¥=ôBpm_¤ÙTðŠ„Î­gêmV $SÒPÂû­CnâéÙŒí…ÙãpÙž¤mOk44^jƒŽY6¨¦êª¦¯h]bAö޳F·„iÜ66Ͼ?]áí_Öhuõ6†Ví¨]ÕyÕQtÃÌäxÂeÖ1Û·H;¯í 2Ûצâìš,¼õ˜àOÇ´qe,½p‰¥‡Nc6Ëö —µý<Ý;Y‚Gˬ³Á˜Ö`Øc ê$¤‡y¨¼½mã0ì1K»³ŒÛÃÃ[7o˜Y7å²¥#–-]†æ`Kß‚-ýŠ¿0Š·;(Ž{-ÒÛ¯E=ñ±Gmz¿®ÔFßnÚè$©ˆD+Šû_lM¦ö‰D*ö“ŠŠš ËjßaYÞ/H\»mkYÖ¨w46?XR©(Y”LMê‚…sì=§¬»PiŸ×2™Xòèð‡o*õÆ-¿ÁO=G §˜IeùÏ—/<©”•ÕÕøðê=ýõxumjfz|th°ÔGÝ’*ÒÑë³0}¾ß¡Ïß½&k|ÆW»„Vy¤Ä*÷Q/㘰m§GÊÚéžþ-ìŠsõþ>š…Ûkëló½Ä4û#³ßO;M{»³ îD<^€¯zš7šÐؼqfzrÂÇƒŠ¢O_.³vïáûÂé?¬Üÿ+çñæÌóŽ÷˜ ÷çìÕ»Êå×í0ýºµî•ï̳ A°ÓrÖ^$˜Íl\ÌPe9GнÇly^Àü7™vô.çí;g×Î|̸ëüþzO,Ìù(øXNŸH’—ØAùØ·¾žölSƒ@ÉâÞ ?!Dz†Ç2Þ’xŒ¡pøq»ŒíI–ØŠ¼"Ú«8¬9 ÊóV­¥O>‰å×ÂÒÆÚÚÚlm¦©!–±òÃZ>Ç×åÁ÷ pæ°Ù˜ãøð.‹húüÚ5Ô×ÕÚˆ‚Îyqÿ‘¥0À'“Ƙ Mpm˜VŒà€þæ&¾È›6&ÃTÄ•€‚ 3ÅöŠÃxT|˜¦«ª(íìhm©JW5§’4Aã-jiŸ¼$p‰XøÀÝp«I3ÀÕ QA RM h ².ˆ TQ˜wÇ|Û¶ù‡{=Ú<º¥-7Û[cÑLþ,§ð‘læO ÓjŒÌ2ig§JÑ|i¨iˆ‰£=tÔíõÉßZ\—@Ï²ÃØj­Ò ìbA5òʸ@bßÿÇ5Ë‘Œþ y0à””ƒþSI$ééîêÄ|‰æDSu@ªLë]CÞcöú´[ë†Áÿoð.ÖOìú{¾d—a4´ÔïY*aCþkNá’ý¾×Ì·ËrÝpãÈusÍ‚Žá‘8V®YYÀ¼3¢$‰ÕàAQú'»éëhßdßøð ;á ¦ˆ-â½Ùƒ÷8ù"à=ß84>"°D†\œ%ª¬.)Tä%,ÝÙƒp)@¤cåRí¬Ÿžè;Ü'îóŒƒ<ÒBmÎ[³æ­úÏû@1ÉÏÐ@)fï ÞGèm€÷c—+¯›£jÍdE8Ë´BÞ*JÂ[´ qyãØ¸•]ëKÖ}<×L_yîwߊY¸Ò /g÷࿼èá‚`dúõw’—’Î;‡K>US>£À3_ã'01ÌŸ69Ôô&3§šç;žU­“y®ª&hª€Q·h‰°Rº9†¥ƒDYsr5{âÂ2¥±#Úé2=uÊËÒú@°2­a=g£qëVnÝ¿uï®sŠ×$‘n¶„¬w}W‚¢}ŠË¦c[ '.»­¨,« /Sð —Àz ,ùæÏîL'0y¶ÂêÜqx'Ü9»Aæ¶ý:ð†à`Õw?@, 7‹¼2ÿŽ’Üß°Ï„Aα‹þú$ýv–w¬gée’~ÖÆ†öà{’á ‘Ò€8:VJ,¾×“Xü`1¤Œýð0Ͷ‰f…-&ðÔá;qS¾ž>}Ó/î<œÿÍðÑwŸ÷ÔSç½ûèðòÜ[„¿ÿ7÷],õ_|߯oZºãª™ç®XÕË;nc4º¥4?:¹—Üþl/13¬Ú=l‘‚Ž™ÏT0­ *ÖÙj. ÍêÇJàv~†eÞœÌÞ©Š€Àðq9ì~`]]G]G"–Bpã‡W,Âa½ŠÁza½[B™}9÷[ÓŽ¡—ŒáPQ\£Ë¯ ±›ð’c’}o«âcÀÈÀ´¿1T4˃†>"Èí¼U3ƒ­c26¦c±l¬#]¡VwÅ‹†ÚtYsåª2lõºÞK¿yÙOÌÏâÌ ùþ÷ŠŸï?ÿû¾ôÍo~iß;Ïï~"lÇ%ÃÅömÁ•Ë_{sþm°xŒÆÖ˜€.]ÀÙàɵ-Æïc^Œ{×U¯GÚ™ìÞ}ð~²ÁX¯™÷M’²„0°† öšÉ»E1°—g¼ˆ†1YD8MÑ ¡_iúXWüþU|Ð…äþ‚‰l¯%ÂÂøVÄ áT8YYáÂ@,·qýsUqÇ·ßÏ< ØeTj[#^Dš¥àÿÔÀ„hÊâ886³Õ¬†µZf.Eûlu±h$ìG«Û¸?êÀqúÀI¬“ ”BW—¥lt×KÇ[êâ1|Ðô§9ü.üžDOÍ`IVæW^J+œ×´¹Ñ³Æ‘kaÁwÀ‘]Ô Å(íU†À)п:^nŽ4ÛˆBîµ>áÁõm\8 ³Žøj³pÑGsÒ‡ËÓUH|VÞkt¹`É¥°l8Šy¾4Âà\dûä|Œÿš_W¸“¼ŽŸÐä|¨YI’Nø˜,D†Ÿèªk3oT1)‰‡ã_ÞzásûÑ/© äo-ÒÁ‚OÏüF!댉•ít:Wâþv«È›ô;´§p’ü-ç×ÃT•ë`ìØü  Ök^þé®8;b[yžæ#>ëÓZ»aá^Ó /ÂZAÇc üª¶j?^³ëj1o´‹´å×î]'1j¬\@ª-„1ÊO•¼ØB~šÜî´¯AtGºmŒ›W§ù[2ÇZ~ïï#ûs²©*?˜¸ª¶TNý—̱¹™Òæ®æÎ¦Æ†:Ç&`ßÙ¼ çèxWÆq>ùÓûÈ>cO&!œ Js;hl~êh쨯«M¹QšsláïËsü›3Çé^#Ø@ƒZWBÐU«¶Ó hY|ƒµHt9¨_ˆØƒ˜VÙÄï|+‚C —a(ÃF>ö|ààC‰Í$Tƒ!u‘¨îXpd5hÆÆUYQœt—²ï¼QGéØÈ`g.Ýä!wÄ¡ø[4½Ÿ:s/½øôøˆŠX!û[HD ©‘Ðbã爓Ÿ5MÙßñH(dý*êCª»6Áù𪼿‚½œZÀÚs|À ÖÐP~÷áA7è/)!7á£~„Ï 8ãO_O¡Žq~ õs´hÃEmØ™¸Ÿl7¶„Ô ˆû¬„A+ÊkªçÒPWÝœjŽÇÂÉHÒÆ.oIŸªÁFÙilÃ]Œ©ë u‘²“¨ºÊÇ$¢@¯;½²ä“kÌщQi ‹J ˆ{yÐx@OŸÅ+õZs® ©˜+ƒeEáÿ*iw&ö“V²ŒüGÄÎåÑ;ó‡`ù/ûçËøVþ¶/åoý–+YËѲX¸6p—t©&uøn%Àô·³$ .Ó0=Îkž—$Ræ³Ã_qO;™J¥êRu•ÙÊ 0g…^Ý…©…•*-;ØŠ¥ùV<þbñÙã0ÄçžÉ°S«mçÇUËs…ü"ùÒ3W~k ×L¯LŒàiÔòyô¢<úß—3}óažSó É»É.òÓ6?ÇØE€IdµÑ$U;ßâó1¥/×D×ÔÖx̬ÃVùwå&'Þ{ÈoÉ ¼€Pûàµõ…îç¨!^³fãÍ/‘oÙ1ÙÒœr ²$Ò6¡­‚ŸÉçÃ|ŠE·Ÿ#­Ï_Sx‚|áìµó¬«Vöˆ½ fNŠÄœ¿nîaýبtqÍýÀ‚¶ºXwU+zû˜Ô‚Òlk}m¢Ò}°óÛë“öü2$HÍè¿å'ø«‚üëáÏ„=S^ÅÙ9TÚc 2Ô!]PUû{ׄ›pÌ{ðVÂ>á~ЇN ©ªS]¥‹ÅîðQÑçWé'ËfG‹ZöÔVëÉL©\ ô¾\_w§‡~a“~-eéw?£ß†ü÷þ@¿\>Œö¥—¯%|Í0ùú¢K¾ú?øÞ¶|éVñ,Å«ù,ƒ¸¸öŽèmš}T ²«ÆÍ¨¹1Un‹¶•øù Æ'K%|2³ü%Sή„?ëí§Øˆí‹àˆ1S‡‹Qº (ö÷®‰ãyqˆ –ZÅ–à5¯Î)­<&ïèŸÕÕYeÜêIÔI6 »®‰WšÚÛ‘„í=í=XÄCȈÉ/©²t¼Ã”·oýŽ»;ËѱÐÊänÁ#wF¾·ð8žßµþ_8¿Ëßí;¦ ùžÿ°1yNÞm?çz¯6´å¤yê++KÌÅ‘Öäâ´¶  wD:\F£i#¿™Ç¦09ìˆMfU~/¢ìÅŒu!3<¥V3ª6¢}8Zx&ð÷Gf„Ç2Ð?³žd5³ÉQ10@‰°o(f'Âq™°ïê0‰[K<ÓÑÝ‘±sq\7zp=Dw®½Æn'qÖ:-ÝJU–QöE54¸ÁX¿n|lpÓÐ&Ž3ØìÂyƒóúo€Ó0¦Ý8q÷¢ŠÌ^m¿7Íßx„ߟ;à‘^°ÞbDÜðwÜ×Ë —?‡ïŠO‹²VSø£sùŽhÞ¨4ÖnÚ`Rœ¿Íò_Û4sc°½{i­››âÒúÓ»3]i/­çAž{èsVg"Z.¾µÀXÖQ¤¼uäéßÉž€Zv Ç´—"n{‰I8>äÑ‘Ó%Ýmó)â6Ÿüú³L>¹ÜôÔèð@¿Ç¾ŒZöeYјٗ ñÚiì°W-ß4.»'1mð]þÂV˜Á<.»u‡i·’Šh,Z+î•k³>çH4ÛOb±š˜eÉî´ŒÑ &a»˜å ]D Õ9k`@ÔX4Vº6­‘ð¾svlÛ¼©¼ ·mh¦«—<ºzÆ¥«›\ºz²¥}UmÈc#{tmŠY –Éü| péñBÀ{€2Æ·[]¬Úµœ¾NEÃÃ=]í¥VÚðL§”±á™Þ~ŸC§¼Ùe«þ:%â°Y£%6«R™°àDl+6ZÞŠõhaw «ðÕ'ܦ™¶Û"Ó*Žº­b¥´ÞÛŸü›FrÔm$—ÑJóF3ËSÃ}½~.Ežãå‡Ë®Õc¦þÿùÖê?|­¼î _«ÕöæÛØûÈ•\Eí4}œ5+û¸ÇQñh{ÜøwY~Ë‹´™ítkŽ—uŠÜ{ÈÖç­Ü&Ò®Öî[6ÍnXÉáJðúG…«ÀÏø¬#çAŒ×k*œ[¸Ÿd¬ê,,‘ŠKü-ãHá T¡TÄbR½ygõ2B„;9œ—YprÏ]÷c5–µ[ÂÈ&'I’*¤¸A¯ÕZøÀx¯ÅäËñ¶ÂIÒ»cPØão!°$R°žÏKìαðy€óF~î€óƒ3b *X4 J *d 3 ,8ÉRº©®6™ˆEƒºÔ*Ãð4sŽ…Ó÷µ%pOθÍFƒ„oÓY=^¸v ÎïN~ý§ùxÿ @³ÅL ŽYñ2oÐ÷ëÐ÷|~IŠô}è÷“£1}2 îÙ$âl&)9‹)8¼ú~F>ADRÍðšU„£Î²úw9Œ“4K&Ž˜åé-ùßRµº¦s¢¹a4—JåFš':k¤þŸ5ŽáDZƦ±ÎTªsŒóÅ{×-à×b}¿÷°1cÝôJ#ꚥPs4XÛ‹<ôù×ü–ÂdŽ ^’X­'¯¦bKK½ºÉ..0YP ¶p&{ákxVóy¡¸¦ ý?ð7)SöšÓïtž$f¾H‘`¹ã†½H1?Hö‹”:â"H”+và1_ Ôó&tÑ·Ñ‚‚ÄEàLhu «KîFóóŸÍ´¥2Ö˜¯ÉßJÿ‡IŸkг¦gòO¯Òc²ºÎ™5a…Ýbq>höz~Wç Ù‰ÂÐGmF& Bgýõ »¡“Ì8 €Óî…#Â$ϯdfŒòæœLET–ªâÑTE*¨I9‹©õÞ7|äO”>M¯ßèak‰t$^:¢FÃ!ÄGùo ;¬œ,ö{Ö³uT‘\ñØ + Ȧ†š”©¥Z䮥Lž{JùŠ.ÖI@žÃûíÂß’/’#Ü$ÊX…£lF<¬òà`' 4;.F‹ ]Bíééïmç¼Ëî½ ““E½Öçß–Ú±J*µü’zPè ¼G8HA÷c$ŠlŠ›xoƒå£ýajówP·DÍÏŸf^.«ù•oú|Èq†³Ïî3Ïì/„>l°–ÊÆŠÛmbN‘…@Àú@œ7Sõ =¡Å6 \™ëdà2‘q°‚l¶¥r@”ç]toµ[‰¬ÝB1Ñ­snóF$N·§Û³l’ªåªeçù!óìýóÿ9çÙîžçG`žç8ì̘mg*æýÚW ’/žj¥*-F4ë*Qõ#¶™! ÙR«ª,ªÚ1çz¨Éº‘Å5÷3zWê¢Á/šêfU£V–&Æû{;Úꪓ‰ÊhXS¥f¹9 qùærµä‘«&WW»äª€¾D.WSV*âY#od Vi+Ü ÌøU xDˈ(R*Y••FØxíÆ{K%¼7cÆù `´OØ£M±**VaÀ´ ,å*ŠÖâ¼íiPØH€£؈D÷µ2€"𕈤d³1øƒûe­f˜[ .`=žùÙÍɦ&EjÊ6e[š‹3¶lÃåå²s~)o§ÿ3Ϲµ¥tÎù#È‹Ž\|1.yË_5o¯ö£äYAÎ ºZVäBÔ,/äQü¼_y™+íˆÊ÷1c¼=\ܘNƒÝ?žèËux%/lÙÿ}Lö<²gä' £}Úú"اù‡}qlÈO¿`­¶ üÿ„rv¶ôYµ}Gá™À‚ÙöLãBïókB®cT<¥aaíY¾UÁ‡~À?:;?Ï ·œkJÉ~¬Å^eïmÕæa3¾4â¿óà<¼þCã” ‹ŽXÜŠ{œÿl,V•βÄ30ÖH᱊û¼¤gœç¿øéIòóÕQºQÈ8ZöêÈú$دŽêa÷DÓ õ%R¯5žŒógshü‘cP†þ3ŽÊÎã6sû4‚#ÂZâ{-b“X‰e&EòA*‹ïâ2ë±SR4W#¹‚cˆ²,ouø§dc¨ÇŒ3ÀY&bÙk˜X%²ypM¤Eo3"IV+0Uá«FÒˆèñÚ3Ù.&t±0b¬$fw™¦™º‹ÊÇ÷ýÀËzÎí‹õµ‘¶†ŽÉ¶ø'@Èú—¿ùèů>õöMÁðGu¡ãÛ.}”ÓîHþÆÀaN»#¸¦ 3¾¶< ‹O…0{“VÛŒ¹‡@Ü$Êeu¹Ãi¬&V8­£©€a´MÝ=M=±X¶½5­™u­qq °~0ôŸ8r¬ÂFÞ_vöçþ“=œ¸è~¬¨ÜAe! ®ÈXô° (óÉØ¸2N¸zhsy¨À óñ•Buµ±±1רÞjBfq¶Gò·–Ð\ gþQÁ;ÎŒ¼…*O?M¿¶Ü}¹)Œù<)L?JØ|n-™ð žEz&BLR#òPZëüàGñGV±}äÁÂnð£9,ÌÀ³«lÞ ÓQ…•GÓ¢Z`\¿bã’ÀôO£é4;è¥;C—]Yö¼š±ËëpžTù2Å“O²åÇy.æ[ó\,®Û½$óìVäY®= žrà Ž{Œh>ÀœÙ±iÌÜ‹Žž‘NqxÇ<–«+.¾º]ÍÌPÐȘ353åšmÓf}@çä€È³oš&i#ÿ wOó'žƒÏjeÔ;˜äs(lØ™L]­ð8™·åo“º¤á߯ÆÇu¹D|gá÷ø®Düpá·˜Õ)V!°D¡lÒvu|"õ,þ¾øŠÒ•K7)µJmï14+/Ê „ȯ3m‰˜|áõv£µ6öÏŸ‹ûà"N‹òl±ÉÑX—ô¢ÃY7aÀÌSfvâ4hÎ cÔ‚eÕÕ$Ò’/ØŽŽŽ¡ŽƒnÖÃá°O”À~DxÀ0z½ãÄW;D–Ü€;LsnÈßꜳ“ž¡ÛA« ý+ÌÝ%@E¨Œo†ýã+\罡֘ï6føHcŠà!¨g˜iéT ‹·Æ’é”}†`¼Ùóù>‡‰5K\¡<\#³ÅójÌÈ߀=k̘ã´ÓqY:n") n¸5)O‰hDb½Ç3Ø_áòêÀñEúq ðÓ& ®¿kœ`ç0©³µíÕIøˆ™Ï¹Qƒ±T“jkMµ×´ó©±·ùl^Nh8I“Qo"sC`ÝøŒI/{Æz½ŒnÄÚ^7I¾ïtÉK³ ©9Ox`>BOºåŒ6f‘YA1#¨"_ýà¥%èùËÒetøÌΣ䋰€Ÿß ¶ª(À?2ÏIeç0(©I·uTÑ*%ÚE…jÚ6Š1Ã!YËÄ0S$ ÄRñXPx¯üÓ?mÊ%–¯”ÿ‰J<ý°¨U„ŸûóÙ)Qö›U)q~ýÈs?çÛŸÛq>¹—ª°O lvÍTR„Y¢€ ­H‹®™bj1˜%n&‹Ì Ï(—q˜2˜O³½BÊn6=,`Afž‹õIaž sdUtW /ì%ì—DÑÄt±j­_-ž5 ^…y7Óh‚YYt7Â\ f|´Á¶3³smþ6E„½uÒµL'Õ„Øî!Ïwé Ø=¶["JKZ`ä&)¬³½ÈœN1÷d‘ p&H±¢È©®Tl ˆ3ÈìÒ~ØS'¹¾êGŒuü~•ü–ë™$°Ò$*-¹{àžRWn /7Ü5ŒGëJ£É«þ0ï¡çð»Ÿ|­®d¹\n87ÈN땦âoñÀ;N~º 4õ`ŸÀ‰r?²ØXg+Éõ?‡yµæ &Ë6gÖÙ ÈþÝ:s›æfÖOŽ õõæ¶vnE A7¼8N“_´,A™PW¶Y÷›Hþ¿,çg«ËëAJæ­á…¥š÷Ÿyáðû/çFœ‡hߥ-§ÂÀdïÀXscó]×¹kîùÞìý¿É{8lMQÅg<4”Ë ­šÍ æØy8lþ&´õz:ÊÌì÷¥wÞ±ëW¼³­3ïl__¸“ü ?ûka@¡ .±þî€Xg½´ÑaCë(ÚѬfàEI,· õaþ'ëáﳟéÏßZìgÍž!ªßøù<…u$ æÜeùÓ|íîñÀúýð×gÍÛÜKê-1ui}PìÛÜ,Å•¥#xh+KåƼ×mãéâ*­±Œ¯ŒÕb¿¡ý1L¬”kŽE3m©6Sl¬yíçyüóúy{á>|ߺax@Dåayô+«uëÖmY·9ÖÍ ÷ën:zô’ɉw~÷ƒû.¸ë7aŽös>ð·7÷¶4Å’í7ÃGo8“µcÚöón8:¼ü2a÷òýwÇ.¬ª?g¦¡=ôêû~ÝM¿;uÉ%§{Ó»þùþ×Dª&[ë›Áê7bRð™«îXºìÎׯ_ÿú;Åå›VØç6Oñ<É ¹"hu´Gü¹kx¨3716´nx]_On°smôN>ðØ'Ò§…ÏìuƳ™pß…”ß7ÐDp™(¶X'hq[Š<ޱ-fFºùUì³û–‰f¾Eçðg<ð™MiÂ+û%=8oÛ˜¡ÕlÌ;¶ûHaØmwðÈÎ#ô ÿš¥V¦Ó6T^é?ƒcŽÿ×äýÖfo¥ô^ ¸©ÛíFŠ"ï%,¯ ¿¾ZXÜæc Û:vèøï—¡ý=ô’o‘ÆÞ0 êÁ%¢KúR)ùý“flÛÖ™Ûv`Û¾Ý;}¡ûÎR¾"?°}нÆî0Õ¸ÑÁ…{ÎIU}£¥x «2$Ù·÷òË.9vñ‘ »÷5û^cq\´Åµ·}؃ûnòoä¶vf± Hò¢™Áã|•íC¾(»@vFGŠnSvnò็ÞBn°ñ€½ ¥Ò§ìÔr íêi<6¶Ù‹n{]ZŸÿ_…'ȇOu³{P+ç‚,€ív„•cE·WÚJÛ±µ ‹‹«5G4[JxeWžµT[®¸Ý ù?.<Ã,U+Æ¢Ž|Í|¡1ãJ>ÖÙ³ˆñü®ñ(ÌõÐTA–­¥“é±KŸ9gÁ:Ù_—8/ml6{àglžXY™åI @‹«Œ^V/ªÊ’:_†j½v{‹j vPÔr~°bÓ¦M;6íÈštÔMžÎ_VŽŽË¿ç±*…¦?б”Ží:š²´ä‘¥&KŸpÉhh%éf¶²Rz•™ÏcÍI\\µ=:z¼)¸m4 ÈåÅ©¢+·nrx(7Û5Ë;³1óog¼°T 3ù S¦†?;\¼Ð«y†Å×_Wtîý˜!Uì¥P÷lX/ûkW/ö²_c³‚…•±%«åX†rüe¿Ë äptc ëËñCŸÕ“ªqú)ØcE†HÍÍuåæ¶ÍmÛ¼ÑIÒ ·±òGËÑtù—¦|Õþ¦eiºu³M 0Y[ðÈš‘¿¤ð8ž3ÅÖ|ÎÔú’3åê;Æ ùKÿo±ä¬÷Å93(|XšåçЫÞ<‡>ìâkxK­`%;ÀÙ‰j8‚³ü£Ó™k‹zåvi+NÒÕ˜E3°~çèÔÔÔæ©í¹\{G–ñ  ÷:Ba~Wñ:<ÿk„æè?‹|Àˆãb©%GFi Ó_Gi$])׿È08½^‡ôj4c_> ôš6&Uórï°©ØšJkZkZë‹à­xLþ üGh%À5†ð"Œ¢¬—Ž5;šŠ€ÙÛ"¾ pë`‰eòW€ ] ªjVÝ”1d¯¤ @tŽWª"¤©¡†åŸ†n•±XGZ; ¥²X˜ÌHš—Få7ßAï~ÇCožœ~ËCôÉüï>COÞ~û'ßøš×^õIñÇtqÅG/¸øÏ¯Ü}ôš·½ê’79æüÏî9K3´ƒn!o?UO%ÅÚƒsàÏR,b>//ªþt¦ãm17ðjAsÕÁ¢tw¶fjë‹ÔÓë1æÃâ›JÙ”1Þ˜ˆ‹1D…@$ŒqÕ³,¬š`°¤(ºà‚¹Ñ¡_E:&Çxì#ž¬T+횉W¯§¼ÀìP;}sþwœz]¼÷S¯¬mCC=îý‡é×òCR?’ï-ÿkâ­—~* ÒGl:4~É›òCï+ÖŒZõÞ`4Mf}!Y0‘PËxŽ)­M%*iŒÂè*•;/}àrG½°´Éߟ)°J[<ªOsÄv_È›U퇌mm”¶ ´õuå²™†:ZKkz€  ÅZ5€çfœØÂjAôƒ[eO¢Ð"Rp ±Z¬ ¾s!ûMd”Žwv´·¹Q±œð7.wä¡O›÷ëëA^g)òëõl¹©°ÜSÅ`¾»f%ÇôN}Þ¨n‡Ù¶´xfq™Å¹Gì¹ dü~²ÛØÁ‡dUàX # —!ñ@?¢îŸ˜èîlëkïs 7tÙt¾Ñ=éÓB‚×Oµ×S¥ªB1ÊâDŒ#Ãý½Ý]mí|!¾z?B3€o½1µë³|§e™gÕÝ逮5Øõ£JÖS g‚ zð&ôrëèªP²|G¶4³¼à¤‰e§±ÍÄbåù,C)L}`eó¡tr¼¿7çæÏ`Æž]•ÿ›O i%¸ƒD^ÀÖÊs3s×ÀÍ~Òóï.`eþÖ/MÞaF›ÕK‚H+0_F›YŸö;™F`*{Ñô÷#V´õ=‹£¸ª‹¥ ­U­Ì7$i‚Áp­ü ¤»ª` Keebéñéü­ë°ª­"Yù[¥þïýþSÎXfq¬Àùßßëí…k°:Xµè?`îš ÜG€*ðBïŠ >:aù-z¥R©ÚTm6VÙZ‰Áó´*ƒ[¼hV¿1ë_U‰¿hÌ?˜vk_ HÃÛ¬êW¿1pu.?µ|íÎ"×,<&T|ÀÊßÿ©Oyø„‡×¡aÐÁ˜¹ÚÌb^ÊFȼѰ¹ž çÀìå[¿¯²8û†u\s{a×R™ÿƒ¥ Foš—¥Ã x]¤B3ãË+Ù:ÂÿGâxù K–6ë—x€%,¬‡w_™?î#c÷’—?»ïgcfÝmâµè]«è6Œ;U]t 0À~Má—/q›è5»ÍŒüÐôBóa¯R ÙdXဥ#Wø=…ü›l¨h&]×ÖÐV•ˆ†Áv¯jÕÚ•ðßC§\øí02øËýÎñ×þº¶ºÖæÆrøoñà?N¾]8I^~º5ÍÂ}¬ÎÅxŽGq£üïl vuæÚê\£°Çpµg °1\ÅqgíÛÍ }¹ÌqèˆÃÞ¯ÖÐŽ¥±h262Ðß݉uÊê&ë'‹£ ¹hã×iò$Œë°™L¡x€ÇѨ®á”ÿ™¯Œ@g¦G†ð%D»æ I—÷ƒä à>Àq×ZA’ŽE·1—û‘á­hcC½cÎ=8¢:ðâÇ™¶*5Pé7éUÚ°Ô"ýö½õ½Å‘è.Úÿ½g,Ÿ§Ûaþ‹Vœc“Ô) ÖA›ß`Vkd­ÃÄØÐ@ùu¸Ù5üï7Ýo¢QW4q%X©çÅl%4Û›íéì¨k­Ë8å4hÑ|Ý/qX=s ³2'z¹À©´ëJ~N9fáž½âZ‡{Üë@¿COÀ:üÄŒ£¬ƒUÎYq”æ‡be‹7ŽÒ‡H| ™’ Jߦ‰µ4M!ése‚-}VažqÁÈPoG{Sƒ‹ t¸ÙÃ'È7@6^v*Ë HSMÊ^u¬¸øa…ßm5™kr‰'*kk <ÞÇ‹Ëx’Çã3äqò>ˆÖÒ ?u¹z3.­ݼiã\©® e„çÀñ×=ôuà#ßÇÇÕITð'TiÑFx8H±š±Ïèø•ÛZ{°o·ÊIKx `µ÷™HÝÜœ@çvÏíܺ¹t^!Ǽ>홬9Ø÷Ö>pkí;I@R¥€º¨SOUå°Ì"Fø‚.⯵1[‚zž³¯?û½ÜÁ¶+²ê:ÜMv®뢪ª®¸këa­ƒª©KÀ[򻑡 ÖaǶ¹Ý›v¯¸^Ùù\Á¡…eÍÒó×Âåp-Ü–Å=©+—ímëM7ÕµÖ·úò¹w”>-|ÆaÉЧY}~£Y½™s—Ìf|vI¶'|«dOÀº­ßyö<òf3¶‡¼ ü…WÙxÃWie[¦Ü-MÛ £ÆLÛi¼~ÜÇvÊ÷Û½T›ýÒSZ4+›‰,à¥õ¢WYZ–{]Ù&)«I9ãÒ)ß÷ȳ/™~zù¾¡byS">ua; Žã¹Öò캰um›šBâÕ©±¾œuè —•Ê4‡cö ‹÷*^&ƒZ œ÷Õù«6[‹Î‡¶‡¸mLq\¦Í~ ŒgÚ˜Ô°¨î¬Ôªa.*‰ˆÒeÏNùeU¢¿¹»¹±¶&1T5ëNÛwNì‡8ÿØðÁÞéP0«‡^åyUÞf¹ÇˆFˆ2Æ}+ aä¸oGÿŽßÏf«U‘e%°&È¢À_žaœ? ‹{ºÀ,íMź§ÒÖ;H¸÷cwÄ´áP]„+Ë6Ü®®®Ñ®áVÖ:cåpoñÀ=NÈß… ’ÂãËAIY‚í‚HK&|r¬3Í¡òóNójÌÓ &xúÖXU¬ÉÅžmº¡aðgµsx'<ðîåu¸Š4%~Ð8,ż#Ðò·*c|‹°èza•¸,XN΀EhK~<Êaáu9éäïd«#¥e3‹ù 4×ÒÞà1ÿ/¨§ƒ‚cï4Ïb6ò·°v<”! j.8Y„dŸ­sXW»` äô'1Ç ‰‹m)¬¯wá—kÌ‘_¶‹Áºûböê Œ7yYw¼¹Z(ÏBdÞËï™SÉÆúd:•în©”ñœÈÞ}‚¿§´!šé-ˆ€1™L¦“M­ÐŒï6aÞâãñ­'qŒMõ‚(q°øT”¨dÃÅØ“½vþYÙ\ãÕx§÷DŽ´Fˆ/?)fÕ\0aÙé?ã!ÇÊ p¸õFÁÙúˆx.Z±>‘:rÀÕ‚S“ ‹"»ÂàÊjA0v#a–Ù¬3ªúç" Ì›pÙ÷ó`u•Ý-i9Þ%Ƈ“Uðou¼‘V¯§cq–ç$–ŽQñ×í«û«æŽð'«Òá`sâÁÎôÏòåŸË¿þRñßébM²¹6¿¿cs&³¹Þ[•}î›yÌ5(æŸy1izzÌ‘W³‹ëé¿S0R¿”rnM P¬üþãy`‹Â¶àxö`×Àª“ƒýÝ]íÙ–æúºäHõ€·öþ=n>?¶ôà§¹OY‘Vk…)st"x³ÖW²õ•éÓÖc‰8BDtæ<Ã0]ÔÞ\TßF˜( ü@‹ga³cD„ ÃÓ £²1Û+'wq]:=xÑIÜ‹ÒHð`}Þ *ãÔ¶aœðÈô#4Ës¾µ)]{Çšå¿ÍxÉ GšÉ_\x‚¼×Õ,& ÌJÅšâˆ`žEˆ¢u‹ãÌ‘ØÄ[‘ÅrÍ´¼ž`¾\/Ñ¢©d®=ÝœìKõ!éøYo~DîãŽ8Ñ.z©'ú53öö0ò¿öŽÇ”LóÇ"³HjMð‡pålTÙ öÀO؈°3ù’äYPˆÀ÷V¢b¶¦J@RæÝ|ÒfµÃ¤ØrÕ#Æ‚GîYÏÕýý©dÿhÿèЀcú– äÊÎÿžbùÿóò›ÿ}8GîRkþ³ùz3ƒâ' ’o–M]³ü“¢IP;b+ŸS>º-ªa ²[TO.ÅAÞ‘,žEO£…ND‡šôr{Óà ¨¾mƒ[gɉÞîŽvnR'ª`úÜÇóßa²ºà‘UcùG…ÇÉ>c]ÕêÞ´¥xW{ºuÍöò»¶h”Òhmƒ#4’ˆUñ±˜povÁȉ±ûñ¶³Æ{Î`+^Ø”65ÄcQdR„}Â3æG¬œÝŽ*Ë>F Uq¤R¤§[ Žà™Åì«jg³/ÞW0ÆRýÌór¹›|£:NÓP˜ÎFh8^"!ZÒ5UÄç4QEüÈ Ì´ÚºðÂÏ?tß½÷|æ¦÷½÷Ý7¼ëO¯½ð² /[ºô/ÙÅý}½=Ý¹Ž¶ŠhMÈJ#,#Ôù’ÿÅ¿ šIÇÒ“´z09T V\#m¢IüK’²¾³¿ÇŸð#­J(ø0vdxŒ¦GèpÛ4í¥íìŸ>óomðü£%ÓÂ"SÇFÇFg`(²f(ü1:6I¦cU’é©§$ýµsšÅ·j¢„¾*ˆ˜T|Ï/þùÁhWlw@+vǺ¢ Ž6D"õ‘MÓJHNõ&+š¢z\Ó*Õ˜U¢JXÖ*M‹VHýøŒqMV“i«Ï«Ã*4둦X2XªŽå"‡å‘ÑúHƒpÝÑ®Öçö~ëXG³xâ[˗¬Mµ•éŠTW<\Ù‘Ô@uD¤ðÜAAH„¢ER+T˜G«V©×7ëUÂïiPM.ÉA)yiµêðÈÝñu#¼SU®Œ/+ ^cC]Ì%x;kQN,ØôÌwY.F?isßç»åŽïW ^˜Ä0¯yŸï/¶„€"ˆ`ÞÀD,Áîô<ËnöO9YáÏøý¾“Ð,mÎc[á—âuÒµ8m8F£»^IÈ?‘4l½TV@:eŒ‹BCcÈ:“†A+² `P73«ÑÏP÷‡¨ªFXMS²Ÿê‚µ9w³ªµtÕYt(`)Ê*c™=¡ H**4/ˆÒ~X”s|¬»K ÓScƸÑß×5Ú=šÍ4Ö×T'âAv aóìÔ"ù 2ýJŒZ·$X ÜOkZ‡Cs(GæKŒÉµöö£ÇX¨xçâí¿EÆEÆŒ±™u“e(bÓã*_Ü{Œ1*g¨ª;)0“ëjðƒ¤—N Ì(Ý9§§HPpš°CDWñ8×gR e»S­¥W‘"ä&E3bËæMsceY£È¯÷Ðâ䡱RÎH²BŒÞƒÚþ0Å Þ˜Ì[Ý_¬r$ÊgqdÚc¿ú¶à­ ˆ±ÑvÞ³rgÙatŠºéÔÉ…hïžÛ·nžÛ0½nìÀø?ŠÅø)§×eNz‘ =S¥ÇXn›¤ÇÈ™RÊ¡Ï3쥜N1)Y‘Î9X6Ó‹\ƒV_hOñ°î@ #xz‚¯¡SˆQ%ì$¤ÉÂüÞ=[6MOùQ#Âøì_N’JLO‘f˜ùUµ%ª¥M3×TA³„\šß#Ùš¤8ú•:Yj@UíÑ}½” öNôMôtwu¶¶4Ô¥’ñJ]%9š E»Ò-m}4"ZE‘ÆF†Û0uH¿ÆôÑBË׉ˆU‰¤XÕÚL¶mÞqpØXÚÕ9þ§?¾ãœãÛÆûk³ÁÞs¿ù¦C×\>ˆ[Ñ7¾làeWïNÏU5Ϧ[b™¯˜{ÕüT`äÈùµMµuñsÎÙ=qÉ{.¿O¸í¹ÈÀÐÇ5íÃëËê¥Ï½ÐO–r”Æ´´©#@æø9iû޶7È4n O…?êÐTÚB©z]©oòa®}˜9/Y}¡ì—ZÀH©$¬¡“Ÿ$à)@W[·0õ°s|§Ÿ@D]{íW={íÃä$HË¿ûkÔI›Ñu¢Iº¶àЬ,9²šv·!¨ UÐ¥^He)³ní]ËkŠƒû÷ìZISXtáqòE½ t9Mtf‹¦·‘oûÛ"s¦‹¥†–VíáòÆ–çÅgÏ0:ÂI:rè¢Ãç¯4õ˜“'Ä_ó¹ƒDãÜßí?׎òÛ¼Æ'¤Zê.ßTµŸ®G]‹cö%¿ûß–ÿ@qŒ–î¡g#ä“þ#\É )Ú„Î0z·N_ÅÄuvÎÌ™2t¹dï^·ìÑ¿¦˜KþJ~ˆž£ºº ¶C甤Ù:û‡ û‡y®h'‰¶ŸvÄ;ü 37VQD<Å!”Zk߀¦ŽÔ³JÇ|ìÐÕ›ž _½ÈïžÛ7ÎMM”|¾&»dÅÃWßý¡#¾kZð7¶×ñˆµšÜ¾äš°â(Ö`B{É4}}}ÕbÛÖ­lÏØ¿u_y#ÜÜ3 ‡‘o9H‘Fßÿ2£Ñîÿº4ÚfÒhÛþUiÄø¨BÞâá£ü5¡ÿ`To¥áà¹ÓBÈ#Õ<ãã&ÒÕpèˆÆ Ë#4¬à eVÁçu‰ñf¦‡ÔÅ ×¶„õpD_,•„#g×t· T…õÐ1ØñÂ!õ‚¯ºTcß e«_á1¬Œ\}LÝuðÀÎíå9 VôÙ哞õÿá7 4*/ ‘ ?Ùâvã,¯X…ª/€C¥I‹G[Qá¤l%§AÌEÙí¤Bè‘ų[",Ó c­tËS?·S/9öŠ—ÍÚ¿w×NÓ¡=wü\?º&Šû¾Ûç€ÿÞ{ÌÜýi¸ü‰ˆ±VÕáSs{ rT•çGkQ!eÀ¾¸6õ…#û¢ˆ×Ф;ïÜÝ;WQE¶!ÿ±‡ÿöÛDýý´1ÖÐöÔt “šƒ«XÅw*98A¯mÖK¯`:ã°ï¹cYHÉqTµ/Ú¨Œ2¼ŽÛ ¦~¹Á`ÿ=~öj¹ã!TS½Û¶qÁÚ¶oÛÞÝ;7Îm˜¡Ú:¾ÕG*Wæ‘¿ûòJ<ÒÀ5 àÃ/œG\°^¼(£ª}ÑFeòˆ/¤³ãÿÁÀÙ¸‚±qÌËÆÀÇœ‹^tøüûwíX™ãn>öÚfÿø "äüuÝ6PÐ0…pt¡Š†*Ü|XeÅ✕.ÝâPO¬a±JÀ%_ìñÕ¾¸ãCfÚ´8‹|ôª˜±sÅa!sʰÐJÆBñM8jiÂW¼ì¢ Ï?ïàþ=»·-n_\™“’桯-ŠçßÜ}ÚŸ› ëØîlv¥9‡öƒ X²˜h½µ ¬|*½”ä‹4šÚe4È)ël(+|—…“YCwï¦èÁ™Â¼d»`aÿÞ•OÑc+ÜÏÞwŒŸ•ë©®úy#xÃ*?Rî^cð¾—³VW­ü ¼÷jvÜF‡öø×âuVù»Ù­[6o\én–H¦^}µy·¶‹¥´ô~-ÅVÙr{d¢11•ye†×¡Áý˜pÝ¥²›6·M5é…½ »Äæ\9À¬gÑÙâÇݼц—v˜âeûVc}¹Ë»ŠçsyGYVÞ—<Ÿ×v¹w@غüàSÖåýëdv¨¡a0›Lf†²Éü•ÕíC umÉdÛ@]ÃP{õª€ò«o{ö›Å ÀÀlý@¶ª*;P_?„P†ê=Ÿ'&O¼ŠñÄà‰9Gl¢z8B·Nç]«©qŽêBp¡XËéëºódoýÙt¶Žöt½x×Ð27Kɖͳ»æv­_71«ßžˆ…ß&ÄåÀ'È™!ÉgÚ¦š„HRbPNOŠÚA„€"Ùf&sÕÕuª© çLjªÀ?Ç‚T %p #Ð.#æ3Àj‘0[SgÉÙ™Wâ–¤”3ªÝ87»ajrtfl&ÛžmÍf:CÉ®±^Š|2CݼT=†!Šb#­Še,¶kkWX.d'9Ji\ú`6×qÁ‡¯¸¾m³“‘^»ÿ’x_æÜGÛrWÚ,W7T}0>Òp.} 8i÷Ä#­ÿÚ¨FÔ±ÌËfû.Ø7{²Íb£ÛZò_|çÎå÷™Ì¦ÈZßB_L$jÿ |ñ•^|* öÏ­R³øk²žl§x¾áJdžY*‘yF©¦Àûs~©ó/çyó!‡e”Ø¢œêã&ƘI;œS£ùèÿ ”u?\OøAèd˪ß -]ᆞ¦€Êž±¯)ÂAƒ£Ê˜Ù87³ÝØ>9žÎTe3™T{s8Õõÿ³÷&p’VÕ¡ø½ßZ_­]]kW¯UÕÕ[õ¾ïýu÷LÏÆ,M÷06= ƒÀÐô#*¨PPP—èˆÁeÆt2¢(òDÉCý›˜hâóŸD“÷ IÈ?J‰&yA¦æÝsï÷}õmµôàûçå½ø^詪{Î=Û=÷Üíœt¶ §3DKÊŒÒë­Pw†4Í}ÎþÉ*œÎ§rƒ©öP(-Çút8±ÔRجÒÝvÿýÜ¢?Òt},2~øöÙÿRù~èGè'N/CïYI@ã%aJ‰r¥Ü= v+÷,ªr? b!â¾Ë–¶ÍN]>~y©Øÿ¼ƒe½ƒÅä!þ³é~g»ßÉ%?ÀV%þ…iNF-I¸RMú ž¦,¢`ˆ¬Ñ$‚•=Òâ‰ñpIOòbÌKx½‡BÁŰŸƒ¼ ìHÀ<Åq 8÷˜ /ƒ^5ª¤ ß&Ý{i¨iù|E¤ênÒ:ÚDAÜ,²*dtÏäðáÕ¾þðu×\}芕õÕubÅÛÕÙ‰±áÁ¾Þ¶Ö¦†X¤&è÷âmÜ6¶gÂâ8¢³´ˆv ƒèîÕô4?EÓœÐõÄ>â—ÀBÀÐÓœl²øò3² uÞeºè Í \tôªÐPQîÚ:³$“$Õ®CW`tåÚGYÞ¿‹¬¶o››èíÔ¹ÑÚPÀ§ E¼XËŠìÒt}é& n™·¦Óz÷OÌbº8 ¾œfÆÅIN÷r±ƒ<>#úãá ß ÞuÝüòp.Iz¶u^sãæà OL7­{ýô®7.ç'îûËG—/ßÍ<~ßkž~ý-hûDA8óƒ×Þ{ ½4Û³o"¿uõ¦¿7Þ³iá×ÎÝÜØWÞyg]cß÷šþ¹\Íúµƒ;{ã¹7n»õȬÐtôÚF"G–÷ï[Þ¹}«g.tŠ ÝcM³S SW-àGN¾®gu¡£8ÞM÷‰`¼û·2ÎGJ›}H3û`É‘} ÀÔV檅+9ÖÔ6h•†X ÝC¸ãâK‚ŸÅäw€Oì×bòÏ’Ñ÷óç! ¸™,…8æDxžkþ† qeøtÈ\„‚ÔI' åV±)‰›Ô#ù¶× ^>ÑœØUø}DsúÂ÷¦wm{Ûã¯{Ë·Þ»·6ÝרÖÇøüÄhaw_[nùmW¸ugæ•kΰéwÎQ]t£ÜÍD b™èç‘toŽÎ!½vGÚè„^îwwwt·u´eÛh6y¿íleðÇ¡s«}S cÉ£sÈkÊXÏÛmä†)ì»)éèšñ˜ºHÊeŸk{V‚M_¸+«±ÈjmºóøØ*h08„=’Ç~Ò©(yJhƒ£ ºp¸58M‹6Ò†hQ*­E_8obOço›ßM6ú,*NïrÚh¯]ŽZšüRÚÛШz;Ú…¬¶¥‰ÑG$OèC7:éj­¨_JMkUfðeFË„¸‹¿}q‚Bø$é{ݯFjk¼d¤ CU§îVDfFÞFxq² s>„—^Û}ƒ° Ñ L[¬ÆD*œêèv·-×–mmkm÷$ô=:cW.žiyßvg‘|³e.šˆôbˆâì꛼õ³·¬í: {qŸ¯oXôÅ£¾Z¥ð{Ã×Lv¤5»;®¼úš¾…7½ƒîÌâ¦D|Ã{ë>¥>rÃÍ_½_ã¯7f ?5­òXh8ÚnHéŸk uuþ~C†û*YM¼™ãº/¿½hcÿž: X|ñ`ÑãÛw “Øžðy$.1í;–iO Â)Øcã¡rÔQäEtLÏN§o Ñ­gyU+'Á¼ö\ ^ „awD„W:Slñ’¯›Pá!ÅãÒ‡—ö/íÛ³KíëéêÈ´4ÖÇ£"·sÛýlhŽèî{Ì?ÌÜÆ4ÿð#"Öw¨¡9 ¢ˆ‘ oÊCðίÀ+é Š%h”f~wÑîÚJr¡EtìÉEªŽÓ¢Ùº|*ŸˆGŠŒ“\Ò«ÑX8Ehêäðƒ‰´ŸzèØþ{®™¾åcW¸µ§PèÏN öÄÚÖ†–IpÕ¿ý lN$ÖW¶½ýk¿vâkìÈsâË×~5ÔÐYŸìϵ8¹zð¶]éÂaú~Ô¡oÈ;ÿÀË è#jÄ¢qCMëyä! +g‹¼LN •.̳Tó$zÀv¶V¨¸¼E Ð ÖÔªý©ÉñÞn»þ}Lÿ¡ýœp÷çÐó,‚&+Dˆœ Á,JÒ;¾e§ìq¥HQtú=eëô5,XݶötÓŸ™8(Ö½‚ÂRX’7*Ô¡ˆÅÔööŒôÌõÎ¥s¹p¤#×âKèvSréëy蘋é|Æ¥n°Ù’ÃŒ~ÙôMs˜þ'v›2|œfOd~"ö„ÞPÂs4Ú¬Af¶#éc,mû]Ò”bögq°»…x +osØö{„=äosvÂ´ÒøÚ£}M¿QLßÀ‚0¡d6·¢`J¥ZŒ½¼ùº•Éù±v$Ú.’¡Õ­üÃÑê2±¡@®Ø–=}£ŸÌo¦š@€S“ãCƒWËrƒN¿Ãæ{*Ç)*ÇÏ¡÷Ýþè ;ôºnòb(¬ ƒ>á²öº`º’ß=ò&qHžM;h ¸ð28ÐÙAë#LNvç;ú;û›Ä5û½¸k×ßï¹ó~_éÂ;¡p`'Ÿ¦®Ìû–uÞ=2a›ÎÎûá2¼PÞ&&F‡+ñ~»÷sè]„÷?W½XVÌ1çYø*Hq2±N¯ø(9^¤. ˜ €À(›°äv`½=°?¯ÎÍ ”`¿4ÿéücE¶ðä&:d›øß:0ãäFX'²ÛtÚNYþæÕÙ*ø?n柞i¶igš_ŸC?´Œ!» `Wʳš‰Èñ¹ y) ”ýŠå—'Ch]‡/æÜ\Ù³k~nlÄÉÀˆ/)ÿ¨Ä—CèÔI§FÇÄí`ÞCÌ”LûËÄÿBŽizpààs«0t ßÐǨ¯7?Ô=ÔÕÙÑÞÔ†C>µá6Ÿåȉ7/TY„@«+jgI\ý‘ß|½*á 礎…+ÇÆ–gú µ»:¯;~ËÐ _ºgÏÂÉÇÎÑ{I™kn©ÁŸJ ìæ_šyÃ#75æâÊÎÍméúæzX—æj¶½ëÛï¸þü½û°z¦pöÖòû¦³åÆËSè!ò÷³†(49€ÃW› ÀÕ ¸<ä/c,šêA‡W :ëVD¥¡C¯²÷ø%Ã'±~)ÔSkõ²’¥ÀKšÍl_„Æà«NZhm·÷롽ÉN «jIéJ]Nš³ÿoŽ›þ/;´}Ã5Ýsø…_TA®º†p¾XBµ*˜¤¦â2éìœÕ^ñðù»ÊÞJHÐ3GäŶéë/¼ì íµìè!ÅsÔI¨¢h[Öå]ô$Cà´éª0D_j*ò*Å뙚à8æ“–G±¦›Êž]KÛfgF†\MÅùmV;AçÉò¢`±“YûœC³bø©…û(G!Z[2Hé ”˜¹^ *˜i$$2ò®ƒÍŠàð¾žy=»¶o[PéD´opŸS65ºl„ßc9(Lcè‡ÿŠÐ¿¨¾cœuù1À^Å»Y~QÚfR¯•Í8ƒ.³ )íVZèä¼wá²RËš»äâ`/¦¼L&üS*“ÿgÉdQ“ÉâÞ²2¡v2Àòp˜ìäOð·ÕÐö{Œq>E?X„üÛO‘ý¾£?™Æ·7€ýŠ×¿ná.DI –pUÛ—â“7^-²è¯Y1μ üŠ? l”FŠ {Fu8Õ½:ÅçW|LjP>Hýe–ÀÇÒ˜‚_yèàþ}KÛJ)¹Æ2?±7&=ÿøg·©ÞC82›þR‘â¦ö×b™x¨ ïÐ83311483?3?1=1=:28>4ÞÚ–ÍtäÚƒñâ‚xñ¡•¾l¹Q ]( rkämŽhLþæÂnúÄõ=Çà^ÁÓS û£Î±lÛ¼çæ7Â¥.M¤þÜ]…¤È~<ïñ̼eÛ·èM"ö[™Â'½Dÿ“^cgúɆŒÀßzÃïÄ ¿¾sèäCuýñ«épæcÒö¼fÐ-»^Ó¬xV®ôf–ŸŠÌP¼ pºÚn†Ý°Éñᙑ÷=±àVöÄx¸ MOÕªÚ{šîŽõéþÉΎл~½1ù§ê÷ÉøÃ ©Ÿ†¢CÖÃNYûµ­}Ñäë¼DX^³°Xa7Y_28“õèHžÈzb|dft&?œ‚2v—"kjàp6Éoe'òð´•ûÌK‘ÖÆ‡î ut|´ú=IÎs!úiªá•OYö& :Öãp–ñž—ÐÇUºz…YOŸñ‘"È‚"—=KˆG`w麼* ¤¨òQ¥ý9ÚÏ£«_Þƒž°¬§Ñµ¶Ls].†è–%,ÝŠyuWxÒ ¾•Å®“G̱ˆüÓ~ñmÎêûB¬>ç?³9—%[psÃ[<ˆg…&Õ‚uiq˜'a=Ù‚ À’•úa,ñ_5©_ 5ô$iå#Þ¶®’XÔm®zR‘9ZaÉõöì^˜çðú•W¬Ø¿ûê=Woß6¿ka×ôd¾³­µ¥‰Þ–R9µ¦Üù‡–úk‹Õ_oõ "Atµ§©K„6vãåâÖ©¼ÌÊ– ”KêìøèÀeƒ—•D‰ ¢$½[®DA3Ú±Oã“h’Nq´ºŽ€ç‘ wM$(=CÏ÷ƒðœ-ó¤ÿ½ ·aÜßÛÖŠ›qs8–Ë%(r†Q®pŽû ø~2ß–]r … CXöpµˆçëoí›5Ö°W¯+J|Õ:½‚€—Œqˆ]rœÅ­ËÀC„åQÊê{lÔKúr¹0¯ñ£Ùp{/žÁCzŒE¯k¹F<˺]‰ÁoüöŽÆÖû¼ó`z²YâºëÛ¦;cóß—`ø¶ ØÔpÃöƒ÷R”·5ÖÔæwŽÌ<~áAËÍfŸ±Â)!ÈÎIb ›=t-õ,w‡Vãs”|~P%#„鈈ImˆªÈƒ©ŽdÙø'T~dêòI}YîõÒ%úyÀ¤XãĤ,Ìš'«ã£½Ý­܈ÃáH.›ðë÷8c…ç„O;è??ñò°1à}Ù…þÙâædÔÆ?‹D…Q5¶yÆŠiðÁ V'«¸æ&Œ/Û œ À]ËÖlÓTóT‘Ã0pÈ!”pᯕ¬!>¬ú‚”ŒC Õ²%A\£tøõ>ŸñOi”à©z†i@4½Œ+Ìšš@J JZ³™0ü&óC†ÃãîD,*e3R#N‡ÁŽÉÿI‡íÙBÞÓ˜}à£ïOå£ÍÑÁvîà>Pxúé¹ץº§Š %¦ºSš1Ÿ8ý˜à¹?XZ>Qxîã ÷ñû^ù‘ˆm$˜M™}[ü°Ù&è»ÝÓÄþ€Õû4D=|òèŸÖXƒv$sˆƒe>ÈÇëálF¡P­çWy½9ó·&¡º€¨½¤é®êš{)@¼m¼‰˜KS{S[6]4_KÞà·›yùÅ?àî$ëÕš¦§ànÒ§…Úx ƒÌQ8JÀ+2³ BvÈÆöÞ r`u qz¢âU8Å ë%†UJ¶8`õ‡n²ó²£ž»æ»2-VÂüÿs»­¿7Hle‡ºÍd&eTd>¦ÂØê©¼z?Ï ŸrØäy|†Ä¿GÔuS?n~¥Øa€u4{p%¥ÜHˆ:J µìˆù0½;Õí&/âêŠÃß§ ¨èf÷þ*Çý“¶q~òdáÿ°ðأ侮SÒ:óë4fXeó’ð¦þ-äï×XèT“ªã$/À=÷ù>,ªš¹ÕÈoŠÈ±ßêéWóWú›¸ …^y x8Q B [ãßTP_öBÍÐbçéôPˆ:úº`1bVqB” ­}­ÿÓï¬À´P žŒ†ÇC)Ô‹h·p)ýË*ð ²ÈI~QZ7ãÛ²Ž2#@|•ÌÐP¹c=ñžÀãcùX{.N,¨6œÕæïhá9éç=ŸçÆæoÐ7‡*ë9R¥ž·™ù˜Â#šÂkk[x‹j¼Å0™Wjp06T¾ÇÀUB÷µµ‘‰T‰2cGéY‰ØÍ ¶Vÿ²*”`»@µD®¨·„PÝᎠi¨ªæ–šGçÔ$ÆÇÀDöí…#funòÐÔ!‡©Ä³P뎇ôCb'*ZÅÏjö®á˜óãù6Œ { ÿ“ßD²º¦¿ÕÓ¯óWƸ÷a®NÈž5òq€aÍô55Æ¿ƒ„t´,ñ8ÄÚP€W”€bŒû"þW„âl¦ý³¦&¼‚Âá2x2<ÂJ ‚ej«©Ñ¿,‰‡{Œ¸Ì­»ã«ˆ…Œ{wHƒ/Ë Ul!H®BT¥ÎÍÎLýì±Ñ‘a˜7¨R#æ©C_úðlé“Ö¿qŸM²ag$yzgcëýƒGÞEE8MȲ(ޝnßsóR*kŽ丽?\8{¶ðT¡ð¨ðæÐÁûy=o Öz…ÚüŽÑü‰}w\Ñã2=ñmn'[çÊQ6Çë¾ âNá§äïž >Œ:+ó÷¥}X7òK‚ä6˜ <¼ÃoÙ°ÃÅë~ uU:ª >CÏ%Šðú€gÀ\X+j`ÐÎ>¸Ñ™c’¸†ÉÅÉ…¹‡;¨aïpÁü‹y‹²Æ?~Ì3LN1"@ÿdw¦8)Èk“‹b“Æ)¬wbIŒßãößCæßéO·ŸÖִͼa„`àhˆ¸;mIÅtÅ&·ÙëGñ#+ˆÈ¶¥Àî“SI4qzRläAfèÐ¥bËЋ å°™­ •Á¥.8Ћ €E¹ sG²¦Ç%«—ïß»civº\\rJÚfË$î|ù»ýW8–Kª6¨GUŽL¸£UèÀšäJ2wƒe‰„Šã×–£V­ÃØ5þ ™ã¿—>ó<ÿ$þ»óW'ç1[Üç"²ZƒˆK¨7_!Ô« HÎ-rrWƒ;uFW„ ¦Êð “^‚®hV»C@”"|–ègM¢aˆf3œß;†?·köÙGÉŸñÏ~Zc ÝF„á5bÞÝö¯ ÊdæQ–Éä¼<Çq#åO¿ †Ö=Ñꊔ†ƒ\Å>¿Ïëß°tW BÃC„ÃÁþž<ìâ×%ÃÆÿ’y+† ôoؾs³.€c9òËXÇ„«wî,ÌÎ^S¸¡` R¶Õ¼qaè„JåÛî³Ö€çõäñÇOÌxV®Òô"úe 5“•ý—ÇX‚}8,éôie~1Tmð ÇÂPIBË"]Óûàùrî'=fº‰­=®s²¶wk ¨/×ÛCº–„Zò-¹,!·9™§sqÜNa"Xxñ)±-pñkd–úØRcë»z¯yðØÙû¾Wú59ú‡ßáßPøv´µñ+ßôF“QMfû>xbÑÿò›…?åÄ×ßÔñø+?’¥Â‹•¬Q8âoú ŸæŸ`ç1}ào®b9J–zY^‡ŸËžá(ÇɋݜÈëY°'½ä'äS="áx™[G<%^\§Æ…è–?¤«Ñ.ïóÕÔ³ Å1íÛ$h²éÄQ\4A’à܃°Ç… W`¸.´|€Ã7ßÜ8røÐÁëËë°éœË¦êjkDOqSìþ¡»\ž:„ðÕ«Lôüð3A;ŽãÁ"_±hý>8}#®—ÀVEüWAGêÕÓžÒNK"*‡Â8äÔ\‹+ítòªÃ î›oÚ8öÚkßzÕ­î*¯ao;˜Î¶èÎåþ‘xþ/¸i¸õ;Rz=Í,#Ÿ“Á µr\a×ÔjÄ›¯=êÎOÀÌÏ[lü| ý.áç§nü4”äg¹"?LÃPây•5ie»ÈbÌPsIpwIh¢¸úÈU‡K‹•Ï1?w-ö)%üø8âéœÆ«'©äçÀSGE¼åVýœ òýÜÍ7¿±?÷N›œ…Z¦¨PÊæ¡L)Ñršn—²“UC*Ý´›†¡ä Ð0¹#Q'õ;è¦9 ’ͬ²i=LÝ‚–Jµ”tè}.&ŸíÍ¢ËÑð V¼CŽS–òœlö é^¡ÝË^6ÝÈbò€¤Ö5Õxön”Cà KŒu£+_³rdõÈþ½Ûaw©5C¢µÀ£I<€Û\ñDšDb!×OÐ¥X´IŠ»F£ÚµFí½à5Ü+´“x-ÚÄÁ 0ž^:%qT9øýéã=8Z׸¯vâúû÷'{¹áÕ›n½iuxöö³·ÜôðÆÐe‹Ù|¨¥·eçÊäõ|Þ¯»Ð4¼´ÿ`gº3Ö`jöàHâï÷îÚµ÷²]»øJ%¯Ž§¤šÁñÝoZí„dKcÊÇ{“£WîX¸óµSÝ;¯Z¼ª!º4ïëë w¾k}÷›VzðÜ™_.Õg"Š(ˆ±ñ\]WS8’îå­n_Z^^ÚvP¿ö}æûè=“#Ì÷aÑêï0Oéò0>ÆIr=æ%2†8šW„|+Ú¾]cÃÇc8sc‰詽°âɃ¥Ñ§ÙúòÑû¥u,5³¦  xÖàiWˆ¢Ò€d±82Œ1¤ŸŸžVGT<„;:³ímþæ<*òx»ÇsÖäaFrböx|Bi/ýÑöËs|ãPtQðz„ ƒz:hã×ïÎ/ëxD/–(¨XÇPÎDp>›¼Üaɸ®-Ø¿wÑ.²@s™dö°MfÏâ–¯õÕÛÅJÕv1îÛ’eÀ¦²Ó2JÚÅSèCäï_^  e¡ASÑ! ÖÏX…Ôâüª‹Ièq·aI\`Å~tYUBõ4¤^- 쀄^w¶£©:SÖoÓº;<‹hÓ ó ûËéÓù+®°Y@ÈìøŸo§‰  ³—hß]–ìaL*%¬ºWoì±q¥”°åðcãÂKíxòX1f)ÿƒñ@ßœ<*¾ð°ÄÈ6¥£½Ø/“ý)ŒäY‡yš¿¶Áô›Ïö[›ö›äøÍòµ M·Þ£ô6Ù/ù z†Éf‰}ùhl%êR8žg×Õê°nåãnPØçÓPøVzgY‡—¤»>€¶‚Ø]€ŠNˆƒ§i<ùÿǪD…͘h•´ H¨jŽeä@A]b÷®åý»î>¸´mq>ný¶ÖáÎ"»Ïn%²hiÆt[¾coEÂi·œ^£,¥V§~Üwç£7ÌŸXßÝ0åkJ&:†–F箚mnœœÛÙ³þc£Ó·|ìhÏž¹‘d@J'?Ž¿Ô²tâ@7ÜsL‡Bí©ÁÜ…¹Ãù¶µ¡kß¼qÙ[×£™ÞÔÑp$Ù”l»¼o¡¹»1¸ýñÆ'ßµ¯¦±+5˜ˆ¿ò#Ë ÈŸâÓ=í>ýÞ2ñÅc–øb'özÜã í—Rñ™çaá«›æJùáˆOHˆ@b”Òñ…‹žps/VKÅ—/ØW)¾x§Mfgñäoí%ú®qäQHÜX”•Wc°ÅÖ´;|uÁ™_Ûà4…dU*kjã<¯æwÏïZÚæ*4^“ÙDshшÂz‰”ÆF‰]Jä[Ñö­# SÏ2‰©ü[ŒÂ °ª£°á!„Hx®NM Í Ïú€¯œ?šÇa=azÏ0„ÍN€æ„üþ¶»¾òÆjÝ@áOŒ1ÖZüó¯Ü|ø£¯›«bðÿ¢8Ü…ÃgˆíÞI”ñ¼ô—`»w‚í^M÷H¾Ø€Ðvu! ·â0c˜øê-i´ˆÄMšbËdævŽ€q{®¾.™ÀqÏÆ²µJÝ“¢}°øÛèâoÈŸ2©Žé=p pÇoEÂVä² yc½ŽÚÓÄbž; §îÓVúižÓYuʄ٠§qêT':±²ù¾î'ÑýÛ‚:WÄîÁ’ŒE$‰ë¶~ƒö©‰>sÞ&v'ñªÂ#”þ>ô)vv bIhÆœ”Q8Äõ@ÕHöo± ¿TͲŒÚFáM¤¼ÑשôL¬ÎCL¾K!ãNƒ6Ü›rõÆkj£|gº¥±>™¨­ ø}^C}¸ÏÊ'ŠÏQ³Åóál˜îÈá¬l~‡úíùiïtaâÞÃËw¬æ ûöM^ß32=5Õ^˜¾[<}á/†Õñë¸üÂ}Ü›ì¹0ÌS±â÷r1-ÎfÒ~žÃ²Còø^v|¯E“9‰ï*pŒå&¦Wb=žÀ¢^@Ïo,×:ÍÈ^óàVz…8¡/£’°ôú6Ç€‡ªEᡮ߀ñ,똫¥Aa„1‡æ¦&b™¶X8Ü–ëô“€0,'‚8DOê`C êÎà±È(‰á /š€j x–ŸÂxÔ¢ÓsÏß>ÓqÍHmÓå7ܾte´á7F”¾{þùŸëq¾€G‰ºæøû OÆ“šÒÛv¤gñîOMwzê:[nk~û®©>u}#–: ðø…Ÿ$ñi~¸Ð…ÏÖ¦jäÂ8_´‹DìŠ ‡È"£ñ¿`cˆzûkWã]IBŠx|¸"ˆˆ_ÓÏ5©³-ÊìùQ8vJþ§dÁO^]ø ÿÛ ßÕpsõN 9M®á¿Ï.ÐW(õ/À½k"O‚t0I$àÕy…ç¸Ð"›m1¦`Él‹a<354ÐÙÞÜKÆÈ!c/škó50ß%¡þ£¬ß+i”›g=PñŠGçïàbñ-"Ž8Î,ÔŠõ@û•…Sv@³çÜt)|›|¦/v4\’é]S±†Æ”D³\O黸lá9nT—³†;‹Î_zyFçu'HØÎµ±#dtÂêIXÂÂ6IĘI£.IŸïzÅŒ”¡]úëµ» W>ÊûHŸIÔn¢‹<¼®ë¢’‰ZI¿–rZ?Äoò⪞í¸ÏhJlÑ×ôNT„€ ¹41‰$8féŒí•#¶K‹xòk…Óöj†GéÍÄñ®ºº®qv3ñùW~d©[(6'ºëêº'¨¬&º?Ébp]Ö£í"nXHœ‚äø$ìÛ€[®V³ K¼:<ä7Í¿¬©a»OƒRŠZª×bîZÖßEÛ;;aêtE:$½ÙöF‹”z콉¤71(¼½7Êß9Òß•l]fâïÜÅɧƒO0'`íø*ÅÃâÁ6èuUÑÚ18a^:r›¢¥ãÑHX‹ÝB¿¿Kúcy­Lýž¿8M>g>?KŒ€ùùè"X*ˆì_ôXzN†^ª ®AÔ.Öí­‡8òâ÷-£,]q<‘Qªå¢»x›øÚËæå¤€aÌŠQfBâäK>õ\—¬¢wÆPëâ/ÈÚÉZ…G2õ,<Ï-ÃDdQ*B²$ÂpáÃbM®qÐS•“…ÓZˆÞÌ?@®ö‹?"øzº|ê⯑Oßa[¯Ìjd¬½7õ[™`›«õH ˆª×Yk[“x5xR•ð¨õ /b«z‹ÐhATÐZ ¡QÃÚnrÒܘJÖBp“’~ä1Íøµ×ØÔWždƒ§îÒæGÀÉwI0;ýH<­µû:òÏ;ü™ ÑŒ:i8ùűmî+á(þG _±ÍµgѵxœÚÒìãbÇ™}dÍ ‹Ü&ô"®@Pµû-S+x%Fº…Ã6¿%3s,brÞ”:Œâ„¶£„wŒ€âµ<‰m9ÐEÁäC>zÙN†ã¶]?~öláá‡ùç _cä𢶦Æ'™<ï„™øMª1ÎëP"x:_½ŽÄ9÷±ö¯3ä¯E8”*ó¼N-ªŽ³EKTÎ_/|?jŠin`1WâRûµØŽ`ÐÞJÀL2‡tç²õu,Œ“4ÝýUá3ÜåEœž1†sêšWgCA2ˆñdëǪ`­øhvæiÔÈ­êôöä³iÜ€à…y¸–½;ü_Bóc„æÃ¦qtƒ6CQñcª¯‘Œ}Š™ÓFS³Þ@„œ°ÚìÔ« HäE €µ9~&‹DBFk6ˆ1BdJf²#z­^‚›¬d»„L=ª§Ò {bš¡™7©g¹fvŸÿìg3ûÞºžnQ¸Tkn°ÉÿéO3#ü›ÅÛ )Ê}Ñ@ e¨KERy¼p?ÿ»LÎÇA&´.ú&ú ùùÖ'B@Ñ;+ÈÁ ‡øuí1xëÕi1Í@FoG¾†2-‡ìíaùkiÅ7÷¶ôâ&ÜníÈe¼úÞ,!„ÿ]¶¿eÐÄ¡/6Ñý­N,É@ì웈UEÓ»´w…q_O6Mz3÷¢÷!K¶>~ûë2;0¶þÃNVš Í]Í­> §²åð OÒ}.ÖGI™šÊñ ¹¬^¡çUÇ /RÜ(Ž?Ôô”då§Já4Ü¡» ´™VLÇMKa„ˆèšH'ÐuZ¡nnpt,BÓj±Ü„d¡ÐÎ÷bD ¸±=Yx2ÔÞø·? çâÑæÿz2õ¤¿¾n¨pÛüxå‚ÜMNÅ3ÁP{¢ÐÜ_ŸfÄÓÿögÀÿ/œ6ƒè{ æMרAóbÔ:WÒM ‘µ0^×kq„èÕÞU;aô 0ÊÁXs`ô÷ærÉ\:nm¡90Š<9$q9Câø´.ƒ‚]>¿…{* CIýÈ– ƒØÙ&Ø2›'6ÁŠ!.èV;­&‰œeXö$Ó3Ü+È|÷q6È?3ÂÙx8P^í°­|‚ÚdE—=5bмìaû}÷ã·÷û4\ßäÚÍ\I¢ˆ…ºnºP¯ƒKÚQò‘ÐË2÷£­ç"a1*E®0j 4#ý¸ÌÑÁÊsô¾üåƒЧ/`6GsöÛÈœû^Fûm:í0ç~·8ç«™sÿ[á—‘Þ¬Ï5€‡Ä6_¹¸MÃ÷ÿσ°h‡6üšdÂkÞ£¨K„h›[îâ /rB#ÄçÛØ › #…}°04B+*ò­ò ”ŒÞ?ÁèË×j#‘Æi…G¹ƒÅ¹˜É„Ì›×½¼<°…,K‰>7Ä"!„2騩@|EpF`¯Æ«pX‚´'³OÂ5‘ÖØÖ+Ý»"ÿŒ ˆñ´Á—ÌGèvEÖüÂaä;…|䡇 úÐ)~;÷š œßw¾p€ü!ý]KúûZ¯lTГɖP2ªÌÀ1'ýý*3žJDÃѨ!2šÅcCd\ˤC2γp/ÌÕ6ƼïnÞ•/ÔD=÷Ö÷Í·‹:§³cWεâ ç¹ÊÍ©¡ƒd qW€}Ý]ø –Б%²¼±7žIU ',\êâ8©Ðc° ŠU±¡ûŽ›}·Mi€1}BR„‰öuƒ7+U_HæÔM×=ìRd\¢åŸà°É"¯=šH!øBÂúƒŒDñGºlÀ=¸'ËÄÚÚ3¬–*íS|§Þ'™õnÒ²˜| n¶¸ôݪ' g7|,”x JØMi‹ÖÚ:ˆ+¶Wsæç%ÛÁ¥Þ^Œ{ÇzG‡LÜøÓl¿’ñ󰃟g¹7“O¯wá§¥¸)X‚`Ù¸€Vš45‚ñìôð ‰*“qá”Y¯:MøZ‘ו&7JŒg‡ô°©µ%æf%h"ãë®Â9éßMóèÿÕòX%΃TÌ{²0ž¡Œ—ù‰žUmmzÆ  ô-û} /Ë·2Í]”LÑÉ, ¡ ‘„6P¥¹,õBvŒ4žGó³Óã™H:—#<¦ðô›î‹Îòðd þ gÎlW38¦Åߊ‡@w}<™Ãõ {z®>Ù”ÜqÅÕý«ïXëýG¸{¸u¶¯¾pಡ״ñÏÇK ýÙèõ+Kݱ¡kÞ³VX´ÌýMÜ›Ï'z»á¤àèJo×…ÐÃÅ )"| ¡ï«A"p æs²PSÐÓ/8L9‚dA>ÎjkðšU(¯g½avÌ!n³|;¦ˆ ŒvFsb… Vf4Fzn4LšQ{Ó›ÑJÙäç!4ÔÚÉDÂíiŸ]xCÈg— él&Df±ž¦tq|˱™+ ¥ëë?öéÂõ5ï.ü€V…}Ÿ]Üó"ïjÌœWžûŽÌíÎ ?$tõ¡ï±©7(aõb®"ÒJ©æ/Äb„…8qÒ½ˈS< m–;Æêۘϵ֔LÈ‘É.á¸@À1»ÖX [ zÚÈ`˜ }Y]µ§½ »ÝÂ*C±X•ú{õSªe‹‚S7Ñõ[ Ÿ_fkÛ·R]stM&,³±.lÕØûÕ®ó¶'JºÒ;Ma<¨Ý­ Þ))ãx¾§ØÖ'sVp¶_SS0s«³£Ã } }]Ù´n ~æ›f/¾$„˜oºOh¾õCD¶§Yî8vB9èšH”èÛ4kù™bØwTjœÄ̧ ¢ Ý(toU·3éxŒÃí¹tW¦«!k‰·ø½8ÊE=¿°;ýgñQ3ý„B•y>(CÅÆ:ý¢@H'Ì–£?MéOw¥;ÛZËлþs´¾Ü'žè$ =CrŽŒû ô‡F+gç±²‘ª®1cƒ´‘6Ic g#ÂF˜è‡p·Yx 1Ó5i™Êzé;E;aƒU’Ãy4í‘¥V÷YyˆoŠ2ÓMÚÂ~”¼^Esx2ˬkjbdx°¿'ßÞ–žÍÌšÔÞU3þŽ›ù£uðz´:x_ŸC1N³æÃDÚ³‡ö¬XÙKVÓ”ò”fçŽÄ$×K´36¶/²{¦f¼„^£EP3ñIÇ¥EïdHÈeDTjÊwÇ¢e3é–úºhs¬Ù§ ŽxLe´[?Ó£RØ#ä:n~ü®¥¥»¿ù–'î^Zºû‰[ÞpË-o¸íÄ ZÀ”iǽ_»íMÏÜ»sç½Ï¼é¶¯Ý»ã•ß9súôø?á­gʧÐo’¿“ìè½C¦ÃªÓÓÉ_F):ÅI¤-ërqü»¶m{¼êöIcWC µ’6Y‹Pµæ%M…V gf?8@M~43j¶ŸeL¿Ó6¦¿>ŠÐg4wÄÌ×'{ùò‚ê®®1å#K[T´÷L>Ïk~4?Òßët¯¬æåÅ_ô=ŒÛ}ŒúE.Pä¢Åæ]¡–^ =š{‘,²zÒf‡‡þ r‡³þ`º0’–œ€:ªhIåÔ‚XQAÖÚŠsO{º½5SzîùÿC>…SÅ~t[Â/ü *ú"7‘˜O0êÝÛ$Ímˆ/¢ó”ÍzжÀîðu„€§É_I»Å“Æ’ÐG/ÁÖdãO'Ûz9*cm›¯’wèÒ÷jª‚ˆn‚Ú@?Lu,`±ƒŽ¡J kúL151:ÜÛ“ËÚg Ý&¾ÏjštõÃWzúɱv®Žä½ôXÛ%Îpe/µÊ_‡W;7·(®¡ipº´™Áig˜¢×F½xØ€©î"ãíÿ xÿß—·!·¡™Ò¼Q½±ú‰&½ýéË߬ú±"«íœGâ´Ú‰ÃÈ# Šn¤µÇìÅŠDë^èd³ì~§Y¬ä6¶ ½4àât8¼Š¤xÉ"Ñ)ÞJ8Ô\ò(’çMÎ ÀIRÌHÜáéÔÙš¸l÷ŽísdEåÔ†9^d5Mºøñ¿"ôâ;°×_¬yÁêþ&Î\ò \;ña/½ë,#PÂ2w)ÐTÈ/y%¿w£ wXȰĂ–têi‘³šQÍ’0ê¿PßoÑh­Â÷sÀs–UNŸËbE‘½¦‰Át ƒ £ °¤µs·}ð•kÛ«À6”«(1KÆ£ik¹¸"¢¯å€TBð†X\%—^ËUnÌÖr  ‘C¥µÜà@¯ËZÖ …/ѵBE“è {šML <Æ´|“þ§3#üÜ¥`mÑ ·–Y`+ŠZ !ÞÃAíiB@˜N[FcâíxñÇÜ€`SC!“­71>6ØŸímÏ%b²ˆºp—j$²7m4±¥J¢ mâö„6tíñ…°ÜÖrÍè57LO=}Ë4¬>`52±ñàzz0ÓŽ•·›—#çÂGb æú¯j÷¸çK·ÀzÖ'Ç¿ðö˱鶆Að5ÖЍùtÝ5„v =èqm~‹C¢GYs,]ÃÉ­€PSèÔ[ÛV®Ëà&XÁíÙ½kç¶…Éq¨-d^Éù˯äìï:Dû;r+½þDûPcý@[<Þ6Pß8ÔžÀï‰ç†sñxn°±q(¿ð‘+»ìiÈÅb¹††!À4Ô`ûŒßp š.^Ou1ƒæÑŸë.Ø‹|¢×·æØk7Xº­€s=w)ÐT?Ã: mÇ¢,ÔÚM·€²æÕ¹ÙÉñ᡾\ËLzƬ²P•‘A/ À°š­FW©«ñŠïnÝÓé©Ífü÷6íÉ·Î w¨¸d£§¦>:~¥šM6$Ómc‡ÕÖdSï9cò›Ûüæ³x ™V`õ¨Üþ]¥6l·¢Ä æþb6r‹¶O2†~õ WEÖZÅ—P%bÁjÛRZR ÊáÁ®ûòî‰}œ•ßMÁÏ¥º§‹9ß§»Så”%>}滎²BŸæë–¹<ƒÎ¿–í¸¾ O ŸjUBØçõi5B/e`]":¶ÆL°[^]ss,¼™Û9·cÛBéÁšâšRú”]GØî *Ñw„è÷¤ èÿcr`Ÿ{EŸwÝÁUM‘6Vg;äË¥ÁS©ŒAmB) Ba"¹öšµCs®´P¬î²>Ž¢ã¨õ {´\ß^mËvªŠs~©¡T}s:šš˜kìéÎwµµ¶t¦;Í#ÊWÉ%êEd#å| `T‹í«äðÄX‘ØÂGÎT’ ;F€À­*™TٜɤÅIKgK¾«FàyóðPG; ?Ã>‰¼æUû‘$ó²ï¯9™çŽ!Œðq¸œIkFh;WôÂ2<©ˆùض—„¡yýÑ {yðh `Ü[Ã]óÑÑޞѹÑÙ©‰ž‘Þ‘¶öŽÖ–SódáÁÏÿx= g|¯§w=C2as' u˜3øk”éí~v±Kß‹‹-J˜½b»‘®’F#í^ ŒµÊ gÈ¥ÓPq6]ŸoÈãNu´g<,æ¢ô2Ýœݼžéý”æ'w§;OäKV4hC»’Fú–W¼L¾ U€$M Ý4œd¢¼Á¸ª ¦öè¤ìzÊÂzÖÑÛäó,"Hî"zr©À€‘„£ÅM »Ž‹¬†´ ].k£ˆTU&¹ôº4® ˜ˆezr°ß"o#­ÝOd2Bl<ÑWÔ„=¨-Ç)ž$æÝFÚ¡ ‹ö"ÌGŠhä)ŠÆé7,êüTjžÔ›«ySKJ¿>aüØZ#Ù B]™Áì ¡7 Õd;ÚÓþx>a¹º¨Ý[„wi±ìHvÃÅ&öF{¸—ûÜ]Œ5-ì]ÞÏÑȸüºž¯|¥ðFniÇk!?àÀòdsªÛ°‹'¥Ç;âÛî~æ-'žy`ÿ@×]ç××ÏóWOŒvöæÚœ<¸zÛ®4Èñdáõ}HÅoe1tD â8¥K\#æšžvÐå'@“Õ™¿õ’oEHN«]g<”º$ag1~”ÄèúýÑã…ŽŸYf×ËAdè…±2 –°Û=ÕA;‚æJM3é yf¯ÂyI¬/Ã~߆+äõZpX®¯ÂYÌ@?Bã£ýê€ YÓp‘5Ñž$à]¢Ù$Ùõ*«_„#VÛ8iµÎ¶€ýfkrd‚X˳Já1k²”ÃÏ[¯º†º ¯n¶ gÌ/„‡Ô$LÍ}ù®æ&d4D8NŒE9ž´AØ€*Ï/nmLÓK óeg—B½=mðܼ»=-×:¥™Ûé¡^bƒÔ²Lˆ‰v›øŽºê3'õA"°º¡®žH*Oä–;ØÄ&±÷ÿÕö÷ßx–ã@NÜßÿVÇmûAB ¯žþ õÝ·Aö›Ûnß}›6·=MxzXõùeŽCI,pœñPÆòØžÁwݱ¹úºÛc^‰Fcçcg#xÈJaœÊ¦2͸×¥[spsœÐOŒ•û3v¾Ù¯ÓÏ¡Ç^Dèj(eÑà ÄS3ƒ(ò› [|Xv¥5¢wÙó]í )EL¦…û¹¯²øæ–"Mß|¡ßp‘h“C†¥Ÿ´ØeSöI‹» ë‰ ;ÚRõ²á~-‘åº[ЪÏ'ª&:§×%7“Œh1rHLSüÝÞ?kÃnÇ· 2ÃÄHÏp;^ÄU$djÿðÀP6&™{lo=oÞ?ßá¢áÐx2;–ÏŸzªðü1n©!{ýÁ×6?SR¢íã­¿þoÿö<2ìú«,&1Ùõ³èAòó.Z0=pq³iÇáÂÓ’®Ž–&]®ìi ¡åpá”n‡ Zð …ó¥hqZ„Ë3‡-ØŸ¹8iA<£E 1z;w»è¹½XòÅL‡Âú0_WÈ›ŠÃØ(r´&–‡dÃi¨OÕ‘Þ“TÿÅ'.6ý‡aƒ×^Uøw[(ü+=O ÃÉmà œ¯6ÀÉ-ÿ¼Õ:.<$œyù= ì(·Žrã¹!b÷+…sT?=팾ï%*áɺM䳘,“à}¯ù¯g'Nއ Í×^ÌvDf1ÉI½žÆCk¬%"Ñr,$\ÈòLo+#’=î0ô©!CÛON@¬5J’&=t¬sÒÁKâ.Ù/Ûgi»H +–™ØúÐÅ—ø›O†ÀÖo׿ØËø†êÅXl†µ¬iù*ʼ-‚ÜŽkyñ €¼â±¬HcœyùZ=\’£l ¤ 2Z7Á`Ξ›ƒC·Ž¾l{_6Ër›o þ8t.†Ð7Õàb:‡z?HB=è~aÔ#Xq= ÇXQG׌ÇÔ%@R.ûGòlj©±m`%Øô…»r‹¬Ž³;Å@ƒÁ%ì‘ì<ö“NEB«»6Œ]6‡[ƒÓ´h!mˆ¥ÒZô…ó&ö(d~¾Àîþšlô›è¢/jí6ÚWQŽ1zÞiºþÕZXÒ ¦vÛ HKˆU×Rpiw~-(Ô…Ð &í¼%± ¯×4XËòþ}—åš@Ôí錒½VÈÈp¯`Ô ‘YÖlzJ7Êc.‘$÷Õ¯Níì?rÅžz(oïUû[‡šC ׽ẅÞõw]9pxy15áM„kÛû¦Û§Úæö¯îŸk«¾`4CµéîÔÑp4Ù˜¬mîˆu´¶-Ü´6{bu°6Ý[8ˆ$"c½ÍÍ©¦Îé+¦wÌòÎÜqdÅÁÝ)×uìOjsЧñI4¥Ž×†Ér…£éaçÃñ¶€1rÑlEÅ¥T.—iŒ!1Ä#¢pG÷Z ÷òª×‹1ß‚‘È—EôÃZ‰–ÀÙalÜ©ÉFÂåÛ'ííØA1¸_* Æbšd\ƒ­ŒÄ(fpîÙsÈRKÇÜК…ëÙÁÖÁÆúdpóÝןÑlôQîçŽyëâ^^À^5¨ÛhËÆ1å åe¤Èî ðR}Á¿åUØv)Zl€I ›ÍJ¢" +u0}"ÓÀ*CÐw§ÄzúrY×iéâÅ‹?§¾mÙx›y‡–Û·ô9:®(}ý³Ë¸ö’Ø—q\>‹—«ªyÑÏõçÞR`È EÓØX<]° µ9¼c;+vc»û‡¸‹o¹8ÁßMl© àc¦6sqö.;³œL“#¿”ô/×ô[<­z°#ç8™ý‹d‚ç r(ÊÒŒƒ Ó˜aTUƒ$ s4@$ú.¹(2A‹":ÖÁJAÐÄ~}}}}¹\¶µ6nKÓd®æAMËpò±,³Œm˜­éÕf{BàO¿úýiêÇ–q¾ãÐíÍCn?nr'nÇÁþîÆLálgáÑ'‹C~qö— þïo½•ŒûÚ‹/ ×°u`-ØòÝZ¦ÏïÑ;,ÞŒåZŒè!‹?é%n#¯d¾•—!dÛüh•–:EÅkˆ¸$²‚e¨f$Þ½n•Z¶ªÒò «$ÚJ²Ì‹ŒÇFzò¸ ·µ-eõú^Èg™í[›äó9®ŸüÝë”O7Y•Ëq5>V2¸®$£!F§»¬‹œFày°G‚²²G!‹> †2ÀÝ´ÞtÕÀ6‘իä‘ä!#@‘ˆw••M¸‚¤×ÔDG;dÁoïïè7‰›¦žÕä-69ä}–ûE)y{ˆì<²ž·:yk0E†]aÝå­™˜GVÂ4Ïew·8Ϲ¹ PsÁ:xeHº¥^ ›IlßÙÑÞFD”c ûâ4~:l‹:4ÐÓݾ­c›iÖ˜b¦sÊSÔKºÇ©Û<›XOPì/ëèRæÈ½<¨‹.µ‰ÔCUÚ+3$!Àò¸ dœ® Î1ãë3¾WÚ,Ιê­ì(×ÔzÐÔøÕÒtÇ´IKA}Nr™÷Óçý^ìuÎûdj”%¯¼¡ØãÓ2ó¾£³PÖmÞWXˆiCPÕ¼¯RÙyeo5ó~U %æýñ±Ña·y¿ðœðKëÚ êªâÿñò}…_ë3V7‚ÁÐ …ô±Æ.ìÅ0‘©wFrØ«Ko›»ÏqG†Jãqî@R(’‚Nï±Etêž­b²ÜŠ dŸ+?¸1A ŸT«û$ª ¿c¾]ô;u0d[‰Yf1ä"ùÞÞÝ9;0d(bò›4UaPg«¶J5d—jE/¯ù¬Ž¡É¶Ïòm¬Ž!ú¯Zl»ü½Ó)ß‘V©ÄcN› }djõù¢eçts§ÍÀf·P ‰Ûœì‚ƒ¥}ÞèBd@çd >¯Oòn81•™“·ïæ~šÛ!06âXçÕ˜Öyÿ¹ÏSjŸî8ùœ#þ¡Ï2Ò¼˜8+bÃ4û `Nß¡ìðû8™,[°ìY£7]<šã ´×=ùUó|Œ7UµOòErìáð†ǹÀ!+X†‚ÑNØR`d†®BQ Q(6ï¹õÐ| a(1â $óx¤ÒR"mT$ã'Ê­'¸ß|Ó› OpÛŸ/³˜(?_¸ð<;׿ø’ô=fãô\ÿ^fãâ ù{“6å§Û9DKêéŸ$¡×^Ö{.NG>™ƒÍp¨ÎZ øÞãA«^©†7ïÕoª¸e?B¼3±ųQ[“qCæR.¸a`©^®”¾ Òàk¸âyU‡!¿ÊÄØð`_o¾3—mi‚ÃéD<ÑÎf¹Ù°~þ zù/lOϤ—Ï #ÿ J½ ¢Çs!~#ŒƒÁà"XUºÙ*dQ?:¤!¦’(Üu4EZCA.´B|0ij!ª€6 ç¶‚ÂE]õê$iÈ7AçÜ&AÚ4PT£ë5µKƒÊs;Õå^«å"!ú–ßçÐ÷Yñº-é;Hd­UnIß[ƒ4ë[ƒ4ɸŠÒúf ùPТ¬*†µ®ïªQ”Òwˆ ¾ƒDßA>¸e}ÏÍéY¥ª×÷½l-iÒ÷9áCäïw˜dB;ˆ:§/'Lßh/'¨ÞCXSª©© ½°Uï[„L‹Ô ²èK Ð•¶N W³éÄR‚ Íã˜Êš’:³C“¥Bu€fe‡ÍÊî ž|ßÞ=ósU¨Z×õ5l/[×5Äû¤ï “¿_«rŒ÷3ùÔ`˜|À¼£¥ôUkÑôÖà’Æºa̦˜’t1×–˜® %Ãs½Œ†k+M×E,eáݦë f-×Z†t1'Zy=G´3|ªçËÅÈ?‰Ñô u¦Út}jŸ4}úèE(Ä­¡²K¾Úm϶LBÿÖ¢­j ÊD[îॣ-6 ]DuðT}Õ–ôÈm,&,Ç{{º:Z3ÍLsÑZí²Ð ž ;/ ¥µxÛ¼½¯§ µïøÓ‡uçÞþÀáhãGénþÛF!ÿh¡p]G6R )Ñàe#ïÜùŽ ï…/"54fysÞ0 ùé A"wh`ŽäEõŒæO6Ù9€iî8/Ü@Œp©J?2†ÂáÐJ„ŽãZ\SíĂ_Æ£sº$ð¤qÂ0[TkUxt)bs†»uZqWb¸¤©E-v®˜8"C;¬y©-à±Ãô ¹ð†aÕ¨Ô—‚Å<¢¦°¦B,ºvhåòûöìÚ¾mî°z¸¼C‹›b”5GŒò”ðibgÞ*íl€…`B0·RSÖÍY'¬-#“q[ôWƒû”5ÎbA“談§q‰  å¬W€†“É*¡]¦­zuÖck¸MS\jªœ÷ÖÔ^°—û©­¬ª«åm%Z6ž}ÌÏ.á[$šÁ8ç.b0½h —"•M7å€Õ‰*áˆJ°Œ‹gzAh;Ú¾mqöf¦á ÀÈð`6—ÍFZ³ÙpÄz¾Wj/![bB?÷ÏVÚTØé܈xÛÛ ¿]< tÛQpnAa¿øÿ=v.qÆñ;µúÁ°ÚìŒp æœg´Ú즒ë²ÀiY|¸U8/Y›]oHë#¤šZk³—h¤gâjiJeë³ôiq:ÒªÕfw§ÿ,¾²ý–Lk&úWìô»6ÔI[1Ño¤d+ÑÈ™’ÍAÿí6úÏÁ_ôQ5˜Á¢¬ó`$eƒ„k˜Œû š)Àãª`¶ É¢¼ /™ÄM·VjÆ¥SøµwuBJs3éEÚ›i§y„sZáÇÄçЇ¬:€‰¹Y¦‰Ø@°ÑEÅU@}#i ÓWXënÔ´ówÁc º7é„z/¡žÓhg¹½6T¯Lh ™ì¤±˜®KvíÜù»½sHà¦'ð"ýf=‘<¦.at,<¦Õ*ô‹Pµ߸oßıž¤/¬zÿk¡¦ùY‰ú ÷_qd¨›ûî+͉ÄΩÝã×½{…{>p¦´ýZ™9{áá‹=×&˧Ñ¢ÏXe§.B&nK–Ö õJ[s‰È#‹žuÈÕ¦ÛKµrµìñÑÁ~»eë|¼bó‰úúB¸xD6}/oS‡Ý#º7txÄ,mQqd6´¶‚Slíkííl·ºF{»ðÌ-œâ¿Ç?oÕ~ág¸²ë£ÑÁ‹-¡¡+/%‚<=û>gµoüåEÞÔÆJ§Ö®ÒúŽÕ‹4ôŸAç×Ù û.<þÆ„°ßçßD>É·‰ô‡dÄèœÄJÚÄ Øs%P¨Y”Ó~ZÇÇA?ãóãêÌ”»–jˆ4ô9õÓÆœz3Õ’“ˆeĈ<]k¾\KGŒzió$þw˜'I¼‹ï×'ÆF616Ú>#Lìý£âk‰½÷ OªAb+QZ‹¼nÝÍŠ‡Ê‚ÃzêÇÍç,®jQ0Ë5–ÕšÑSh«k°4U» ó²Ào”hŠ×XÖõÔ“ËåÒÙp$ëM˜…œLµ‡Bi9Ö ûÀ‰¥–#&)›„÷£¿Ÿ[ôGš®EÆß>Ûd%Œ?±‚gØ= Œ¼ûè:î|B·<á%bÃÚËÒD†k?º¾—_— þÅbŠmÄü©€$QÖKµYScÑT…ËTuÉH´?Ÿ{ÁŸúÈæ棊´à¾€P›šeĘ2z£b­ŽÁ!6ç‰Þk çøÿJp´¡“,»#BJa^ ïI´"Ý™†Ÿ›É2Âm“çÛÊx´é- A2}sKs±UÒá|‹ÏGˆú‘xbE Å v+Í‘®ù£†œüæZo]øÃrkÝw þÇö´‹…?㟯ÄÃWõ®÷ã_FS¯üÈZst(#$þ#[‹Ë ·ûÙþ*­UuäËm …çè±›–ÎD(±Á rDˆ%2³ÆÏÚLÚÏkj¨·gh g´w´#ŸN{š™wïÿ,GÖ|è*GÿZ½ –lÕÙ¿ö³Ñ¿$™úöôôŒöŒ´²îõÜÆ¬ÿÛmýŸÃן×Uÿ™ÓzJýÚ ìÛÝ´Q§oY"qÓúÛšê w¥Yï™|±ïãæ¾é;<Äâü‰_VM2€à%e^ì'YHB„°nïÛ×ÑÑ‘£+Ù¼Ñ÷Ã6¾ŸåÈàAÝj§Þi±R‚¡†K¦ï åÂ)ñù¿´â"||ËŠËŒîh^ÍŠ‹Òõ]]Oãs×Q“,À¯´À.²lpÙ%ÒdÞ°‚¦¹´¶YäBü+šù‘ðfï&ý6ª)¨YLúeÃW›';3AE)“g Ðýn-wâ_“Ÿ;Õ6 ¾X¹Ó‚©#¸Œ|Û ×q3.š«ìÇZ\{ŒØÅ'5”Ì,êÌÅí,¸ëÕD±¤åÈSÝÁºƒ¤þO ¼@erpp¾ ¼â«Ó ñ©è5ÆqÒª¹p’îÿZm~¯ØÎâý¼Yâþj;ÒžD^ÁÔÿÍâ9ß$píj+é^°ÐMÛU=¼!x ¼1x¸Í¦ãóÝ(òPUÀ®"`£Åê’ƒýÝùÎöÖLScr¤n„PC«êé9gãëØC~þ<Û“ A™–6¢XJ½³©}#ißh;½Mˆ(_ŽJXs,6É3tͬÛukFÏ…â²6hi| ¹¶#¸’mKfu«â4^P¥þ'{o×Q$ w¿kîÍ%niit߇IO’eù–mɱÇòíÄQì$Nœ„BHHØØ#,g Âa9'ÙåûÉr|,GvùöbÙå†e—åÚ/߉gþªî~3ófÞŒ›;D3óºêUWWUWUwWã˜xUÆNˆ=¤-~g4ª†:(𝠷w 4çðatô®Xìé¥_ûµÆFû#® »=àz3x"Ÿ»Xùüôkjéëä?© ¤ÃµÃµu#µô¿B5˜ÎÏñõíöãcð¯!/Œ6T$R(¢ÅòòLžG iÊ,1 ÙªÛèsYðµŽÈÀYæjѰ@GqÎêèo$?=vêSEsÏÓÏr¨`ào³k6¹ØäDØÏ–o E£‘H´+ÚÙš0¦#·2>ˆƒ¿{ß}Ÿ¸ûçkð·Æ@™dW¾ž“»QÚîê<ìzbxª¨0õáLdÕæ@¿ß_㯎ûÃþ°­1÷~··Ù÷K䃕„¿¡šb˜[0U€Y“Åh~Þ,ðTSÄu·VM€û~ˆ‘€æñ~éç\‡òúÿ1ò×Ô®·æ9KtÉ¢b¿¿Ìîä}a÷—9~yÙû Œ`k6j5@~àB?Ì'F¯’¼äNö"j7+x»‡`…]h šj2`{j?½‚öz™¦$ÛršÝ’(€ðŠÜV„ü‰@“pñ»…&2ÏÚæÔ·b¿&°_#,ß|Ÿ²]ì½?oæúœ¦ng˜ªRcƒ¤¨€—ßóYSøÈ.euäýŠ—=DXÁ·Srº¥%fg\«â6iïi®šÖ¼Ro£9¿®Ñ: 0’ƒ‚ ¢ äò:€‘ݲӽDÜ’ì–N1DÆ ‘ ƒcq8|ÅôM«†õH8ì›é2òšj‰îܱ}ë8 ɺÑþ¾ÞXsuGMGUeÐï°ÑˆñÕ”Ó¡¿`:TÍ.s:„Ú!ƒ-ñ;ñJèhšvZ¸”¥t¨Ý¤Cé?‘~Îý–œÑog®:TŒÂ&¥j%_‡`>Iáò‘ Ù-òø–@1÷wçÖŒzÔ}Öä>‰WúCZ¸ƒ† l†Ybhpx„65ãgéçOúeôÏ.ÎVøéé[¤÷^|)[Jq]¿ÿâŸüH:úñçÞ÷Û×8’þ y=ö•ÜÄB#0R`s§jñ/0ZÄKMðj£Ü0èÚ–ŒÒh®Ó|‹…FTmɪ©q#H5^QèÀ;âüá [eG5Hó®ßæqøâ¿aûFaŒ$:´Äº÷ŒéËsòõÒ”úrs~?ƒ½(ú î­7¶j6PQͨ:†·â…&‹,Ó)^»„ÉRD>bÜY‘„iA‚èrÉ ¥Úë-Ù¦*Ä<ª¼h€˜â2Y²U¢­ýÉþæÆšH8Xá½h‘ZœB/Îe¢ÒB¹<‡– zòÜã$]Ü÷l ¼Þ€ïÒÈ~"îîÍU².׊¯Ž°eê‚>”j\·²Æøþ"eû‹ÊqørŒñ+´ýlŒïccüG0ÆguG¿¾NR?¥ª‘oÒ²·lC%Üõà÷Q¶¯Ï Ê¿¿Z=Å[æ7JéÞšêæÆêÖšV"³ù”Y % ÞÁh™e´ü1<ø g%*¿ç²¿PñEì®Ålƒ1=Ër*ʼnhöb#AŠe˨ùf5,\t* ¯mBÑL£ ÒżF0O&[bq~³šÐ©%Áo®SxkÝ‹u_fúZ˜(˜÷ßq¾5TR` ð Ø-I $àeï ­B%‰IVA ApÍNñ¦æV)½²:"Ѧ†HKuKe8Pa×h•Teçû毚߯c4ý)Ðôù a`0EYa&€‰D£‹8éâ,‡#Ï:Š™¥“ãØN.lgÍæÖ¼ºîÅ0ùœN-ánÎ%ê¢cf¶IÄ%ÚÚïHtÔVý^·ÓNcRÌaœ#»ìÀ"íG;pÚ?evàzS_ëso‘qg‘¤ì#Ì&d6+Æ-¨)jVŽ cƒ|3}PÛËxŽŸ%v³íqÝé§x©‘ÂrÏ èç3_ú Hæb°»dŒR²Žvö¡Œ.Ù‰ñŽ¢Ì€£ ŠWGëüÁx¢ÊÆc¼+¤ÏJ{Ô;Ù»á3¼ûõðîgô 'µÙûÂ`@Æ;$¾òUÉ×m c6 y.õ~Âî®ÇKs¼Ùœ^•jØ÷n ÆN¨}ÉÌA ¡ô~ŸÙm„ß9Ç!e˜elEÑæÄL¨)³¸Y`û6‰î™Û¶oû¾Í›Æ6Ä£µÕ!?(Ϩ4*îø½éçë囥󦼞ÉÒŸA?t'æãBxœK˜ï˜H°™DÚgÜ+æÅŒm ówyhÇÑ÷ÚÃÛü¸A~Â4 ÇšˆSðŽVí({_Š½ï ³•;–!è *Ç"s»ð† 7aï?+h¨éN|€%\FÃA—ƒ*T ¨µLFÃ0^=âa6vo„'žp9óònuH·DÉÞµ')éx˜†0û\𞀦ìTÙµÈèaÎÂTDi"V[ | %Z´:ÔÍŸÆœx#ëß›peꂇòu–òÃê’Š3 Ú[v\sTzÉžÂëèœÑ½»yõ{6§—‰–xTèåIxWP¼÷${ï›´A¯•8ϤÏà2K®EzÿZ¾Y~RØï¿fpoa¶ÄåÖ$…ÄX*_ÜLÉ 2ÏD ~²ðËÆW& øÓ•Dá«&òÖˆÈ0JàÐ8Ç‚Ñ`<upÿ,Ì÷ŠwŸesǃðî\ÞªXp©pUÀÎ#ç™—ÆsL¿§ô@eX¢u5á¦Ê&·“†¤&dü,Øí{¹Ý>‹vûAf·{õ®,"\*Í©b $:[ ×ýÒ¿J÷¿>ÍošÔ}6Ãç©‚Á3¶P¶Ø²>»úиRÛË®—ŸecGrC®µ?O¡‘÷!0ZÆ\¬Ù‹&ÝÔ«ÖTS þQ[MD€‘d"jgëÝ×ÁØ~LÐ}燘ïÀo ¯Ì»¼:wó·—-ψϊAf%|…60Ͱõ!”§ f à×9tl$…—}˜âÆ ŠVáäb aŒù÷Äøïbãÿù™ðÙ†ÁªcH$c_øêú0øUüKJ¸qUùnDpl)·*»]·ØË“$ùr;ôKyy¦–íÌ¥1Z~[a»|ß…i´Ì5B'ûÉ®XK§¸<—Íí×Ó'4YÌíÈ“‡'§øMC ⦡¶[ÚfoññNÛX÷08d®]-¿ Þë.‹¦§)=$ÑÚ꼫}š¤&;ó-^…>¦z3{ÿ«Øû߉«H|_”±;(+&m=^™( ³Hßzëµ3ÿg. “ñæ`\3üö °—§Äû&˜\¾ ô\°S)k/Ý`m¥3L±gÀ øs ÇSû;šec}à<زw \ç.Ì<ëOÆB69g}½<Á•3\aü?Íq«…bCäÙÔmÍëšU#¿tž¸GîwîqÝFÍÏÑêÉÓdE‰•ê2ß1EÀ€_KÁˆ7Ó.£í<ç/~7³M =šÅkÀXIøõ`ŒñZÏìÑŸ]£OxóטÜ8Ÿ²ê|N˜csÂ<Î ³©Ç;øÒ✃þý·À9Çúú@pˆã Éf–YžÁG ¿&ˆ35˜ÔTþ#=’ýÞ)r*„‚U¸€Eœˆ€½Ø¯ö' âÌUÁDž¸Q±Œô¯?’#,}A+ëis¢Y^j‹x+èíÔKoizîc`Í›ÿÉ[çõÔ{·Ewµ(»Ÿ{|ëîퟗþ¶ùððÈ¡—4^OÃû€÷ì†ÆÏÂÏ5z•EwŠ]¬EyBÓß)„Œ’$HȦw*4‚Sw EÄ–€qçgšküJ°w¯áò³mœˆÄ‘ƒvÓ1ÌE[Zh#m¥×(¾Úð{ë[C­Sµô™ç_[¢GÒäñË7¦çûæ3µ·†k|ªôOmSkuú±¦~æDýh˜~†~Ë×:û›­j°ù«‘D}ïã˜!x`±pŸÞ-SÊVùÕEE²ËŒÅšæÑ6I´¹©±¡¾®*ìõ¸]NûFü“¹LÈ„z4{'ÚÿayÛï§ÿ4óò&ÝÉ· 4‚åÝ2ú„S]hIŸ¹ÃƒGS=Ltš¸íå{yS¶‹¤°%F’ðPÒl©\kSL=MMGc±öv‡qŸ4£}"{/:ÐNºõ¤™šK äx„}Îìbx–Ìengvm±À®éé{3Ÿ*ögý«ðgã¿t6£YÒ>™~åo"íñ_<}OÎÏÒ>G? FýfN;ÉÒÞÄçÍ®ÙìK–oÂÐ…™†j;[÷5Ú¢JÙ÷—"Nw%bþ¬7Ì}{F—É·gÞÚW™…bôÑÖR>þzÃÇw:\§k9×ïÛpÍ—«Úeè͆¬'¿&MlËJ!ŠrA >\ÖA\×þÒ6I÷Ÿºòèáƒ)“‡†ñEúžÒñüåñpVº8{)?x’¸]—Û³dé{KøÃx³4ÕƒÁfD…²·„‡\€¨‰u‘ð}½å|æ è=g}á"L‹v˳¿ää1Q„ ;Z^áT—ÅÀ¦’êTêº3§®<~¬ ‚ñññ{mÉñkdãÇã—Æï7cüò"$1~?{VÒÀqÌ4@|„ã7ã÷ÕRqÒœqH…Ïï«ðç¦åÀÊã%,FæŸ'~µßˆœv–G»‚è§)ÆR»Œ`è²aEyÙ±¬åB-N}ëjÐùˆßçß_:€Kéññ±»î|É‹o>_:Ž ò8ŽÍo§ æ· ÓüÖ_*žÛ â9cvp/¥ÌNìN·lж6\u pð³K€æ)n¤4ð²s\dbìʇì·pº1–d¶²D,Éæº¯åÙÊ+çLæÀkïøJÄ;*RŒ©0ò•ˆ€,¬å†´MlV_-ñËñæ¹rÛªð-7YêILÜqû-çÏÝX.E±#Ó™g•GÕ'pì¦q¾ü{¾ÞEž†¿Íb1¹’jRa;/Å üUÎûÕøA?àŽ„N¸(º¬ì`8ø´b?ä´Se;úÙî1^'8ÉSR¹Æ¢À®8 P ÐɶHÚ\âRÉòÍõn¢¨DUðˆz ¶¹Ò`¯ëÕ7ë›§'G†:&:'p+T(à´Óv©Ýmì™ä<|ÈÌCuœ†àïfÎ…À4µÙóù¨|4žÈyOòTÅÆâ°I6Ç1‚挈K¨ ®ºD¯Y&Õ°²l %VÓÉr}Å0f~Âça€d9] ”³Ú™cu2úÀþ¹³3%mÍgôðù U§ëáïÊñ»½¿s#á´ gáH¤ŒÛÛ‚PÞi¯Kr8Œ/yÜõ.¹³Ümb÷¤•‚43ªº%.ÉeŒÀB]®#éd³\>bàpÎv¨< ˜m 袱.ƒ…»;;ì)½åÐ!ùC'8z¸ÔøûÊŒT¿eéƒ/Œþoñè?Zfô±×þ²@÷§Èº‰ÙÚ0Œþ;Ä襯µ­OÌcœû±ÐÖŽŸGòøŽ¹©¢)»æc˜<^˜™Áaðμ&o¸ý‚Q&ó;Á±HKkGƒÄO¯ ™é…¨À×XŒ¡[6>„9»Ý=ɦÈÝtæš“Ç\±wÏÜÎm[6Më‹“‹Å#4éñÃóå›/wå4ØQr¾tXé©Ãz ±&)Ìö0†6›¸]{Ù ³‹Ã «W„„õY-3eŽ0¹Ñ( [4gF§ïç÷lߦ§¦RÅüö”æ7Fjä5bÞ\¿VŽïñ|_£åð²^:)vÓç’°„$ÿRÖtb`5Rt9³ç)M0\Ào0Y+ÆÒÉr<&,Ä@²Bë9m€ƒ¾24Åæ³ýàA.Š÷OäqÇ+:ë1Ùž¬¢­ é°3³ªT__¤Ýå 9£=9ùKr•^N•¹M&²àyi#ãdŠ|_ð9 |îÅB&>ïrޯƪøÁ¼8d¬ˆ‰{yÁKæµ]Fîz²ÊX¤Pìê:Ú)Á«˜†ÛÇ;ÆãÑšÖå m´ÍÅn{nõʹ:·•U#X¢HöÒp¨AÚ@±pC¢\š ÝÐÊ+á¯x¦@ñn¿£9ºûŠýíû¸nbómï>~ôŸÖys¯jÇ{k·U¶­¿øŸíÛÖ·Ø+½mÓ‰îýÓÉèºébÑ çŽ½]-þMw^8wë§îß9=¼®ª2-w/v4÷5Vœô7F¼Ÿl˜\ÚöÝêîX8±çÎÅù77*·<”Õ›ÍúAþòBßz©úe»:xï1üÓð¹Åp¸²Ãça›‡Ð\rØ‹RÅ•«GÍjßýíŠÉöÓY°))ÒE{C¼JÁqérå\ ”çÚ¶g÷Žm›gô}“ûŠ5Í+l[úåQù'ö~޶àÁ—Ëaï1ÞtØìŽ¥Rã4<¯vd9•…±‡Í±¹°q0§‚eá BFžeݹ}ó¦IÝÒøü)α²ØÜàÓk.wN{ÌÈi»œn§Ëm§6Yèi˜CÜóÄí®vò9žM?¯K'sY ±8‰ÛéÞ_2—]4ql.Jƒ¯SÑ$ÇñºæêÇZ67j=n˜‡¿|ÜDŽ\j¿´,©Çjd=%¢¼M¦Ñð0Ëá÷²œ§§ÐŒTVú,Æó¦3Vœ´Biæ« m6ºƒ8¼’·Ä@ ¤Ôë]1Näζr8‰’­6.‹Qß“E¶œ•AÊEÊ—.v^{-JÕµç®=wöºò²(#[,ïž'Y,ÿþ‚d½ YL²n<»¬dre‡`sN8oÎùîåÎíï¹}â¯Tø97?\>9Ü rQª¨Ì“@ :`LC{² öˈ;¸w%ˆ+H "°¿dßô…TárÀ¥ ç"ÌÍ_ƒÜ-»çî;nñ­+[#¨4Ùœ¢œµÉyýå^+k†>ø,ÓÿË9"Ù¤þšÑt²¬W¡¾’KE®ÈláòÂÊQ´<ÉxõUÇŽ,\&ÉXzüpÍÁ䬃YcàҲΫš5²–ÞÏúîaùÞ€—¥=…ÙçrÓæ¡7ð³4Îål|œm+²ƒY.pV0È+FŠ Ú^)1p®pæØ›Åf-P+ÃZÍÅëô §o¸îÌrBÌ‹å¬Ö3L~É_°$Ì,ag¯[‰„åìXI…¯•óÑ\Ž5“=bͤÌD\i¹æ±œc²7»lq91c׿W€Y8•%×EŠ\“ƒ…Ë,—„½È75V]^þ²Ûn=ÿ¢•®½T™æ· ä";¯‘Ãß¶Ë‘O1r<ù;kJ,Z¤–F RD«Øå3Q"Ë´ò->zËîÝÈâÝWì¾b~O©Ä“ϤkE¾Î»€cø±ÎûQxü6Ý»žR[•¥&ª±Â¸I&¢„Õr\"ªŒUo±˜´¬Jò"np0Ò‡"ÇJϲóÏa‘-µèÊÀa ˜Òƒ6l˜Ü09¾qt¸%oŽÇb.^‘Óÿ™~ˆï¬Zv`œÚù}P¦ŒhÌi·ÙÇJuËËàź'miRº~ï¶-Sz~‡¬ûÃr­¯1 2ÿD¾«¸sµìG—ùG±ô³ )u’c–„º˜OæqKN§ñźãFN«\¯WŒç´œn›{9V ¤Ôí.Sß¶"tÄÀÆìo©AÂ3¢û÷ó“JÃå-)˜¿¼…ÇoÖ¯z´~¯8l0øÐÁbgùûp~O0ý~‚ñÏi¥ßxÆÅî(©ß¸¯Áð3¬;©s ¥;¹Š”Û°kÇæ™ c Ù^¹E ‹~±†qÞ`þ¾VÄÅ,§çLrÅzîaôòk8¼nÉá0¾”’#–èýª±ñÑ‚S†§&¬ :å‘Bx¸"|Ä@·œ®×ïÛ7¶aßâ¾ÅW œ/_ß.Ð÷‰ë¹¾ÿ2¹êQû}ctŽÏ”çóo»_ò@Ýš$ÂJ zÊLÿZ ç/×Ç |ˆ–ôGåŸq޵໿(x÷(¼ûŒî¬¦Tq`%lÁ·h¶F Ö¡…‡óXµ¸*{ó4»ÕŠW«Q4[ªD“”î‹F£Éh²%žˆc~Q£ºhùžü,ÒR;Y¾øcvGiކè8­)ן‡Ìý﬽M÷&¨f3ðåUŒÔ”¼Š‘ezÖÆ›*K˶e ÛE÷xÍ+Ú˜¿ö&aÇ·À÷ïòY¸€ÐZöƒ#÷ƒ˜ùsõ½ùUK÷€c‘UœF78èôrÐú  pb/Å ýñññMã›&'²¥GÇÇcÑñ}ã è^Yi®QÃï7ÊWà´B^d¦·FXT¢ÙÈ’%Å5þX[Q㯴¿“ˆ… ý;¬æv´õ‚¶o[{dë Ìé 'YÎÛ²®ï'µ5 ÀÅŠhQ²hY¯/¥û·oÝ451nšÔKó‚ÝXE^ ½ºŒ·ã6YOž¥Îï ›¹*^ózÜt¥kâYÎâ¾®_Ö‹š!vâ)Á9z<Ë!ÃSi¥ñ [¤.3)½z×®] »öÌŒŠ/–¯7Öþgv\˜ú¸üÆe~OÙq±žK™ ‰6„öY{Â;…'L*|ÄWAʧ+9E…|vòeªo_ BQigY<¥ÇcQ¾£ ´lʳGE~\žmþoko|ƒðÆ p/ãh[熓¾&úºBp!ƒîå]~˜GãÑ­³SúØÆ".ßXúþyv"z®Œ—[l¦ byY07óP}æíºéJ¹eéjs·W‡0ë>o&6Ðäb.šÐ./‡MßT1ð,ošvìˆGwìݱwn§ÕUûŽRñGž½þä ãóKŸ=sËŒOI»Íb a·‹#¡]"*c ƒËD4ÖÕ×x€t¹Ðê;V€QXíàòXJObvòø¡ƒû¯(ˆ…šòýjS쓳Mäðí‹}þAÃ_.9evÿû Üÿ¤¶ô`ÿhÿèÐ@6"pÇ:–¡›Gn¿t YÒ}¾€nˆ…$“âˆP'7q{PÙ "C_éÈp5·U{s›P²xr£o¹€Ñ„Ð9”ŠŽÌDÇ7½Tš¡oI_ùØÕcúŽÚd·kóåèåqVN>Ôñ!W]¨¤²B³×˜©2À±?evƒc°$x3²T¶]êI,liåå9 a§ÁÜñ‡H x•äÓ,VäÔâ_« ¦áõ¬x/«¤ªÆ³L´³ÂlªØ i  `Vz¯%1`D&Ë$'º¯§§g¨g(Á;ëÀu½ÌwsüÎö«ß&Ö1¾ý-ïok~¿ý=Àý޼þNeæè&”³ !o»ÐONCÎFÀ"×±œ2*De\tS´Ò‰—<ãú9ß³‘Oñ(‡ÌQ¼RД^ Înß8<ØÑ–ˆG›êk«*ƒÝ¡nè„ǸW“ëÉÃz2zrH5èO@ÉÒŒרš­ k—£Öº!05l‰7Ô;Ã@MГ¹ô¬çôäéÌÄýLgF ùGæ­ Òòd(ÊŽ g9„ïÄ Q½Ó›¤(Ƴ¡Û;@%Ÿ»y° V 0Û[BÈRŒªººÂÁ®®Þ;ë™­øFnòäi"s£ÐÌÛý.ô½¿·¸ïåuéðy á(êËQN‰dËúPp~ÏÖY\7µÊ|ûþ@}×?JHWŽ¡Ö.²ÙÆûˆs¸ÖçpMþ¯ãJfqe> ¸ÆxŒ“Óà+®ËŸç1M%؆[QBÓ8ô¼.¾»äx þ•hšÒá`¿œŒ_DÊ|õ±‘0iïÐ :…#›PùÁ_­PCÁ¾£:$7‡›ïHçù©oú;ò7.nËóS•ÐCȇ/²÷Ý\`7þ2s+|ûß4ÏM¥è¦Ø së.лÝË X…và´.ZyO•«ÀZ³B¬z~S•ÓEMÍv$¥ûCÁx¬±¾:L†P øö·¤?ª¼„Ç·`|Üø~¹»¯'¯ßš¨,±ŒZî)p ±kÛ%¼‹ZfõñªöxxxºåWÄ84Qˆ¤ä5a¾ŸO0ÍRºÂÓ6ÚÖ¢ŒÛyþz è£Ü¿]Âqú¦Øç:y™îí¤ª†~m §ŒŠåàgª¶c­ŸñŒÞ˜Žï£ó„Ò0rÆìëä­a‚*Ó<"š§t'8£ÉX0™°Góè{¸€¾ Fß+}¶Bú:AOˆª³ƒ¢gpÊØÅ›“¥²ísú;Ú6®h›ê˜b„:qœ_”yVñó¸ýEHçˆ5ý"9]õ¨¹Â'×dc ØÙ.’Éò"Hm-x)g*jªø)n†J&)Mv'»;Úh+mmëj±3_DÐw&Ÿ>´ó™AnçÉÓ`çïât6 ¨óHHhÚnEhd™VŒÒ:vY7 ž²h·$•ÒÛpïyw§ Õ!îrç´~ÔÄK‰ü`˜¯p »ìšKβ‰1nÑrZÌ.äÉÕhåÊqÙYL»QÏPqqVçCZ4×ðÚ¨x–ã´è3D.8>Æìf'wþ8=d–#ÐGDtÝ…ÀlD.1¢)TÑŽæ¥ÚŠ»¦µ¢K%›E˜PSªOŒ õtåI )A[×ýˆØÃu Ž 9qµø×Æüw›ì )NÛ%M3¾8JÓJ!"Ù€¹÷¾Ø> Hj·—ÄÝy0Äúï°¼êM›(Ý´mÓ¶Ù³\&¹-æÕŸ‹=5Ïü>òjëli^qÿ3WSäN±73g_˜EQÈû0q;©Ó}ÌAUb][ôR”Î.„6b÷»ÇBF8 ]Z $ÓŠæ6ÐÖCXˆy«>¾~ÝÈPogGrºmZtÆ—¯³èìÓÙ›pˆµ¼¬h£z >1Z)m‚7£K¥Û1úÂí@߆uýÉÉöÉ<#jM[ßûÿ„Þâ©'»9²Øád¯¶Q|·Ë.áäÍ¿X ct5 ‘l¤ÞOT,è¨ ”eÔKBbª+ˆ0eå±~j ™5µejËÌtËÜ&ý-æÙÛ„þ~ú÷—g³3åxVRS|‡‘*%t;­Ø—;¿b¥Á£ÒRƒKƒ2IlÚ„J¼iqÓÁ…½;¶[ªq…iü(è‹Nžâ¾RúËÜW¢^𕾽FOÄ¿: HÖßZ…û¢·æü‘²-—÷HÊðe’Öÿ^ñ¥«Ã’/²†çPL¾õç8_ú²ìËJ0fM\ó˜¤ M{K¹Û«äó¶ ´0êròZ9å1T–¥SG-ìݶ¥¤²XË[5ð-®(ÞY&ÔY6–)|÷$Ɔ—ýÝÙQ7½û¼96Q§NÌŠS6Ä‘…E3<.0¹nk©¯\9€!ÀbDË·DùÅ‘´A·È ‰¾ô‘ÒNöqêëÀ»SöÄjŶÒ×|„´·%[aK[WÂè þPƒ4Ð?. vK­-ãÒÚ góIþ`s¸™ž«ØÜÑ>?Ýæªðk²;\ãù€3àsÌ¿úÃÇ®~ûõëÞ rL¯ê˜îŽ4íº÷Tß‘£Gû+›‚Žñ›n<7öÚ¿}ÅÄЉ?Ü/ÝBw?”Ÿ› âð¿$Àß0Ï;Å„§kÌ„në ÓNÍDƒfTS9ŒeCßÊqV®¤i$›ŸZæõL$š1ÌVOg[ZÅÚhϪPÖöww%ÇÚÆ„,8 [¶.ý´z”ï{_‡ÑöÂÏ.ĨF ßµhe©ªi•"v¼¨0Œ ¿ÌDÃâqD³}Ëæ‘¡îÎdK}­×mÓÈ&ºÉåëÀòè#Cá’ÚÓÒꥼÂúðÈ@ƒdóÊØ¸–ôWÚdúõh_ƒ—¦½½±ºu]õõ±FÙ«U6zžªn«¯ŒuÙúV~‹rÅ‚ž ³¡ä(r}Yf{Û7Ãüöâ[o¸~éªÃ‹û¯XÀÍè[fŽm>V¨GµÏv´xh@8“±¨f‹±ëF$ôK‹ZÚh>/®A³è›j›ÞÔ8¼½3¶q¨+PÛ@ßÒ8²½#>6Øùp"!©N¦s“㣠й#`¾Ùº]S/Ô·.ê±HC¤©môÐd,RIW‚þIݘcy8B`¶Û‘/ ÝÄs¦=+ ®\¹MëI¶‡䯾é†YéWìž›92{¤ptÝk·’´¶6ЪZO51’•°vëH;4§ª&bu•GÆwlÙ8åiì_³QTü´¡Ñ®}}4>1ß³ynj ÔRçÏα|L®¢3º¥8³¿· ³_tN¨tv>˜¯ ÓUBó… ÿµ¡Ž˜P³WŒ•]øs r6éö¢ËüêWÞýò;^º’Å…ð¯pqA.ôÖ~ 뵆`ž{wYÖËùLr¾àþàD¾/H5æ ::L+Uæµ Ã6y‹VÊ;ƒ“…K+ATà Ž—Â! eYT<˜Û²Y¢×9uåñc©ý{vo>ºåh¡KhÜí\À/Ç;¿®æ{è¤üý7‘ÙMQ§;»V _<¦µÒÙ ¥‚â•sx»éÖå2¾XMŠEAÒ% ‹ ‹²ÛO¬C¨U`Íf’·Ìd®¼*ƒæ²²(õ­+ÂF dËMŒ7Üpà ‰Þpó 7ßtã5W?vÅÙgç÷ìØ6»¹PËÈÀ±^~þø­•ó7­B`Î7dàíyör-Èæ<æ ²ïefÒpQ½üÜ¿P˜”_#î‚ÉÏ*Њɿ,vfcOœ˜+û÷ÜyÇKn;ñâ·ž»áô5GØ¿o~÷®m[7Ÿ=^ÈëJ“¾=åõCyyŠï˜x½.—äWºyв­î|Ž:®SYÆU!ˆ˜èÃ…°¼Èܲ(o¢›3ÌïÙµsóŽÍ;¶ÎZ$$Šù æêIú†ßq~lßZŽç ø1E}yºxÄÄ™,I¥®Èóç'}+òyãñø²wÚU^ ²H²Sj}ãÞŠP[V ¿!ï@yd|éµõT¤ÇÊê'³…D"gAP½LA=îE¯á•Î4ª*TÅ£ö¬J;˜Í¶5{ønt™íîNåæ§tßï¯÷×EýÉæ [¨ƒ6û³îz³¿™úp &ÿûÅ·?öò çõÛ>tCzD>ñíoœë ºw)¥ß0¶tÿÜ•ï¸iB½ùùï§Ï7϶um¨ãû¦¯ÒÎq~Êϳì¬òûoú0i×[!²ršÈ’|;¹(sã6fÝd³Ù¶Š8¨5`•Ÿ¢}H{u¶^ÂO°gt€Þ“yŒ¤ô+†«%܉í¡.§ë´‚SJœîE™R<ÎBµCŠä^¸±2ŽªzÔM7Htïn¼ÛkïèïíhK¶¶ÄëÃA¯ÛåtØèzi}…¸P¼ûnó»ÉôáÝ»õ¹w»áÅ.g…¯•èÑÃûæ·oœ°~¥é\]æÇò»ùûêð}?e<{"Îz:DU›ÂNôIžÂ’ª¦à,‚¥Õ–\Xz æi; 5©"/qh?„ôÙ¡ý0Ùtüèþ};·'ã±xsK¬µKÔ¿dìiÎã[ñ½ÿó˜| ú9ªÕ{$ÜyŽUUÔÓ¬ÜQ4è,vP¢lԢͣýÝÉ–æÑþNÂc'ð>eÆKÞG¾›ù0éÕ»^QFB%êiÎXs[ksg¬ñÙòðuà{ŠüèlÕã0@%Šš2!óG ¯Ó&ðÜ]€ç òÀÓ¡'sýÒˆª˜»šL&I¢Ã)ÀõRÞÇSˆë®÷“¯Bw^øBñŽÍvšhTCy—%"/ª€Í=g@'›šš:šÚ@èÉD3«7Fz3?R׫ˆ·¥à9Ö k²¤Ê)˜vª ¦&7¾ÍM•á–DS{s{mM¸±²1™°×ñš=?ÉüX¹.O.²{žN†“¥K=hêb16Ã'îøþ´ß_KÎõÛiwÈ¥1vuÅb]Ã]Cý½±ÎXGœ!wq9:cßÌi…QÀËh}Ùc8R¦™ðA¯Äj- Ú¹ˆÃÎê®x£8 ‘bÀy†õo8Î3ˆÓ†U•ˆ_é¶çÈ謾 ”ÝAm’Ýs€Ô(‹šKbXe™.¨\V+*(ÅS½}=­‰¦Æ†úŠºŠÚ Ÿú¨/õÔ±>Ä3?RZøûâø>;ïCËcä¤~¬‘*ZµQ Æ,¶*ÜG$xN…ÁÑÐàÈXˆEJw…²÷ƒ…m`ø®XàÕ,º:bÍu5Ua…Ïë´Ó>©ÏcÜûz'pû=ò7H ÆèN&—~xÚãf•ÁÚ¹Ic‚ÎÊË0é‡>F"=]‰X$i®­® 7Ûj;ŒùGú¼z¯1ÿàÀ¿GÉ»øY8¬cÑHdÐqØL¤ÂÈøÄT©V0Qå?Né› zZaó¹0Ñjþ€óÔcø%~†Ç…ï3{ÚG›±æÉƒaIÍäIzq΂8$ÕìÍ!Ü=zl×$Eñ`\–䓜@AÓ”^=>† èMScƬmmf†ÅóÅòè;“O;—ù?â¼ê]ê'ÉM9:ñ˜e"û6ÓéÊ""cÙs”9ú‹è óyhz26/§­>ó¬ÜÄÏ[Õ£$Tm¯ÂSLñÂDQ›WϦ Oüi6u‰À(*„T`÷%°× „…Um(+h³âö^ÿèð@_WG{2m¬¯ Çìµl¯9%õé÷3·“ïëÁ&Б.*É£D²Ñiê´×~Ž:C„ÊEY4¥*§(ÓãÎÂj'z­ý«ƒ˜áõ‰"PàÁ9(‹‚ä0`Õ¦Ù}|lÃð`‚ÝæuUvLP–×èlé CüC,Êòº=tœn¤­ÁÈö¶†C•Ðl ¿²‘ŠlrýÖ@}UÛÀæá}û^uà ÎïéŸé‹Uª½ëÖ=ró¡×ô'Æ:çŽOŸ}ÍöØ®öDMÒçk¶…ûvwì¾asãGªêšëv®Õß56л! y*OŒÌtn?>X7¶®ßßxsjë»Ú\îÖ‰)g°þT(8zèæqbÈV'—ûz”ûJqNûëâ<òûÿ²–µu¥d·ñ+jCÓÖrÇh"”ê(€åÆ— ñ-+Â` ¿³@–ݵÂö¾$“‘ÎmÕK'UÌþö_tõ56ð, KUï0t2©b\p¹ümÄx™ÇFäEBئãqÆã¤î¬Èz•|Lš„Ky(¿F¦Â½öªZ½¡Èÿ<”ß ¥»¸  0§h¹ ø‘à´Ü†´´ Ùx\ø@Ë¢Êà¿™h‹Xh‚ M»—ÌÞ ªt!;<­ùN«€²j™Ò£¡º®Ó“ëFй 5„ÀfU+‚á¼zëÐWŽ‚=ŸátF‘ÎÁ³· žµ_Ñhσ‚gålyêRmyê²ØòT)[îß37;3¶¡5õb-ɘ۸ËèBæGô?À†£ü^`ò‹U5g¸ÝÆIŽîQ€µ^t›°¦V)¨¨ ‘©¼ŸSº‡ÒÚj¿|8gX­eòpWú{ôqùS ÿ]EÑ‘z´ë‰Cz„lz †jbû[àeaÀÂMLº ½VŠZ‹y¿¦tÆ%õu Eøì(ßN?¨~‡½?KThi”¶áx; Þz­ú8ÞŒŠ®Ã»( ÕØ+ ¹Žo8GA’I øcÒT%zmÁ/Tõǃ@)™ç¦JU +VÅ£ Áöªì“”îÖÒÚX¸5ª‰Z±äåò)úЃôâg ÄD/ü&ý0÷ø6Æ"1v¯ýB$±{ïû£é ôFõ_XÛGY«ÙLZ°x1ÎóûýçÉGÒä°hÿ&÷³™Gžï•z ݃JÅ ý~…ŽÌÿžÖ'=#R¥‡ð²S¼ƒÐ…äï X€Â£ÆñeéAV_? ïÃÏTr|EÔž“ˆ~ò]éoÊ·eî…÷ mÿ`0Ýáµqï+X€$î4~¥d„‚ç”ýrE‡n’o»h¿íïd}&€ïù ›/ȽÀm|ëj0ã-T ÈÂ÷I¿"ýMz{ŸìøjZZ;FNŸô³||™;~sðÁ/óô.ú÷Òó$˜·9¡„Î&ã0îA±™Ã(E2ß0°¹5¹y°¡aps²uó@ƒ4žœhh˜I¶Î Ô×Ìüòð*Ä›ù‘Ö­þ!è}?ÙLöÒõº³l¹‡:lÆü7BT f6iÉM%›&ÙN‘604ât9œ‡˜1Åy¬ŸËEa†ä&ɸÆq9pˆ%eøÝfqvx»àó.­€+,úÔ(ba{ÔÂv¶²Pçrâ-„|ÎlMD›ÐŠ;û]ý±¨ýJÁ:(íφblP*«lük,ªñÍ@Ã#ÃÔ(E#ÆÍ†qžÆƒ9å•îDÓ¹çýžDÍ­ô– 箎{Rówì:úŠÝ{ïl™Ô**|•õñ-©Ú®±hËPOé9yÿ¾+î;92rò¾+öÝräSG8‚ÿªsU¿ø ?,ßåšß¸~ôä½»vß{bx´ý Ýë x¯>Ù¿}°Æ]å¹øµ÷\¹nÝ•÷ìÜqïÉuëOÞ³ãȵ×9vú4³ {Éÿ–ç•ÝÌÂg° Åìp…Äíð{àù{Åó÷°çaÏ}ÜNg~ù‘úQžÃe³Û\`9\Úžù09¯;ƒ`T[©"÷Î&m ’4›¤nÙ4z* j9ž Ž®XK¤…qåuÏî.ËÇÉ3™ÇÈkŠÇ²Ë †Î.iv,ÈN3]†Ì^nµ]²-YÁYË\ÕÄøÁ ós;·mÙ4=~xâ0£ÖmŒqúMêGÕ'MôJôÛK6¼‰¹Úx)ÒÐt²Ê¨Õ¢7•N/Ó f-ˆG¶$€ú}MæY­ëÁ5HËò5=xR䱪ý4±ÉXEIÔý^dk£Ëé;`PàaÕˆUEþvÑ^¶ÉD!ÒÅÑ ÷ôH´g}Ïè`[2Ö\W 8ví–º]5¥ûõ>ò7¦~Ùdx‘H´¯¢_+2úÅÚËvè—Ú—éWôkd¨g}ßú²ýz¿¹_`ÞöáW_gLâcàÓ°½[â‹S2ìAÒî°ÃjQ·Â÷Á…dÍg;ŽÜêÍvyåp¡UÃE²Œ!N‡ìpB˜…'ç À±r§€”íÙ~ Øî°ÓS…ðÅ lOUD·ÎnšÚ¸a ¯pÜb_ƒ» dëqò4Ø›âÜêgd»sÑ˶QkÂ=f~%×ɺ<Þ ôرT¸wö‚Àmß:³Iß°nh°gWï®Â^óõókÒoÒÚ¹ÍÊölÖAy¯Qƒ°„*°îaœå«mFGËDLÐdÀJuŠÚâ5±Åú™…ýp²~ÌdžUëy^lûñ‘{5˜ÃêNðèI„ÊŠ”­ë ·TÅùLSÉ)Í^&`£˜FÀ {òŠíu® ƨ¯È¼ªRžX-Ñesh¨½M¢CãCcëG{»ÛÛ›*¼4)%B6­û÷^ò¡’ýSqW^µ¨jH+»£h¹þ­&[?½3è¥H–îß0ëHâøðx¹þý…¹ôéø;(ü£!p…¦)µeþ‘øÑžû1ÅK3öÌlòîØ´#vvWÉvÉÁušÙÀʪšàÈ©†VáõÄAˆmh1ሮ%ËA2‹†·Î¤ÿD­W^lâظ!ws¯{¢–²\à™•n1ûf$·˜‰s°1ºÆhŒËýõHÛ »µ…$Ét¯ ”ÀN‰œÀû¦¦ø6è^p£ÁsE÷šÚ%˜eŠ0K¼¤˜#ïÚÜDÇ–˜WÂ.yÉ›Fôʾ>JûÖ÷­d1›«:‚qWS)º%òÞ[ËP öÈ' ÏQ½GõJ@ ª²T÷Õ#C}ëû×›¨fv†Óý3¿ÁÎ4“$ÝÂõ¸²\« *9ò»¡‚­18Í„½é'’ÃîìKÌ/9bËÜ# æk—ÅÀð˜ sý\4ÊÒêN‡äp"‹é" Ü7²B ­¶P:@+q• ‚]˜•sœvçÉÇ[ äã±Ù$yÜZ>Ö ·¸d/Þoì^õæÕLM{¬º2¾Jà"Io@™AiJÛ02Ü·«W®G>CvÀŽŒq;’ëýöÇÙíAV}jÁ§©H3;P·ec¥&‡HPŠ+„9:œÎ‹¡~BÚ£ÝÁr7DýÐø|ù¼ó¯†~Q9+½Fk`9qø 0—ƒÉ<¢œ%‡8 ðî,0úâÅ¿+µ6ºC9+× Úà3‰Ñ‹™1ûp‘ÜíëôjÜåÀÙeÎV à5óÜ;þâ‡Oü͇'½™gå7r³÷ýuU |gÚD÷6SYí'E¥D3ä{xî˜ve{Âíó>§WF#âš´*ßÜÏŠyeŠ›ë=Äp:'V…ÂkÁ Qnup U»c޵!ŠdW œ9,Å€¸±|jrt¤³ÃÉS;@ñõQ½·§c¸s8k¨«® ]Ú.µWp?bøóiΟäφ ãO!ߺPMÉØÕQü.cÒŒàý£‹¼ÆŽ×á‘qârSÜ“Ç}ðX~š×Œé\0;‚4`À±–¸Ü6¶n¤ocÿÆìæ±––D"ó||Q(ó¬­†ó>„¼¿‡ó^Ñáï—9\NJå­±cÒÓø¦Ù³'Vûù¾p?;ïªÊxäÏÑ•÷¨ì@‘Çí€î’§¥ ÿ #Ù]硊ÎòÒ²(Ð5#À^m8¯Ï+ù–²˜V…W_=( q<úx1  xd¯oq9ü.ˆþm[õ ‰î™Ûº°mafzb‹¾eÝÈ`OwG["ÖÔP[S©ª ý>øãÒx˜Å½0_(”½}éfn?4BÞ~Áâ†si »VÕ¸±”Yk‡f—sAÑ=²Áë–̲‘¼Öì†SÖ5á7`.6Ãè¦-ÙØÐÛhˆGZ[ƒ‰®fqŸsºE9E~ôs_ëÊmº à7sb|¸…„Ì«’ƒæÅi¤Š}Æu~þ#gî×,øñn—Dƒ~W¥»8䔜ω]üê¿PöMá_ÿ p…°wtêm%!&¿Ïr‡²˜€Ö‡”cô¨ö'‚ÖcÈÝôm€çi¸Öbe¶áI£âº71®.©Ên™•4¼ºv?kIÔOy5åèQø¤*0¹å ]Ò;€ÅÚÎ21 H4Ö\]åõ¸6¤€ùdé÷¨Ÿ§ûmÿ#xôyè›Äú6¤÷£“ÁƒAŠÂF¿¤Ì±s}•i¶2mn¬¯­ ·T¶$6oCÊõÒÕ{Nø :.ó‘Åš: X¢g}lÛÉÉTp¦ sŠ`‘`t½ ø=. ºRªT™ìo…¹óU€d+ó³Qö%ùVBmÖ~öFÜð¦:êQdIV0d.ƹ^jv™‹m¤[_+O{¼…7{œsew ÷··ÌNOŽmÙ1ºÃÊÛ6|M2|ú çÓó· ;ºG^Ÿ5yÜp›$qØÉR¡÷L0ø÷Ðò~wû¥ à%û‹ ¾ã:Ͳ>xœ{àS“,õ°¹s±ÿí5üoˆa¥(çIÎÿ–äÌ9øvœ÷¤IÅ#@ŠÌîØ”æm´Ø oÈ6§8˜J/FO¼2 ¶Ä©Öh5Ì·›ýð{L~8ŽËÛáñãâúBÜÚŒ«$übr'-ï·¯„Éb[¶µÓ¸Ì¼´oÞÀ=óþ>ðDZš‡£Ãù~9Ï•_|ý$ïÓõY¿\’Á/yÒÂ/oÅýÁ|s5¦ª´´wκƶÛpûD®yI½-ÛÚiÜÐUÚS ƒÛÑÞoj¬é©í1¼t—±ßò óÓï1ÎÆ1?ÝС'Š=õV+J­ýõ’+éµ[t¬´ïnÑ1ôÛ]y~ûWy¿r~»$KÝð÷|±ßÞ¦ËætظÎL/v¶œ÷¾.™ïÆ—À`áÃoGÚiלKňV„ƒïUã(òçÇK¢ðF|Y¯¾}úÑ‘¾®ÎÖÉy½×Øw‚>ý>.9Ÿæ ¼·øùúô£à€VÈ>Yø×¤¢Â;¤+öìןóï'²ðŽ~)DÖ^þº×^©b©ßÊ1áðo^&KSD€ƒTø¼+ôû»Ñëß=‡9ó‰}¡¼Ïb~èÀ—j~Ï ÌW»LžóËGÞAîRÖÑ?Õüü|Mä\7¥…û…«Â1©ö’à2ЖpàÀ%Èå õpäZh{‡€»–õOåP”L £Oçt÷XÏVW%Œþ]ï{™é}j†.y pî´‡ü$óÏÌOûg€ÒÒï¨1nìh”´Ï¨EÂ.=Œ°wŠb)À‰E>¨4“º`D°Àk3ãeçö XÉÏ$­(ïDì`ðSâD6á%Ç•mÒV-Àú Ÿ¯vŽ×ÁÎàëq” ìîÕGˆ4S«‡ “ô‚ßS^¹žÓ«l#?x à•%»Añ¥`&¿Í¸ÃçóÆÐÁ0OqÌ&RG$Šî,[¨‘÷âpmÆ“6¸ã øIv -äÃÉpëwÈ•Žæ-€ñ% ZèHÚ™¢9±’=Ã=Íe/Äí)û]lò`R]Ã~Fj1äàrG<©2“zÒÏþ§{·Ýé·þIÎóÌ$¼ÁÍÞÐ)ü†¼C=µzEá2^ê1¶öùËéW‘Èü€ÑùÀâI šm½ài #Æ8;usT"·¹¶š2zó~‡ösâ©LgSbà C{Ò¯¢‰¼wxÙ;véNˆ{©GxÔüEl‹Û+"ò"Õ{QÞï8'Ü”p!Ü2ãkrJºSù¢Ú‹ïQПâUŠ>Bë~þ,ÑCÄM‰“‰Jì6Õ¾HlD³iût·q|2Þ’ìôûcÁfq^qk:&¿Jþ‰)xÉ{~þòaë,ÄHQâÀN\N»k‘8‰ÃéØ_2¡oX1¤EÖWV$ºo~n'žä³Jxùœ™‰¥cäË™ÇÙ˜<޽IzÓ¬7d˜øÏ‰B¦$Äx>’#‡2ßg°ßç«Å?ÿ»eÖ¥‘~ôÁÌÇÌÇÀ° Û?Ø‹ù?ÂŽ J)q” 4Lx…–!§»éw¥ßC³ødÉϤÍ™¯òÛ‚ó‘ô?ÒCy8éG¡éé <|+0¬A.ËÒ´.d#m?ó‘ñ½!öP:m~¦×ä~!˜Ë>•Épbh Žá¶yÖ¯7šúdýÚǃŠ_:?ÿA³Ë¬üølFÞ½Òø ¬ÇNñ„‚¥6Íå8\̳§¡ôSð>”W B³K…îD¾íA³4“eM¿'ýèÌGIžžWÀkˆzJ¥)E•ÐrI„.r€’PÀ놈O%U´JóuTᦖX´žÆ†à­ËPTÓ¶ö}ˆ¶$.ȱ¸üAïéíG‡jvLíÜÛÝ5ZÙÐÛÕ½wçúúz°W+[¤i-Êì4|ɯdgƒ2?P¶ŠçžËR%ã¤hÁב/S›»•ýäåÚgD›ýЦ*= ¸ÈLpA ˜§leiÖ_ë¯QD~$s·úy¹íÇ"üÀGVOÎ(²Üª~ÁÃgàE5®„ë•´m¢¨ Sþ IÒ£ÍùåépãRkµ³´‡u%¬ˆŠH‹ù¿§žŒZâ(¯Å•~'¼ç˜x'XV ¹:½î—ûVòk|ïÅo¦ë¥Ú̇™ž|ÞZs1ÃGÈi“ˆý›(´ÒÕ±hàâÿ%Ïd>Á`?°µiÂý¡n´è倢ÀÄ$JêY5Œ¼Ÿ©˜™s6+6‹<¥l•\Zœñ>Cë™tªBN*[Éâ9ç²T/äWÍÊï Úå: Õ°6øY’²v}z¬Q@˜„b"“Ù?bؘ£Ê5d§€%+K ™š•A“ËO•*éûjšÁÃg ½ôê]ÙÕ 3ÐÏ ˆsîn²©2±…™deR,Ì€ü½àO \>ËR#‹»WíòâÊl¿ø,y0ó9&cŸLMlîÊÉÁ ƒŸÈ{^Ÿ~êçïÎ=Oßyñ¿é{3ŸeÏ?KbRýÅOÿüpÀÍ\ ³‘ý‚Ó®ÊÆ™÷7+§è«´*F7~–¤˜ ^oh…˜¹T"ÏFü!#ÆãëNϘ}>ù^ð ùŸbŸ¯’m!0<7qª®¼…¬ÚÀj˜—Ywв’L±»°dC!¦’nãÆ•ƒZ®61Çqvó̦Ö¯+å8Âø<®œ"ï;uö‹ÿþ³ôO*;Æ“ïÞwÕUûÞï¨dã°^ý´-jkc}ÏЗVBl¯Á7{=6ÍòÍUH™ÇPÏß§ÌË/Rü˜GÏïÕªÉøÅö6`%–òB$ÞMø»Ÿß[îÝðñDúì‘Ìd™º!UÍáæ¶'~¶íé¿Â ʬv?Øè:òJ¶r°¤EíãxŽlR,TáóËxY|ÏÖ’ùn‹J¾gF!Š„•ŸT¶†yV^¾ó\´Ôë³ÀÇg‹àÂ?w÷?HDá-ÜACZ,Ú­`1ƒ»¦DÁ*vÚýŸüüÂÝ}ÛKîÞzuêøüº»o»ý®­Wø;õÑGÿ²¡k¨«a÷ÁGŸjèÄÈ‹“ʬ=òkìÛþ÷ÍùîO—×›|÷‘ûîøÛïþôêÇßõ–ÏÖ}¹4ú†²Ÿ©äDŸRô“Š~Ò_K?cš8¿°^9c‹ªŸãöA9vº3šB³'ø‹Ý†òQféüynÓ)6Êx½íyðÏ]¤Ù¬¡Rì‘sYR€ç6u¨ù„¶‰öÁ@úó?ÿ_Oßøfgm}ã3@gƒÒG?©…øYÂU]òÝ[J{ž‡âg•½U³òò"ž·´KDƒþÛl,ÑÂs‰¬¡,ðhÝ.¥×$[%ÚÙÑÚŸìon¬‰„ƒ>‡¶H-NcïEÒGþYÐE€.YêÎøÝ”ýæÒEj•~ú%-ÄèÂÏ’ÔT=t!œ·s§ÞF‰·r¨‹2ºÛÄó”j{p‘=[<,n,Uç“â¶z»E3cÌœØv#)³¸Á —h²5ޙ謭ú½n§Æ¤˜ƒó4©ô“ïÚ Ð.K=Ïoõäw€~Ò¢¬#_Ó¾Æèoaë|½ð ]oÅ­›ð–E…×nRrµ›( £_[•T¶÷(à¸Jà ǽ¿$üO‹±ˆ0=èã‘&s*yes–™Vi65û•òw)‰=¡™0Œës¦qíK_\ ¶Ë‹‹tƒÝùºæf¸º™ÍîLýzHõ±õU‘Ä §±Ü±€B'm¡°EÍ8¿¿  ìÙ—.nÏúÓÏ­ ãÙeÆG|À«¾ùØxbÆðíܺ:Û(Q«Ø^±ZüBŘö]𸖨˜\R *j†]B-Äö•fQqx‰5•¡i¡þàõݸ$E°J!ÖÖÁ6 M{2Ù‹Ç  3 cþ Ó˜dú¨ÿí¦Ô½ßÒ¾Èh¯a2ƒõ&÷è»ÂT•êë$E…)Z‘§ˆ$«07/a5jU9Å”p§¢_7‡µ{ÉÕ°\Æ–šj‰67V·Ö´VUý0ïF¤ˆÏ]UÀ§¯‹wq> 2½þ%¼üÞGFÁÿ—xß(³ÉCð`@ï·†YÕJ–¸Ý)ÖQyz"a¾$DCÑp,`Ô¤Ì €ÿ÷,ý¸ÇcˆÙ‡5a¼üø°Ò ¹çý†ö#ú [ÿ?ÆŠ`Ír_j€‰¼ÇnSeÜæèâ4 FƒñxÔ!öé¥C±=¹•LÁQWA¥‰ÄÊ(+{T(±UÑ*Ü kÌwø<™õ“‘¾‘Ìžµ`ûÍÅÅæ·í¾ì\'I£€éf݉Û,ëÁRaþ¯ŸÙ:¾Ç"Gª„¨snR_€YQ%[ÐÖ±6»çc‰µÕ0ÊdMI®eJI´&RÄž¿F©ÑÎ|Œý0÷¾/YÍìÿõQÄäà7‘¦ÿ†qÿfÖGA9Ç•µˆ¶SQLmK‘ŽûÛ›e^=ã…v4F–ÖeËA­æß™½Í§m=@$ôhØFÛ¯‰GžgnÓl2šŒŠ5(`œ¦w®Ï4¯¹L8ˆKþ¬Ü Õ1ðú±téínõgeËÁ. ¿¾pg_2™lc˜äô-û* o «"ï•ei9ø¤ìG¨­µ RhaåÐ_¼ŒÞûò‹U­õµ}-••-}µõ­UôùÊÄ@}}¢²2Ñ__?¨TϽ4ýÒ;NÖõ%ÂáD_]Ý@keeëÕõÅÃáx_]ýB0{oÐÌø‡}w>°ú¾ÀÃzÇÉx¸‘ãÁ”î?Àà\åꂺ¶Y<ŒžWçÑ#KÓ×­¹Ì¸ÈãÊ.Ù¡¾žá‚Ïпq\ ¸€<ŒšëÄ6K. Ùò—w|„³¥Gré¦Ô…P<$ö¥¿ø:nŸei<½<Øí}ÊVú -Æð÷±µä¦ÌkLëÍÛ•­äÿoïJc㺪ð½oÏf{<3žñx›Å³Øã8^ãÄëó–:«Æn28©Ó%KíÆAM›B(¤)­T„ ¢MEhS–VªŠ¦uU©(´ˆŠ_•ø¨ ‘í çœ{ßÌØž„D¥…DŠ<óÞ=çÞ{æî÷œï{B¦÷Íáì3מæ²w–ßËÎýrÕe0÷Wõ‡‚O*ÈXTÙGkŸçRçÙŒ¹k€6#ÏÑf±è ºÄ“‡u¶¦é»ìª@ÞV37twuâ˜?•ˆ¯w4å)üæ‚諉¨ÄÃæ€•¶õµêµêóЉt@aÃÙ¯é7«cð²\ø7xÜ®lìU4;.r›¹MG`2|ª®zšMÌn÷m1G™‚Rœî/1ÛLM¯OSŒ¼ðQ€ý»ô- ¾2=Åq¸])cv¦­+‹±‚…cvuôuŽwÇ#1O2;éQÚã$7(AÎѵ–Ó†¸Ô=m¹pCYýò| ¢W÷?vhêô|ÏÀÒÙùuÛ†6ÝF$ðø“ñÎP’mÚ§[§mg—býëü‰LWÇ®M¡ö‰@ 3¶{üÔ+'½òÐTeCK¨3Pýþoÿ<<æò6ôÍ›u͵üÅöxrú33³wm‰P;©Î]RÓ‚;‰|z¿5¿ÍtàMi,c¡‚5áVE¡þ‡³ä!+€È€òHW‘ цpc˜[òÿ‘2äþ‘}ZÙ©K¬](ÃoZ{MÄ2;ê £Ø9N’f žèÖ“üÜigäÝ2ÂÉ WÇîT“]‡Á @Oœ[õ²“˜j4 8ñNw‰+Ó˜Ñük’†~+SY¶†âS^ŠÄ“‘8q2žûyöi5uëçª(€ЬuØq곪Œü£Â7µP©&ƒâʘ¸ÍF7†ÐGì8ñ hòš<¶Hù×tÆ•_!e'¡eêFÂaVãeÌv+­ËöBèqh‰`O­ØÔ»¡;î%ƒ4U:ƒÐMËU¤™êE¯?ì•=žbPs-÷D,2ªB*eë›ZÓøáÉæÁx]YÇmw…ብÖw‡«±–¿Þ| ºh»ß©¨H†:ãÛîÝßíõm¨rÌdÔ¿Ì~éH|«÷:g÷¾îÇC'ömôúÕ{£ÃÐß…Ù{^ÿ|¾ þž<ª50;Ú Eó5‘‹<ÞDáµWñµ¹ŒTç_©ðWá" u9K,?ö)ƒ|¾(ò‰a>€D7˜NÈEt’ùÔ  hkG¬FŒ,=¬ 'Üjç䛌é„ß8Ê£±DZ—|Lȧó…Ÿ¯Š“j[ f4š[a¾v#U•¼‚—Gª É‘áWO¨Ì£¹>ƒ–$$N›)éiKź\d°ÏÑv ÎëôæÍä]žçö9®žPž“zŽ“dÄ9¸ì$zBÑ£pßAn&6¨“¸Õ¤8e1™ •q`·§Mà÷ù8O7Ç"¾°¯1àç^^-<÷@=|2ÿ{(,ؘT½y×5r`VN)‡¶Ìr$‰ ü“P¯i©÷$éÅ;ÔqwÚ@ûCøô0ª@–ä7RA˜µ…*ÓTm6ŸN42k8Ž–Ô%%kl£üà3ä‡w¡Kf9aü.dXγ|­îg„ÛÖñª;W¾6(”dÊày¿$ZHÄBÁÊr§v¨mJ›Î÷ï€zÿ]Öûª7Þå5šu:ÂI€ì¬ ìÐ\‚8¥×Áî†ô›¤ìÝ$Û.‘Øèê„åoN ¾õ¬êúM)ÓIõÆ;“Qô§•2òÑxD.äq’ 1@ϱǔ5uà­õ²7Ò$à ¨Ëï¤î *O§ØIº‰yVG˜öÈÀ\ȸOÆ#’ ÷ È7Hùƒ$çÿõf—æ …‹ïëq{±9 Åž…6ù')÷,Éu‹]JÍZÎ,°)E Z‘8‘ê ‚9 ô<zœRϤ§G0gUêD=†^µèkOÞµÖN؈Ð- ä üç¤üÉãYÃzX;B1”DåZ±Û³ŽcÐu° ¬^1\0©ëg+tõ‰?7utj¨RRjCïEu7.ÄUPÙ­ ÁTÓR•ÍâµëwK}¤³_2ƒ%5òäßã ÛEúñ¬Œ—A¯Ññ¼‚}úÌ{RþsÔvDtfŒÎĘ+:3`EgÂÃËDgz«ÜNC/àw «'´›¥þa*r5˜µ‰ Lٌӧʰˆ7=­a÷aõ ¿ cÊ𳢞zIŸ‰RnO.Å¥ZgÞcêuTÊÂgUO}€Þ›§ø}WD„)w+<àw˃B¡uø°ÖÏŽI¦ÁÊH½ŸüzÅHVEÎk|ÅMU."/³ƒÎ,{‚oþ¬æ#ÔÍnW?¥däxŸÁ†âx(®x`jåÖÔZ»b¢•w@<W‹ò 'ÊëЕFγø£Ý¶&"kÕlËsµP³F}ž¥Ù£2ï*'ˆ‡h\+¾iâ›Ì»Ñ w]STÕ'hŬϖ¿,ju˜L„úòäcù46&¨ƒW$‹Œi¥š"UÉJ[uº™{ºÛ`ÿÐc‹yagT£!¾Ôã®Kó+g³ï…Ú†&w&GnùJ÷×ëkÕòžôà]‡w××ͺkh¼o4±cr¨­–ÏŸ}û™ãõçø‘†ÝÑrôâ#\<ÚRøvýÄñgÞ> ¿K%ÚC`çÃǨú¸àD¾8¨}EÔÈ©§Ñ$`_Ñ9KÅ’uv&þÓú‚Ä9‰©óƒøWþà­Ü\ÕÍìCð7Œ÷|ºú<ò9‚PHw‘±*úÆœvÅ0òßò6G‡´)ã!Q8’±^°b·æ&=ý¼ÂDÌf”¨•¤;J3ÃÎìB•äe¸Ý°Ù3%DbN¤Fø_L?ÇvÍ(+•œBñkL3•07››‘Ȩ£=1œ&[J^’lûemù*Ùr$ûÇÿÛrµ-'ÆJÚò5´¥qÏ[Žf‘ô[Õ÷g_̽ÆSË`F'®SÜ“ê„à<8/'ž—n›«pÃ\Ë]»™Ëå_ÖT»‚b/A…»´ îvK%­"'|*AÆúÕÊ›® Ê\ Ì]î¹’Æ4#££ÉÄhftïõÓ[· lìíéjokiNŒ$GȦ•’7ÇIc霅iDcöpîuv-iXþJa‘‘.Úc#ÔW5Bl%õŲq‰Æ^¨T-Óá)l§K6³<‘H´&ZSTÊ2¼¿Ïž+YÆ‘lä¿\Æ–Båœ^5_¸åy\ˆ1©+”’*=ÒDBy]¯Ð¥²—Ùƒ¹ólÐì+ŒèƳÙaܰ•Ö“‰žîÎöÖtbSr©wÈýaö-²çÂ*{g?™{ƒ=¹v$jû‚)”­V5vâ]Ç´`¹#WNŒ¹"³•‰Á¡¤¡=©ÄŠ‘ÀNk¹¥ìëúFq¾¼„K‡‹ÂŸ·ŽºÊ2/¦ ‘TÜxvJÌᔽܣýQ•]Úz.wI/sçžO’­_d¿È=ÏÎ,ñ€zÔ⦀5 ,hŽZX iaÁJ:72Ö1Ÿ¯¤Œh®®Q^Ì0zvÞ0³}먹qúÖdGª£¡Îï­,w9пÓ-ü;Ûsïè}¢Ü„ýOYîw¡Ü“愉-ú ¯BÑß4ãäòÂ7æà…SöH¸Ú?µ:Ö'âá–HKmÈßXÝ˜Š»`›¡°±ìËÚ¬z‰õ³qö+³¢ ’‚Çe]𛩫-u³—q{¦pì‰Ê’3˰Z„ÇfÀ]`p¥ §–ŒA\0W–eQ<90‡ÆÇã‘–D¬)uÒñ¢CóH›*èá×s\«!ÿ£a[{|žìÜÐñ5(Êéè–ÔOÜaêî€GÙm4~¢·wz°=Ðà™l¾uq©ëÐùÓÛ7v¯f„즣žì¾@ÇVÝî¼ó;‹õñjûäm‘P8T½¿ÝLTŒ?ðÆ©Ÿýn1!üSǧwôÇ, 8õ'‚G1à”û%ßqBò?§ÿˆýz¹¦¨ívšZ_1ûqÁp+[¯èÑ×,¼Ê†¿J~ž0"ñ ú÷Þžµ8|nk?fæ.inQëÿSEÔ¬ÿ¨ÿ,.ÙüÜØ¶ÛN z;A ­Î¹ /xM2„†×"ú+êk—KŒqqõuèĉp ]ÍÉpc]´>Zô×V× —NÑ¿³4?§Vîã<0>ÏeoÇS<±“PPÛ<ñØi\‚h¸Tk#SÍhë¿æ­ÀY ·Å/2b“â5‘TT·ÎÜÚj—~•>sEÇ›¼íâÑÇ yá± .O°DtVˆ‚w§ðܤ9| „°‘«Tר£BJHŸÝçd> >«ú>ªë4§/ö¬ÖÌ/êßgMðYÑ÷C.ý¢EVÑþv>(HU:ý)~–Aph3DP³0/NkÍìQ©¯5aÎÎá9óø2ZZàQd Oò¡$|±øYÆt©ÜWåròÒÈ_è#Ö¶HñWõoH»¤À.C0~õ¡vni÷îÂ<+Žƒ©Äs€Å¢G™‚‰„ð9 ÓÑ£R'ª>DeþZ‰+ÃÐRJPèÅϨ—p?¤^6–{Ç6mœÄ± –9QÂÛVõ3Ê| »`VFÀ(lcbØ<èÉ$£F*ÜŠÓÅ\¸Ñt›®ÙŽ„£qÈÃ+˹ËYéÊØi0Å_À7VF±¸x‹£š9h‰`çµÊgÌÆ;w.ì\8xëMöïËìA♘7‹§šR±*ä ERý¨Þõ:†õÚ&êÅ¿ †¸÷¥¾ú :œÕi²Û=€9v¹¸Ã`9 "m:ñ\†®_âV2\M^6]Ælá|dÿÈþ½{fvOOmß6y7ùpOWº¹5é¦Îƒ¨aÖ?ÛyëÆ;þ !éÁxÚÕTÍnÛFʲl'ŽséÁð!ød¡&)YR‡9Ù†Bl8s)¬DRbM‘—²"‚öZ è¥‡¢/Ð{QôŠÞ ô=Ú7è©ß.7ª•T *b—ßÎ|ó»CÑ+!‹Êß—X%¶h§W¨J_¼DÓ7Wi~2x™>§_ ®Ñºuf𠵬O ^¥Më+ƒ×è¾õÁw¨cýhð]p~7xö*KߣŸ+ƒ7h«ò2±ªkHtké#ƒ-Ú¨:WhµúÄà%º¨\¥Íê·/[•ê÷×hsùƒW(\þÅàUjÔö ^£µžÁwèÓÚgßç7ƒ×É_Ù2øžõÅʃ7¨½¶z˜fÓ< Þé×y¯ÑlØØZÜ›r1=Kc‘øÜ_ yÍG×Qà_G}žDÅÏäWÏÇiRð31 xûPäqT¤Ûw£~Hhljä\ ~þ´Ë§Y”†°ËA.£4á¦ÓtŒýAû86ZRèzFj+±×Üo·¼çm»í4ð´÷;m£g¥¿á æ|¢é±ˆcî ‰Ôý@Fƒ„iêï²L‘t?e©ŒŠ@òHL¹p†žÇEŠq‘#$»sÕpöÖÙŽùÛ9wlŸ Yx½flÞ>Ùfû%ÛáÍdß&I\äÂF"¿„ÿ…­u)Þ9Îy–Ì#å5 BÑ8£(žª’qèFW©ÃâÈ»îd2A˜L‡qP½»H^ö uËè¯ÞFw?dhv…©ÿkó4‹)ûy”Ò‘Qì¤ùÀ==îÖÅ::¤”2šRN hH1íPŸêxïQƒšX¶A-Èzà2XS:ƒeL‚ò!éÒ˜.q’tÓöˆhÔ»Éïþ™Î!—X9]iÓ1<%:ò3xAÊ´Ì8±¶Kqvtå-[Û±Ž¯¼±ö¯lŸÓS0™NQ™âÞŒ0ïa’ m-!O5·‰HjÍÇ?ÐÕúFÛ@/<Ó….Ð<מ±=p÷©­Ù/Pyº6<4ÌjCÛÁ>oÏ3û¿Ï­Œ®Xøu°ˆõ m¡ºîë^«›UÖxI!SUK ²Ó}2ìRÇW ‰Ð7ÝÓœ”B<žŽSà ¡ë\‡°IÌÔ\éšö類[góH#u;h>1“Už^c/ÑDßú –:½Ô{¸°³ïv,ÒY _Ž·ª|¤9—&ÿÛO­sk‹Ö.ÎYjÿåWÍrUÓ«:Ý×wêo#‚ýtvËå7PÎFåW}'C}K=&ÏD?ŽñùW5޹{÷Öü›sUN¨;Wû«÷jwÿ³š[WSÿþ?½_§„êm™GêYq`•£ß.¢Ã{’°û¥,ÏxÚc`fƒÿ31`6^ÿÿxÚ%Ò7LUq†ñï”ÿñÞ×–hPŒuA5–DŒÈ%ÀÞ.ì T0–è¢Î–Á²Šˆ`I4ºé¦«í‰Þ“<ç—w¸¹7ùÌ3³öÿã[Û¿·‡òé,+@…V„‹­¯·8iuø ÕãkÆ-vßç ìWižWå§™ç§ûéø“üy¸È/ÂÅþ |Ó¿c¾7¸d^дZ´…ûÍ “a#m ÏÐ aí »hOØC{Ã^:Òwá'ú5üfž3§).…f¸ šé²,p“].pü—p –*·׺fÚâN±œvOñ3÷Ü|÷½ÂýnEãÌ‹ÆG,ˆR#þK”MÃ9ßåEytz4ƒeæ°ïæ û Ì…±[æÅnÇâGñæÅ?Å?[ÿÿ‰Åã?ñ?æÉäY _>äp¤áx„Fš¯Q…Gk ûX¥â‰JcOWK¦²X&k ÎV6žªœ«\œ§|¼_pRI\§ƒøáÃ:‚ëŪAGq£¸S>®ã¸Y-ø„Nà“:ψ»ÒYÃçu_ÐE|IÜ¡ZÕ†/ë2¾¢«¸]í¸C×p§:q—ºp·ºñuÝÀ7õ?ÒcüD/qŸúð+qKê×k< üFƒxHCø­Þá÷ú`Þ_NAŠ”xÚí] œÕûÎy÷×2ÑÌ0ÅX³…²ïK¶ì-”5D!»lYÒª_í…PÚè)„$EZd—!d'Ôœÿ÷}æÎ˜}î,fFÝ÷|î¹çž{Þí,Ïó}ÎùžsH‘K“EIÒzŒ<€‚û îÕŸ*¸è@ªD‡ö ‹R0‘R$‘Öô…Y¤ãwô/I6…’Ö¼mÛfT´}›;ŠR™Ží[¥Ê¾ÿ5r8$pÅèrùBå¦<=ò0µeÿNöïaÿöû²?°¯Ái(û£ØÏþdög°ÿ û/â±¼÷JÝ~ùo×÷«‚÷&xŸÜÈ“BüÑ¡èïñ·ò ¿§Ae¨-~Eê Hõ5m¥rtR䥪∤²D´yÚëô„ö±¶Ÿ¦ë·èÕèS½¾Þ‚Vë/ê/Ò&£„Q‚¾1Öki³qÀ8@ß¿‡h‹qÌ8F[SÆ)úÞ¸lüM? E³Ý¬bV¡æ­æIúɽñ‰ß&’>s”zK}‹6AqÚDx¦<Ó·ê°Z©à3^µ¥{U{õ˜Ú«6«Åj’ŠûÌBËÛ†úò†z”òª‰êÕU-§Pü¿PÝ¥~PÏ]“%±Q½£f«µ±¾¼Toù¾ßIâü§f§zåm~Ü}íUz«µþÜ=‡•ãýr“I/õuo@ÎgþuAôåp__TD’õ.Š 7.A#ÆáYIûÕ°ÑÚU‡#ݔĶô~‚¿ŸãžžÆçRlIÌOQgD—D$Ò±\9î;k¿:Îð[彿J"˜}+N.ÏŽ–þ]ZlŒšž‰Áìlîá–R³:¡žö7S»ØkìBz—[×o¾˜=j§w­L8¬8è" .8QÝK|”AªªpA™cŸf‚Œ^”û…%™î­$âÎ'™rWŽÒƒ—ÓlUeד~äo:™„ȼçxÓ“wÜ[|Z] -…”Ç`+gý‹z¢¶$ÉýK—\›ø*[%r <©=c‚£AŽ( ?{l’² ’–Cjcö•pÝØr—¯hu1™”çS«{Y^~Jõ$d“W4²ü•Ò~Ç¡³ýµ:}¶FÖ>Û™ÄÏê¿Uèjåp=q)QÔíɤ|*ë$M-&p¤ÁÆ Ë˜-”ý=¢êbŒVS;®¡’È—t/¸?È[}ûæ ¯¶Æ””g©fÓáxýÑGÔ€ ¿Ñþ´ÿ“ÓŽ¨™j‹úYòºHaê÷«|»¢äë•‹ËwŠz; yÞúß*¤£6dS»ŒcD- (Kä¹ì²"â´‰¥r@.üàWUæ—Ä÷W샌·Ëäß!{Þ.]%±/ºß ¸ë|V¢¿¸}Q[-9Ý·Î}%Yyß8˜&ê§Œã÷´ÿ“ãJâo8‹[±ÚeÌ8êç@‹@޾…OVJ§8=‘Q{倩M÷¼Ò £öÊ9ÿ§‘ OÐ/Á(4QË$ÓõRß$Š,ŸÊÅËfkæ–¥r±á›’ISÜ÷v‰ä‚ú@6-œì™=³lúÅ Ÿä¯ÄÜÄ«i1ZÞ³DˆY6'}gý̘9£Ÿ¹S¸{ Ï”-p/Þ}¾7k"‘7HmHÊþ‹ -ò1ûW'{í$´œ—à÷±¤%ez_Rw÷ÆÂ“M‘#¯~•EˆÆßû¨ï¯1ÁäÇ;ùkIUÉI%›~{œð¡+~¶kÜ­@!ÿ‰Ù€ê‡á;ý–£žÆO ë?ŸL»æŠÄÍ9%‘’UéÓ†Çü•l apÖÚI°ܸö@ª½^ʈlÌÿ\Q»ë/¯_0F‹-M‹ýg[\“*š×„³¦E}ïBqPpÔ—13Ync\=©Þâk\…ùqêXÜZ“óìÊÿQ £VDm‹ú9ê˜ ŽÚ¥ÚgQIÔPzæ]-êû´ÿ“óލUQ_©[Ôê:• %Emˆú%ê’JW!êURåQy2XIŽEÄΟx*…3»yÜ|ž?18eŽzû ÷æOð|ì„GôHm ‡‘’È™˜Ù%i/ÃnžÆ%±âÆ’ÖÎÆ7lzÍ•‰“™¬‹·ƒ5YÖq;®RýFŠbPb–}Ÿ™<×Ã86?m6Œc§§Ý¨_RH»GZòvZ©FªÛ÷¨ª¡äÍ"S#©Dl?¹ë;g(âo÷õ}©WzOq%ÏòÚu’ó+ºùä¯ÏvQ+ÉõÎåßA™ø^«£ï®zªEÑ ü8½¬AIëXtáj r"9û:u”š7»ÚNJ7F¾»T±Ädjë•õœâĽ¯¶dMºêõa?­Ž¤Î][[÷Æô÷íóÆáø(Èß´já„ýªÉÉÔäCÜ^#u uͯvÄ ¶„³…2øV»’¿{Nµs¢Ÿ8f$966™•R[A îû$ÕMº:îUÔΫôV'ÓÚŸIk#äôv÷XŽzšeôŸ=ÔÌõ4oåØ|JÀ¤ˆîŸµ"S?#*O*6>w$kGŠöã}¢G‰R™ÙÛÇàfõ3&ûD‰GRŒ œK‰ÆÿÜk Ó¿òH°HvÍl ó+UÓÊŸ^€9º(ʤ «²àÞ*Ò¯SOÑ¿ÿÈÎõH®,R ðyÙ[o.Ñ?/S~á€ï5ùcÞJ-@ÅžîýŠ=k oœŒuÖÊP/À~)áÀ3ùÚnpêã»*é¹>×X ÉZIê¿V*ó°'â÷¹·ÌÒ›G äø5š]ùß›’(‰œ}üZF@cÿÇð@üWõ:®.¨KäÐuêbvrYÒ¿Ù¿¤$~¼’(‰³±ãfYÿ$‡ÿã%3#¯MDí KƒÿGfr&‡%±×Ç0 bk8.jÈå•NàȲ’8èëÁr)H]âUcÚBŸ«ÕŽÌ)‰?Ô…@.䈒ˆáHœUç£×iø}œû­:®$d^^-÷Ÿ= øöã[ÁóÔ£– lNª3ê”ú %p6º?¡S8®æQØW#Wñ|ž jküy ±œ¾]W{å“Ìe}_ƒ‡¯O#¦ÆÇŸ¥—•½ÿyéwoÀcÐ {“Öj`Ž,ER¾5C þå5T½z>gv÷vËX½)æQ¤‡5R·^Çì™êí ‰œ†dՠɨ³—9ž¼•ªN'{Yý“ІDIÄÊZÞ7 ¸Pý¤~Ã'0o¯ÆüN‹=8ü,‰ó~JyÔ)&ßcPÎ9ȹ«V"?û³Æô•öãÿÊÎ#%q‰ r!Ç”F —îßuø1.¥1øx#o™4kó?:ÃÁŸ’ØœâßùbCå3©$ÒŠºóT‚£R ®²FZqíµãiI|˼ø%d‰}êÏq÷·$by/k6—Äfêá ÞÄßþìØU:· ¡’|O¢$®¬Ú™%{+]YÁÓ[Z=©–$‘f±ïûÉDÿ,õþ‹NZÇâ¦ç=qþóã ê)µU RƒÒyöÒtÜï©@ËK2gžGI WÃ%8rîõFŠuj›ŠTûÕ)Þ³å2¯¥ºZ%±=Å¿]²ñq½uCQ†ëÕ,‰¿RüÛð•CôžÞâ«©Ce*m"Ží} î'z-•DHЇ£=yû¸øøàî5±"ñµY¡)þIJ‰¥“:®N\[;a]c%Q$ iÏJâ*–D ¿rß`=qŠÒ°úz޶~§Ì(Á«z ô;ep ³ÒqÄ_w}×}m$äO¶Èþ«±Cø5XÑ呵%¢ö¬“¤Û…ŠÂ' û”»‡ç˜² â#ËJ"°fcÎ>œ4ÆŽô’ÆQ(Ü@* '€kËFuàòÐCp·ÑP¸q4 îVš —›¦Á§àî mp­i;\cú ®=ýל~…kB¿Áµ£]ph7\3ÚבöÁu¢ýp-èÜíô;\Š„kI‡áZѸ¦t®3ƒëJ ÎŽpè‘[ä¦;E>‘º‹ÅtŸ(*ŠR(.ŠÓ½¢’¨D÷ˆj¢ÕÕEuj(jˆÔHÔ5éÑT4¥²¢…hA%DKÑ’ú‰V¢õmDj ÚŠ¶dŠ{ŽTAÜ'î£Úb HuÄ`1˜ê‰!b•CÅP*%R1L £Åp1œBÅ1‚úŠ‘b$õ£Ä(*&F‹ÑÔ[Œc¨§˜"¦Ðubª˜J¥Å41BÄ“âIÊ/¦‹éTCÌ3(Ÿ˜)f’-f‰Y”W¼(^¤ÇÄÿÄÿ¨¢xI¼DcÄËâe%抹TIÌóh´xE¼Bb¾˜Ocű€FŠeb•ËÅrª%þQy©¤¢›5©I Ö\Í¥‡´BZ!Ê¥ÒGQa}´>šÂô1ú*¢×Ç“¡?¡?AÕõIú$ª¯OÖ'S5}¶>›è/é/Qýeýeª©ÿªÿJõúN*¤ÿ¦ÿF7ê»ô]®ïÖwS}¾‡Šê{õ½tƒ×1GÂô¨! Ià ÍÐh°¡: 1L䡆eX4Èp ‡1šé&s9ˆî2G˜#¨²yÖLÜ€ÖîI¥N¢(Œ˜p)SDAØ“S­D1Q RÓVíE Qñ7‰› )J‰Ò—e ×ÊŠr—å!Ë*ˆ )"1EEj'n·ÒÍ,ãr±ŒsXÆcWDÔµ!ãêˆ:,ꊺ×õn"š@>z07KÀ¢,ç±üKÀ±¢hÙÔAt€$ê$:Ñ , k°4!ú‹þDž|‘åà“,×°œÃð+–€³XNc ø%KÀ…,‡²Ìp4KÀ©,k±Â°*KÀ)bŽ˜éæÉÁçYZ,Ÿa98›åà³,Ÿf9h²|åàsâ-ñÍo‹·é ñŽx‡ÞïŠwém±P,¤WÄ"±ˆ^‹ÅbZ ÞïÑ›b‰XB¯‰¥b)½Ê2ô)–¡ÃÅ!qš#RDÒ»â°8L-ÄÍ$ËÆÔD6“Íà7—Í©‘l)["ÜJ¶¢pÙF¶¡we;Ù1wÊÎTHÞ%» ¦«ìŠ»Énˆï%{!¦·ìp_ÙñÊ3D¡r¨Dk•Ãä0(GËш+ÇR=ù˜G“åD9‘ÊIr5““åd*,§È)ˆ™*§â¬rµ”3åL¤œ%ŸBÌl9›ê˧åÓHó¬|1/Ë—ž+çRùŠ|éçËùTQ.ï⎠å"œ»X.Æ›¾'ßÃÝ—È%Hó¾|ŸËär\áCù!â?–ã>‘Ÿ æSù)®¹B~†ðÿÉÏqåUr®°V®ƒ¿^nÄ6Éoðïf¹ï²]nG̹϶SîÄ}wËÝH¹Gr¿Ü+‡pÍ£ò(rø˜<ŽsOÈ&OÉSŸ“ðœÉ¿h 'È©ë¹æj EcÀ75“j–f!œKË…óh×Q -X FLA­ 5eØ@+¬†v ׊ }Q­(Ò׊#MI­$üZ¤ì¤u¢úÚ]Ú]TX›§Í£G´ùÚ|*§½©½IåµÅÚbøK´%ð?Ð>€¿R[Ik«´U4AûRû’úk_i_!¼N[‡øõÚz„¿Ö6ÐÚ·Ú·ÔCÛªmEøí¤Ü¦m£IÚvm;Â?i?#ý/Ú¯ïÔ~£>Ú.mÒïÑö f¯¶×Ù¯íGÌí Ði‡àGj‘ÔW;ª¥îÚ1íOÄœÐNP?í¤v1§´Óô vF;ƒ4gµ³ˆ9§§ÞÚíR^Ö.SOíoí„£´(ê­K]R?(n“úê–n!lë=¨»ºK=õ\z.ºOÏ­çF|=u×óêy©—¤áßjz5š ××ëS½¡Þî×ëéqýv½bZê-qvz;ꦷ×;á Cõ¡´V¡ ‘ŒT,F*Ï0Ry^Ÿ O Çô‰úDšÁxeã•™ŒWF1Ry”‘ÊtF*5©Ìf¤ò,#•§©˜ŒT^`¤ò#•ùŒTÞ`¤ò#•·©¼bà ×¯,`¼ò¦a6½Æ¨åU#Ä¡µF)£57Je¨±±ÉØD mÆO4Ð8` &ÆAã }aD‘ÔÂ8n &'“ðO§©¥qÆ8CÝŒ³ÆYêg\6.Ó$ãoãoêoücüCå e(*gâ fU³*µ4o5ëÐíf]³.U0ë™õè³¾YŸî7˜ßÑì‹øÍ~ˆéo æCæC5‡SÆUÍQæ(\s´91ï™Ë¨Žù¾ù>ÂËÍåðϘgèzÆ^mÍsæ9 °P øÇ ·Â©œUÔ*Jý­bViÄ”±ªP«¶U1m¬öT—ñÙD«£uÂ]¬®Tˆ±Z Æjã¬{­{ÓÍz~k½k´FQ~k´5žnd$7ˆ‘Üxk¢õ…Z“¬©nM³¦Ñ#Ötk:üÖlÄx8ïaë5k!5³Y‹€ü[Ÿ#¼ÆZÿkkÕcØÑÚlm¦Ö·Ö·TÞÚbmAüwÖ6jnm·¶S?ëGëG„wX»¨±µÛÚK½¬}Ö„ÿ°þ@Ê£Ö „OZ'‘ò”­Ñ@[·]*lç²óRsû:;üBv!šl—°KÐ-öMöMT…gu»´]šn³ËØe¨š]Ö.K·ÚåíòÔÁ®hW¤–ömömTÙ®nW§µv=»>üöˆocwßÕ¾ŸZØØ=¨¼ÝÓîIýì^ö£ˆfEÌcöcˆgCŒ‡hØì'pÖ${“í©?iÏ„?Ë~šêÛÏØÏPWûYûE„_²ài_³?‡¿Ê^‡4_ÛÛ¨©½ÝÞA ìŸì]ôããÖö{…ÙûíßÈŽ¤íÃöQ„ÛÇáÿiŸDÌ)û 5´ÏÙçí‹ÔÐ)è¤Û0'ŒB›æ´Üiá´§÷ŽNGjé p¢/œÎ@jê<ìŒCx¢3‰V;/8/P çEg)}à|à,§Çi’³ÒYIåœÏœUÔÇYí¬¦œ5ÎÄå|EœµÎ:êë¬wÖ㾜 ÔÍÙèlDü7Î7¸×fg Â[­ï|O½ÃÎ1jäwNÑ»Îiç<îxÁ¹LM]Ó5©™k¹5vסn.7âs»yh²›×ÍKºÁn0Ò„¸!HSÀ-€4anÒÜàÞˆ4UÜ*t»[Õ­Š4ÝÎð_s_£æîBw!µt¿t¿¤GÜÍî·ÔßÝânÁ¿[Ý­ÔÏÝéîDÌoîoô ì†ñ°Ä‹—ך ‹{*P÷@ÏÛ”Bþ –ôn â}@€}#vÀB>G X57péÀ—ÅaÿV&¬,Ø(¯%Ð]ص÷Á „;¸íQ ¶á@k#ÒFM*›46(l&Ð× ®ÿm½ ”5èj>PÕ2 ¢¿¤‚†uµBÉ£!‹ÇCúN‚Ô ™û2díNÈØ]­{ô½§rTƒü4!1Xvƒ yÎÂ’« q7¤Á´óyÖ7°ÒJÁ::'ÿ×6M~qæäÈÜgêíÍ 0lÒßãyB÷±ø‚ÈôC§;=6…‡Ûbò$]ùè±H]_ äaúóñ\ìXb@:fÚˆP@>¦;=.l476ÜÇée œç¹(Û sN⨓™•³«Õõ3ÜaµU-U‹böÝ>*Sƒ£0Sò±e`¿ÕL:«µd–Ü<}tQý [òB`Vdæäc 7üȯ¥j5oM1UkµAuƒVï¦æÓÍðÇr.A-ˆö½U3Ô®”gÔ¢†¶SS¨Œª‚ïHäçTžgíF÷¢önP"&ŒŠ©WÕÇêQª@%(7å¢ ÕŽÊàŸ’T áV°Šq5USpæ²ðon\÷S5W,„{ŒJ๮•õ67žôÖŸN%aÁœaÍæ\[ŸA°Wò¯-¨M©1 õ/L=¢¼¹~±ÔšÈUsQ·©±;¤Ç¤Ï£&ªÇ}g’ :­6á{eügÀ'z¨5q—j¤VÂ>ȃ_8OÍT·«:¨— ¨-Õ¡¦¨Ï `•ÍVóÕÏd¨MlE Bª8s$BÙTƒaüš€éüzAR.ã³ÞkQ ó·Ô³Èao­ Ç;S ‰^m%ñÛª¹ìPxO£ö¨g½U:ðYˆ\|@­§!ô8Õ¦N@·Ý’¸ÇB5G­Wo ,×CN4Pkñ^'³8'WÆì5‡ú¸6Ť)¯ZH¥¨¥Ú¨zR0¯.|Ê+HÍ-¨^l˜àê÷¨U¼CEœIÈïÿR±«Ŭ´ØR­Âu£Ï¸×{u>ë9OÛ©;¹¤FPWê‰Ïýñë3×…Ÿ¹”–ÅÏÒ\äuYâ`Ä ª›â ž­ã¦«õD[IÁ î}Äë{Jøïµ°ŠZ`}›LêïÙp¥¿'ÈÇ•ÌÑùxÕFzWzñFŠý™‡ Éì!×HõCv팃:èÆ·g`­,Nž™ßž¹²Ú]AÀñ9¿Fîõcv[Qàð¢¾ôóÓ*Õrõvžokú%¸ê–ùšSí×[o…kgíM¹"¡]˜ÖU 3Þ§áÝѯgÌî|ŒL€ØÜóñ9öýÞZÍðvKã{<M°@ÍI?Æ?çÜ ß.k‘Çíe`Ëxk•H¼N>ÒDrOË1¾ÏÆèš«~÷vË¢]^‚`Ñžàµ6,29& ÖWæÊÇ“ÞN\)¼Ò‡ÏŠ Š»vÎ3Õ5MõKåÇ‘ê$çÿ‰$䣕uÈgYd’'Å|\cKúÛ3§¾V{õÖÛ)5UML y{êÆìaƒ§:äõè©ñù?ÕVu@ͬMÑR*SµÄå¬>@ªOPëÿ–!ê0Òz˜©[Žk×”½íÚ³àsr»VK}ûnÉuwtÏKüøãǾê àǾjr W¼Ó»öN£¦ÎñÌäãþ8¼¡z%‰|\ìo>ÆžóºzG M1Åvõüÿ]Ù‹V½— Kv³.¹:ªöR4ÁŸjdé"5 ~¦2Ô_ÙA|ÖpènõZ2ù¸KÍ,-¤º¨îÀîeÓx§9q÷ŠÝ™`™ï»;>]ðù*úix÷Í]¾gJO>f}}¼VÞRßó¦<”xŵ›ã虾i¼C_5(mRõ÷kŠ Qe“²i’³soŽUÁ¯˜²ÎÔ|üîJ>&{¤1UsäÞçÑ{f&—ª—Ú“pŸõ¤F‰U kÒüßǬë}ù¨¦Ã-W?àŽÕìhí§¾…¿\½ ù³L­Rß@Êì¤^jÐÁ=žöOs>Žã|\íw>¦K@ ʈMƒ|”tž¥nw§\3‡ïÚÍ|ŒÑŠaÔ “óñX*»¹Fãðh]ãÆÃáûðûŒš¨žH96Ïx{çª#ñ¿/³@ÛC"> ùjøêØ}‚SÌÇ™ì÷»"axL§´y’ò6]Ÿh½í©š¨Æqõ ‡¿`¿ûyíO³ÌÃŒj(ôàuƒÊ,Ö•†l Þ2½ª…¼W±7êÓ©îQsQóÚÔHª­†¨‘ÐÏÅϧÒÖ’Ë™•Þh¨ß©D}<¤~‰×ŸX4yûÚ«-Þ¥þ¯®æáàÄ}H1÷ãëùvâëF£ÙýjwÌy±çV¦n°´Âyý„$4çé)uˆ#Êä ~.iö£§|hý«”z_P/¿HÜ›w¿ñh;%…+üšà÷6õÙÕèËR?¥zöÞ«¿~æ#§øÞë÷j{ÒùÈ1 Ê—æ§TÚÌÏǸ{3'½ï_tÍW;®`µrå„?ø(y¨¿Ó”«ÓÒ˜\>ÆIçîêlâ}¥QÃ÷'_ŸR)ƒ­­;W±>fr>¦z… ¬ê—}ùˆZ”l>îR‘@1«Ó²†gôæÐ~»<‘7W"a_S´ ôñ[bõJÌÎç¬u¶%§Ábüdï¾'õ½ÖӚܫä͇Ӣ÷½ÌEò;?ã­)͹ÌœÅ`æÕæ¦/õ1sr5P¯^Þ‚ëæí翽œÆ;ŒƒV7O8Ÿû)’ê1š“ƒòñhß_<–Ï•1—˜ÞéÏKƒÍšë.®íêÓÓã„WÆŒ£¥aè ‚âîXëÏ{dbN®Vß'ˆ*‘é÷Ø›þóü²&¾‡eýäÿ/‰Þåß 3ÒÌþôú¾²éYÿÊØ“S¢Þ¹¤øfˆ—tM–×ßLÍØ‹¶¢¼S­aã†G÷mï›ÞIÓyÜ| ´½8W¨ßÔÁh­ªIç£Ú¢º«YTLµRýÔOTE½Ê{ny½•S·Uw*Æu«r\›™ç‚¬ˆîV­bF^ñ4‘¾=uá| ÏÐ;Ì„|š=šš%õsö_!‹Þ4M}1iÆÃ3š*üšÈÅ#)>Å•²ÓpÝÿŸâ*•Íù¬=/wI¢§íÊìhuaýä ,wõ[EZ$žúî*Ü?ß,Ù4~òÍ[E5Ló0ß,ö×Uç›eQÍÉÐŒòkeF®zËïZY–Š õ-pþ_}ŠzK­¤"Tgñ~Çk1œÕ[øº¾ÔwpS|3ì—©¹p+©)MgpS¸„÷˜‹T¸ >oùVˆX§Öù®_$ËòÑÿ–]¹±~­4\}Ú‡;á3cß)¶Íǰ£k!¦ì•pκØ<õòqçc­dòñ;vû¢}ŽYéËÇà¬Y«- ¼”«¦gRã¥$×Ûé/%'#k?î\‡D?”R¿2ó>ÌFÊ÷!ÅçÙÁl,»ÿƒú ϵØmGVi¡ŒÊò8W{:Óž~ÕU”0WGgyú9/.M|­Ì‹Ë‚VÛ ™ãÎd,A]ý<·)÷h±4¯í­ŽDe¨jR6g*íÕŠc™ºqë‚wl›gÏ0Kpƒ…P;Sa¢§õOð«j²)ý›ïšAô˜]ó]ã–Ó5oÁo»"qPkŽø»òÏ®rçXŽË·y ¬EÀ¯»…6ý>{¸î—‰é‰nõY’“ ±Y±L¸fy¦¾Içuø<õŽz'ƒÏsôÊT4>ÑÏêO”À…„89Sò58 êûifÆGYÔ"OÅï`Ý–f–ƒ·º7‚œòøµ/mv_SοöæÌÇôžúqµóÞ4|:Y,ïû\¼Ø¿|ý¸ç£kAZjBŒîÊQú&°Þ^†q‰%žgDJuJMV»”†ë?ÊH|êò$ø³“Óö®èÍ‹»2W"ú<àÉ?T¸J¥ï Öò,×"Ù«ÿ¢UgɺI‰æœeûQURª%êËÓ RŸÄÎZ¼cC&™²Ÿê•æ'X·Å@æ·R‹!þVëU;–»MxΪ§š[õuõÊø0õS/ªšÐ¯$×˦9Ÿ8Ò\ýš7÷ŒkhÞ0kƬ™7üžµZ‡íF7¥x†7¦V/E#®ë.µL6}7õPt»ôÎTõ£×¥‰;ã•Ë!L=ä{žç½xÐʺycaøŒPÐ.çÀšy2§-îäëG‰~O¶|Z#U5G `*5C/ÊÊþ¿ç_ǤÏòù×±GŽŸËöŠn—ùdðÈçJÊ—dØ)px,›à¿KílÉÅ•1}u]3ø–W]ŽÄíS¼ºçe×* h Esþ®´òä--uè/HÓci­‹ê8Îý þåvá)ãðX´œŒîáÈHÎ1HèLò¼$ouÚô@®=ý[œ{~‚Õxceó^'/Ñy¾V»-ý%»ÊCý_¦\%z–[ ëêy½~‡Ô›%˜}óâ=ÑáÄ=‡þ®Œ‘Þ—n€·çïQ:JöƒL­'k2:wE¹’§Ñµ-vÌátB]¦>ͤ§Î$æ‰Ú¬v$æ,$Ðq¿Ç=I–P+Ð ^>žòÕù 6ågË„÷ß )£;R³t’¶rMÒñ á 1ˆ¯7à„Ú™D›w2=ÏeŒ¡•p=©´—Uf®'•â}2•a¯]íÊ ûŠe•~¸6vþº&<é.“ƒ1%¹q${ÐBÜz¡öÅïÅL Yú0RªctþXè™×†bìÁ”ð?Úý~îÙØ–Ö;¨-¼j𖄺%¶ÿl›Å|,Áy)<“ÏâLyÏÅc)Ù¹™xä‡^ˆ¤¸ëü„%ß'›^;!•|ÏÇšpâkÆþ­RåI@ª#YdJQ…4¸âdÂõ! .˜l¸öxz—î…tÈC…(/\;=¼®‡kÅë;(®:ÝLN€nŠQ}\¯ÝC%á:{üqüS® •‡+MàÊÐ-T™îÆÙÕ)ΨO-p·{© õ¥©+ ‚kJÂõ¥a4œzÓ8¸4®= W•6Âõ p¹é¦Žtî~:׋þ‚ëEéÝGÃõ¢à D÷ !4ê%t¡#lƒî¦0c á\"ÂyDª#ò‹ë©µÁô€¡T[Ü n f¢°( ¿„(øÒ¢4ü2¢ bʉrðo7ï-jÓõ¢Ž¨CùD]Qáz¢ÂõE o‹·ÉïˆwPïŠwI ÅB2Å"±ˆn‹Åbâ=ñYb‰XB·ˆ¥b)iâ8DMD¤ˆ¤Æâ¸8N­Ä qþYq–BÄ9qŽn—Ä%ºI\—é:y‹¼…Èj²5’5d ª+ÈÔG6” é>ÙX6¦ö²‰lB÷Êæ²9Â-d *"[Ê–ÔN¶’­¨¹l-[Ó²ƒì@dGÙ‘ºÉ;egê,»Ê®t§¼GÞƒ«Ý/ïÇ¿=d„{Êžt—ì#û e?Ù~ÙW  ~˜F-äH9’ºÊÉr2u‘³ålê-_’/!fžœG7ÊOä't·Ü* ¾r‡Ü÷È}t›Ü/÷S)yRž¤jòœ<‡ø‹ò"ü(E5Gs¨€–KËEåµ`-˜jj!ZÕÓ h¨²VP+H5´ÂZa*¬…kátVB+A·i¥µÒ”W«¬U¦P­ŠV…ªi5´8çÖÒjáÜÚZmœ[G«CaÚ]Ú]ÔEë§õCÊáÚpÄÔFR]m‚6é_×^§žÚÇÚÇt¿ö‰ö ݧ}ª}JµÚ ª£­ÖV#þ í ĬÑÖ æ+í+„×jk©¡¶^ûÿnÔ6"þ[í[„¿Ó¾ƒÿ½ö=ümÚ6ê£ý¨í@ú_µ¸òoÚoˆß¥íFx¯¶þ>mÝ®í×ö#þ€v€Ðj©™ö»ö;µÖŽiÇs\;ŽðIí$üSÚiê®ÕÎ!þ/í"µÑ.k—þGû¾Ò= kºN­uGwΫç¥6z„pC½ÕÒ›êM©~»~;u×[è-¨£ÞRo‰p;½ÕÖÛëí©»ƒ*¶aS%#ܧ F£ Ým”3ÊQ=c­±–z¿‡¨­qÌ8F÷gŒ3TÛ¸l\¦>†2õô„õ0qP ³²Y™™UͪÔ׬kÖ¥¢f}³>34ûQ-ó!ó!ºÝ|ØB}Ì¡æpjoŽ4GÂeŽÂuF›£cާ{Í æºËœh>ðLsþ}Ê|ç¾n.Dx‘¹áÅæb*b¾g.§væ‡æ‡tƒù‰ù õ2wš¿Á?cž¡Væ9óüóæyºÃ¼`^ Ûü˼Œðß–¤¶–fiÔÊ2-È+Ø F8Ü §²V«õ°ŠZE©œUÕª…p}«!u°[á7µZS7«Õžî±:ZwÁïbu¥ÎÖ½Ö½ðï³îCšnÖ´FQ~k´5žš[­'¨Š5ÉšJwZÓ¬iTšaÍFøÖÿ¨¤õ’5—î²^±^…¿ÈZD·Z«¬Õð¿´¾¤6Ö×Ö&ºÛÚlm†¿ÅÚ·µñ'­“ðOY§©…­Û.u±KÛ¥É±ËØeȵËÚe©“]Ý®M]íì¨ÝÓî ²=“úڳ짩·ý¬=þOö.jiï±÷RU{Ÿ½éÙ‘ÔÝþÓ>IÝN Êã,s–Qç}ç}êè|è|ˆðGÎGÔËY鬤ÎgÎ*jè|å|E­ ΪívŽQç¸sŠ»8¨¯›×ÍKÝÝ‚nA„u‡SwŠû$5rg¹³©®»À}•ú¸o»oSw¡»jº‹ÜE”×]î.§P÷C÷Cªæ~â~‚øOÝO©ž»Â]A•Ý•îJªá~æ~FaîoîoÔÒúkÍÚ¤ÃÝE\chNÚÎÓ”·³Ž,Éz1ˆ oèN* w'ëÅ‘¬K±+mX:¯ \qÖ”%XSÖcMykÊÑŠ¡b(½&O è¿.†S1RŒDx”EUÄh1šcÄê+¦ˆ)@!SÅT*+¦‰iTA<)ž¤Šbº˜Nb†˜AåÅL1“*‰YbUsÄ%ÞoÙxXG2Ö±ë8q°ŽËXÇ`¬#ëhŒu,±L,£WñÜɈ§‹8,ÓqD¡G»t–Íd3øv¹“QKgF-ídÙØ¢l¯´‘wÉ.ˆñPK;F-cä}ò>øÝd7¤é%{áßÞ²7Â}e_¤yP>ˆµŒ‘C$Z€*QÿÁŒ•£åhÄŒ•ci |LŽ^™('RG9INò!›¶rŠœ‚˜©r*Κ!gÐP9SÎDÊYò)Äx¸g|Z>4ÏÊgó²|á¹r.°Ô+ò¤Ÿ/çSi¹@¾‹;.”‹pîb¹ïþž|w_"— Íûò}zX~ —ã Êÿ±üÏࡨÁòS ü!WÈÏþ?ù9®¼J®ÂÖÊuð×˸Â&ù þÝ,7ã]¶ËíˆñP× ¹SîÄ}wËÝHé!°FŒÀ:Èò®yTEž“Çqî y‚ÈSòÂçä<ç_ò/ë‰IêªIMÒ`  Õ Í€oj&p Ô#Âb¬åÑ®£!ŒÛ†2V{D+¤¢ŒØîb+‚ôEµ¢HS\+Ž4%µ’ð;h²“Ö‰12k«ÍÓæQSm¾6ŸòkojoÒõÚbm1ü%ÚøhÀ_©­úY¥­¢ÉÚ—Ú—4•qØdm¶ñëµõ­m ŒÃši[µ­ÿ ý€”ÛµíðÒ~FÊ_´_Þ Ö 8lRîÑö ÆCc“‡5c6G;¤‚©EÒ ÚQí(Mû1'´4…ÑØ$Fcíµ3Ú¤9«EÌ9í<5×.hÒCfÏi™ÍÑ¢´(j®K]ÒÝÐMzA·t a(­½îê.=§çÒsÑl=·žñyô<4‰ÑیޞӫéÕh²^_¯OSäÒózc½1pÞíÀpSõg ÷,0\'\a¨>”Bõ‰úDzUŸ®O§ùúKúKt³þ²þ2•ÓÕ¥pO ’á*Hpd£¨5r <"™Œ]Ã4L2 ˰H0"Ô ÇpÈ2BŒ 5J¥h°Qèðac“±‰:ÛŒŸh¬qÀ8@H#’†Ç Ô7ã¤qþiã4 e¼ø¬qÖ8KSŒ¿¿iªññ]ÏØ1?£Æ±Œ‡š·šu€ù<ÔØÉ¬gÖ^ô°ãóf³â;š}ïáÈçÍþæjÌhòyÆ‘ÍæšÈ82?ãÈÎÀ…Ë€ß7ßGx90bgF„ ̳æYϸ°jAû3æËÏhoªUÌ*˜2V ·ÚVmÄx8o¤ÕÁê@3ÚÉh¯u·u7Uµî±î¡šŒüÚ0òÃȯÕÃA]ÿÕgü×Òš`Mž{Üzœ^f,ر`;Æ‚M­éÖtø"lÇXpŒ5ÏšGÓ¬ù@„c¬×¬…Ô•qáLk±õ9Âk¬5ð=\8ÐúÆú†ct8ÄúÖú–®gŒ8ÐúÎÚFƒ­íÖvšbýhýˆðk=ÌØñ kŸuá?¬?ò¨uaMN±NÙe4ÙÖÎeç¥Áöuv>ø…ìBt·]Ô.Jýì6ìj»¤]’^´o²o¢žv)»ÝÏè³£ÏîŒ>{ØåíòôŒ]Á®€³*Úi¨}›} *­N¡v=»>üöˆocwßÕ¾Ÿ†­ö ë­N±{Ù"f˜=1Ù!fœ=1ãíñ4Çž`?³&Ù“žlOEøI Û¡ŒnÙÏØÏГÀ¸/"ü’½Ïÿšý9üUö:¤ùÚÞFØÛíÔðön{7=¼Øûíßïáàööaû(ÂÇíãð=LÜÞ>eŸ¡Žö9ûræ¢}˜¸ Sîrœ0àà›æTØéèt¤¡Î@g =â¼à¼@CœœåÀÇbÎψ¹•³ÚYM-7ÏqÖ:ëèg½³Wó0ô³ÎFg#â¿q¾Áu6;[ÞêlEø{ç{jÎûNFØ]œÓÎy\ÿ‚s™qMפ®®åZô°ë¸ qs¹¹ŸÛÍCw3 oï»ÁH↠M·Ò„¹aHsƒ{#ÒTq«Ð]nU·*Òtv;Ã÷Ðù÷5÷5Ìè|¨û¥û%5u7»ßÒTw‹»i¶º[iŠ»Ó݉‹·^Øc2úöz¬ò]}S°b8¶‡¹KY—¢®À½NÕ©>²×Ï4HÖë]GH7ÒîG:CçA^¢¿é[7jB‡`3…¬˜Çë6 7ˆÂÀv¥æÊ‰›Èê‰Õózw€zÞÚy(gÐÍ{@5Kc"ÅqqBœçÄ%qYÞ"«É²l$Ó¦ðK+ÙZz½,@,@*÷ÈûeÙSö‘ýd ‘ar$Ålù’œí¾U~/€¦Þ#÷Ê}ÐÌ'¡y/Ê(ÍF ÖB´У^_G ­´VY«¢ÕÐjjµ´ÚZhÊ~Úpm¤6A{]ûXûDûT[¡­Ö¾ÐÖ@®õú!´Ð|ßißkÛ¼¯¿AûÍëi€fÛ½v@;¨ý vš ZËëAðú £þÑ”×c ;Ð6A^ÞÔÓ)Ð(íôöP¶ë¿¬~Xü°÷ÏÀÎW°ïMØõU!™ë{òr÷aós°9²÷Qs˜9Vü(HÝ1æXó1sœ9Ö;,wÏn‡Õ>Û|Vûæ›æ[æÛæ;æ»æBØï‹!Ÿ—˜KÍeÐ@>Ûv;dô9Xë`¥_4/™—Í¿ÍÌ(SY€›– 1a§‡Ã>/ »üVë6«šUݪaÕ´jÁBo`5„…ÞÔºÝjf5·ZX-­VÖVkHð¶V;«=¤w'ëN«³u—'Á!³ïƒ¬înÝo=àÉiHé1ÖXë1kœ5Þ“ÏΓ­)ÖTÈçÖLk–õ”5¶úKÖËÖ\Øéó­֫ȰÒa£mm°6Z› ƒ·@ºî±öB–Â*‡5lÓ¶lÛvlÒ± ¤bu»†]Ó®e׆”ë ™5ÅžjOƒÔšnϰgBn=e϶Ÿ†ÌzÎ~Þ~ÁžÉô³ý‹ý«½ÓþÍÞåÙè°Ð!• ‡NØ'a›/ƒMþ!lñ•(ÿç|4ÙÙpÄùÃ9êƒ|øÓ9áœtNÁÏ ëûQw˜;–÷Twšû$lï§ÜÙ^ë†å½÷rXÚŸÀÂ^Ëú3´aY²¬h 9yc}Xz3ïj·º þP»ÔïI­hq­1ëNG³¼xÔ&µGÇ7¾Âï(üú)é½t®‰wŒ™Ó·)1_)óåÚ7¿¬–©}TD­WÕNµY¸vß; öá¾+3™yòK9aÜÞ#w†ÎC]>F¡÷’ª¡Gò|øcÙ(Ç•í.ß,õ0ÔÚ_Q—å˜Sêœúíø<>çxŽóùla/øÖƒõv@LßÛ1§p0U¼ÛÔco‡D—‚PŠgP;\ÊÍ!Y覗½} •öõMV°Ý7D•M×ywÇÓ°ª˜äíj‹:û™êÜ«ø†û²è>'þÕõsObN$Ï1h ýÞ^ð™~¿˜ú•ÎѯhÁÔVàIòìD{¦¨/ÔiÜË=g•_2­@í…ÜýìMù™$–ÏIë_gèýÎ&Þé#1ÍcåžÌÄûʉ³wPk7â©~Nïò±ò%};Ü—Ý%qz-ÎU×ÄÎ”Ž‰ñs-ó«ûò}ØÖVšæ ê¢º#‰pw„ªÃY‚õÃè_x Ww{k©0Û1RíöBð÷ûÖ%B½Š¶1½ßé©c±õ#2ášO±|È]¾YèÇ’Âï±gíOÿL¯ØžúӦтÀ3dÝ¿ÍÇÒÝÅ«è¬Æï]üÎÛbfÖój{Ôio?²xÌÐj™V”rPµ JXþ®§ñw±ú+zŸo¸s¾·§Â¹«-£½y1~üú™á+ŸŽÿË·“óyÏ®åÐñ¬Xµ.z5ÙdÖÏÔý¼®¬ê½¶n´e{µû!¯Ç›Yow¾1Ï Ž^×ì$êäɘ:žðUy¿ã±¾¤ýÌ›ºŽySùšrÃ÷XRù©•F  ŠTùN¡Ìw aŽS(Õ¥z7 †ø÷nº×èF÷#ì±›ò1«)s™ 2‹© = W€ ¥Á4玠‘8Ëã)éô8œÎ$“9Hsr1û(³òÒÏp¹i'œK‡à,: —‡¹F’.ÀIf f á ¾ÇÒDAQ¾ÇáÉ'ÉFt=3U®—íd;ø%ˆ9'AÌ3 ’ɇÿ°L!r„Aä9aaÊ ]Ž—ãÉ”ä2˜gR@>)ŸDØc•èÌ* a>‰!Ÿ“Ï!¥Ç'1˜Ob2“Ä‘ ä<Ï«ò –oÊ7~K¾‹°Ç-ÉÇÜ’P¹D.Ãu>”Á_!WÀ÷x#¦\-¿ ‚r½Ü“Ü„x1"ȸ‹Ç¹^“Çó§üW;-O#Þã”—ä%2˜ Ê<f€`H>-·–1¤s< 0¯#”¹¶ö–ö9ÚBm1|Åáh˵åð?Ò>"W[©­"ÉÌ ©mÒ¾!¡mÖ6#ì±5³5³5$s3„©%YšvI»D3(4fJ`F„ÐëMHêÍôæ·ÑÛ’f7ŠS°QÚ(MŒuÆzÊgl46"f‡±ƒtæ'\o4’Ë,…Æ ã…QF9¦aTÀ´M›B˜psr1ëà:fäb¾A~óqóq2ÌIæ „Ÿ1_£ü–né”Ï ±Bà{\‚ë­bVøs@0 ÈzÒšN¶õ´õ"|o¿ µØú þÖ µ¾²ÖSÎ<:jí³NÀ?cÅ5ÏYç(غ`]Dø’u áËÖßÿcýƒ°²å³¥-)ر QAÏÇ#òÁ<ælW²o†‹} â«ÚU®fWƒßÌn†˜v+œÕÆî ¿‹Ý1Þø{wxä½¼;<æ^€GÛCx$]·Ú qEöbœõž½áOíOᯰW"æ3ûs„½±u“GÒMû°}þ)û"f£öe6ji–ÎX:b6j9f£F°¤.ÏlÔªÌF­ÉlÔ[˜ÁlÔAÌ=ÄÜÓAìnb¦iEæ˜bŽi³K+3»´Ëë2Ì+­Ä¼ÒJÌ+­ÁŒÒ›™QZ…¥ýYšWgi^‹¥yf”6b™Þ„¹¤íh;\mf”6å¹#u™Wz;ý ×’~ƒ«C»áš1Ó´-ýלõ¡#p-X´gÖ©Ëš „Y§AÌÈÏú 8³Nƒ™uz³N¯gÖi(ë‰ë˜uZœY§¹™uʬÓ™o­?Læ˜Ì+-ÆŒR¥3J 2—´³H‹2s4œ5M+f‹:ÌmÀlÑú̵™-ª3[´,³Euf‹–ežh=扖d>h)怖`Îe/æ\öaÎeEæ\Ö`ÎeoÖd½™sÙ‹9—½™sÙ“9—}˜sÙ›9—=XÏõaÎeOæ\öcÎe?æ\öfýׇ9—½™sÙ“uaæ\öcÎeEæ\F0ç²&s.+²F,DZs.k±.¬ÀœËÛ˜sY‹9—¬+±^Œ`¶e5ÖŽµ˜mÁÚ±kÇꬻ0ϲ&ëÂJ¬ k0ϲ"3,Ë3Ã2‚–™aY–̰¬Å ËÖ”Õ™aYƒ–5˜[Áš²³*«1«²&³*k0«òNfUVgVe-Ö£½˜UÁ¬Ê̪Œ`Ve%Ö¦5YV`VeëÑZÌ¡Œ` ZÑ7ÓÅcOVgöä­Ìž¬Åšµ"kÖ̘¬ÆŒÉÛX˶fÆd#fL6fÆdcÖµ™1Ù˜“!̘ eÆdqfL†²Þ aÆd(3&¯gÆd~fL^Ï:¸8ëà⬃C˜1Yœ“Á̘ÌÏZ¹83&C™1™ŸY’&³$‹2?²ó#‹2?ÒbÍm2?²(ó#‹03Ò`NdæDcn2'²s"-æD¬ã‹1²(³!Ù i02”uqfCæc6d³!‹3Òbd1æAb¶â f+F0[±<³k1[±&£ÞŒ*2[1‚ÙŠÌP,Æ ÅâÌPlÌ ÅFÌP¬‡›Ø„Y‰ùâ°›0+1£„ÎÌJÌǬÄÞÌJ,̬ÄFÌJìͬÄû™•Ø›Y‰½™•؉Y‰¹˜•؉‘D/f%6bVbqf%öbVboÆÅ™•ø ód>bæ#ÞÁ|Ä<ÌGìÁ|Ä~ÌGìÁX¤ó»3ñf"æe&âCÌDìÉLÄÖÌDlÍLÄžÌDìÇLĆÌDìǦ3؉X™ˆ˜‰XŽ™ˆ˜‰X‘™ˆ™‰XŽ™ˆÌD,ÆX'‚±Nyf"†3±<3Ë1±<3‹1±&3oc&b3#˜‰X‰™……˜G8ˆy„ƒ»TdìÒ˜¹ƒÅÁTdÓ˜¹ƒÅÇTdv`5æVc4S‰y•˜XƒQKufÞÊŒ@ÉŒÀŒcª3#ÐbF`ufVgF ÅŒÀZÌŒ`F`­8ŒÀ‡œN{ªË\ÀŒ`*2‚iÏ&„˜ÌŒÀëÁ˜Ì¬Ål¿"ÌöëÅl¿>Ìö«Èl¿Œi*0¦)Ïh¦"³ýj0¦©Äl?‹‘MF6åÓTd¶_ F6•â°ý*0²©Àl¿~Ìö‹`ž_kF6ÅÙT`ž_1Æ7Å™çgÁÎZ tcÀ~ôìEÉ8Fg{ÑZ¸)<ä‘‹mÄq»g½‰¶žõ&º{Ö›xm—èíÙmbì¶sâ1Xlÿˆ °Ï¤˜êYfb†g™‰ç<ËL<ïYfâXf·ˆ9žM&^òl2ñ²g“‰¹žM&æy6™X›lœx׳ºÄVON‰í°º–а´>û<9%"=©$þð¬+ñ§g]‰³;aâÏ® vÕC2̳¢dUXQ/ÈÆžÜ‘]=¹#{–“ ËéKù¨'qäpÏf’#=›IŽr¶9ÛähÏr’c e9Ö“)r¤I9Ñ“r dGU9Í“r:ì¡7ä Ï’³ /¶Ê=!çý?oˆ. xÚÝ™tVU¶Çÿû|·}›2Ô@hB@T½J‰ˆÒ")@€ -¤Fºt‘&PDDˆŠeAä¹|ŽKP„Á²”§>†yã "ƒo߆æ×{ë±æ­³¾ßÿ˹÷ž¶Ï·÷>7 L¹^WRÆJGøQƒ†#'=);;À GZG )ðË/—{G*! ¤¶âÑ=ñ C‘‰ñ˜ˆ©¡»-C”GTG}܇ÑÝñúa Ò0 £0¹˜"÷!ôD@z,{šˆÃýx­Ð½ñ8’1#…±˜„é(‡@§=:¢cB÷‡#0°WB—,Ñ6ÊÁ ˆR¸Q5ƒ†h‰6脇‘€¾HD*†c$Fcòô‰ ô‰Ú¨‡Æh޶è†iZ/J¢,* ¢Q Ð-ÐнÐO`Òñ$Æà)ÌõnÃE Ñß¡2j¡.bñÚ¡#zàQôG#ÙÈÁd<™)qY)8eSe e[ege”¤ôì@oe¢r°2C9F9)%)kP O9W¹H¹\¹F¹1%eÄÈÀ6eò€òå å)å÷©éC‡.û´ŒÒKÍÈa•T†++(#”ÑÊeÜàQI)VSekeWeoå@å0e¶4œdMRNWÎV.HÏ=ÂZ¦\¥\¯ÌWîTîQîKÏLI·ö++?P•‹£¬Ê/•ß(Ï(Ï*/*¯dŠØ–’•¥•唕”‘ÊZ™£R3ìúÊÆÊæ#ýúxe{eWe‚òqe¢25KldSŽTŽQNRæ)çf Íl/R.W®QnTnSîÊÊŠmh(ßPPV~ <*Œ³O(¿RžVžU^òéa#‡•aÊ ÊHeeœ°±Ó\¯l¯ìªLP>ž5:9ËIT¦*‡)G*Ç(s³FÌr¦*g*ç+—(W(×fË*9›•;”»•¯*ßRT¾'n‚ğ܉F~±ƒ’üÊïDKH±#½¯š\eù ñî—zƒÁ^cÏ0D䯵sy³Ô“ ;¨;Éñÿ{|úïî,Ñ1Èiñ«eàMeÞ-eïmË»7•O­¸Un)-ÜY7åmJ°æ%°ZæsÓÒi))u/ç ™m’“†Î|g>Ê:œm(ç8¨â¼éBUç°óGÔvŽ;ÇÑÐ9åüqnU·*Z¸ÝÐÒmã¶Aw¶;Ý…îrt V VDÏ`å`4‚µ‚ ÐŸßæ·%÷5Í,7Á[èöõVÈÊZâ­Çº¼•×öQ)a_‰ÅÅŸÇ‹úYp¸Ý ÆŽ·; [Ù]„­¯]i£WÚê•v7\i¯W:ê•z…äÚ^»…ÝRÔedú«Í¡¾-Ijcä[¬”:’ÿ7— ú!) $×N’º4)dç —\=KJñ3ä¾we›‰w9"™þÇRÚᘔö8.þ¦NJé„/¤tÆ·Rºˆ:-9ü÷T‹ÿ‰D5 XdÒ7ô-ž4­L&²«°>Pø [­†ÖýøÄŠ·:ã3+ÛÊÆwÖrk9þÓ.k—Åi;ÂŽÀŸí(; gìƒöAüÅþÚþÿeÿ‡ý þjŸ±Ïào¶œµ¶/ãœý‹üøpÊ;åqÁ‰qbpÑiä4ÂOÎ}Î÷¸äœuûS›è®3Õ¼Ê^e3Èkê55ƒ½x/Þ ñÎ{çMZ°|°¢*Öm`Fwß6“ƒßÿf¯¯˜U,ŽÕ¬f‹-³†Ksió<‡q˜YË•¹²YÇ‘iÖs4G› \—ëšÜˆ™MÜœ››ÍÜŠ[™-Ü–Ûš|îÀÌVîÊ]Í6îÎÝÍvNೃûpó'r¢ÙÉÉœl^äA<Èìâ¡<ԼĜavó“ü¤y™³9Ûìá±<ÖìåÎ1<‘'šWx2O6¯ò4žf^ã<Î3ûxÏ2¯ó<žgÞà¼À¼É‹y±y‹—ñ2ó{~ŽŸ3ûy ¯1oóZ^gðÞbÊžþwó壿#>ÆÇÌ>Á2¾ßo¬ž.©æD:QNu§†íÔ¼ÅóEý/ø¾@(+#‰Ìêooò~wg,t,æ:û7 #çõÚxÆzÚšiͲf[Ó­Vž5ÇZh-²[K¬¥Ö2k9=M3i–Dù94×ZAó¬•4Ÿž¡´ÑbZb¦¥ÖZf=c-° ~<%íÃ<)l¡ø©ÎÔ6u§GáÑ@ˆR”Ey(m·Æ£º5ÙšŒv3»¢ƒ'ƒ'Q3øuðkÉQü¬Ÿ%Ömì€mÙ¶íÚAÛ±=¹bdË$0¶Æˆ y¨P^ègÎ×G)?v]½#2t‡¥m´—+ Í´KWÿêÚ¯Ôn¡|ÚJÛh;½@/ÒÚyK¿FN¥h#m¢gi9=G+h%­¢5´–ÖÑzÚ@«éy?w¦Ôóú£¥´Œ^¦=ôжcÄ;–§¡Öf+Ÿ2i"M¢§h2M‘ÌjM§”w󳡧\J£4ÿ0’Mé”.£Ë  dusa‰Ý棤µÂZ©ïZü>ÒäzeÓhCci—ümå^oašGóýg¤Õtíc( •>2)Sú˜(V+\â:†póf”³ò­|9eÔGø íçHÛ×ÏgºÌešÌhÌÿæ÷Jz%¼b^˜wŽAúöj€œ ËÉï[“'óœíΑ¾®y}÷Yw¹gy¶çx®çyA=ÿŒCGÔZ¤çJ(vûÞ|[êÊAGÐ玆ž»z–¶®ö®W?¾Öjy‰½‰VâM#&©`òn‘¶5NŸ|M>í¥Í’•ãðàÕS¶œˆÄãÆÂ4Ü'ÌÕž›\û6È?è·q×êJ ɱ)°ß?cº¡¦c®ä(«$Ù=xñ>ŽÊÙùÎà<.“EÅ)œ*QuŠ¡Æô µ¥®Ô‹úSªd‰£$¾O’X>[âö2ÉV×K–»Sbö>ÚO‡è}úˆ>¥Ïé¦ïéGºlŒñLIn*˜mbLœijZÀ+ðä­UŸ3º¯ÉxD5½TSÑ[u°}Huˆœ7|MCÕ\³ÉW:§o1²²Ý¿ûjGº?ûêE¸—U«áaÕ(÷ŠjutS­îªÑ’]øZ=Uý÷›¾ÖvQ1›Uëy¤ZßlUmàÕXªqf‹¯ì™|Õrh!k‹x¡ÿNßzm„Éè,LAa*õ­f¿ã¿9Ãc¾mí£ÂáèçÛÐ>&œhÖJÞ|}`dŽ?ÀÈ ÏÁÈüÎ «¡­0ÊýQXí„5d÷™UaMtÖB'am÷‚0ƬÖsÖ7… ÜKÂX÷¢0Î>"lâ‰góZÚ ãíO„+Ìz™Ùa9É{‰ R%+Ì–}öÏ÷Ó%?å’F婊ì©:KMdWµ¦ŽÔMöU_9Ó –ÓÂ4Y³\YÅ]ªÉÒª¯)˜¡šj¶ûJç¼7|•3÷k¾zÕ¼—T£¼ÝªÕ½—UkxªÑÞÕšÞ^ÕZÞ+ªµ½WUëyûTë{¯«6ðÞTå¾²ÇUËá)ߊÞzµßdµßµßTßNf›o!o§oo»ooƒoÏ_ßêÞ&ß*^¾oo³oo‹oo«oo›oo‡oïßÞ‹¾=¸¸¿â\AWœtå¾cµ[ÙñZ³MVÜ ¹øšß|öÀJõQ¿9Û·²ÿ”guacamole-client-0.9.9/guacamole/src/main/webapp/fonts/carlito/Carlito-Regular.woff0100664 0001751 0001751 00001017010 12612226251 032121 0ustar00mjumpermjumper0000000 0000000 wOFF ´\GDEFÔ,]‚š&™¿GPOSÖŒ8ëLìeföGSUBx Lð‚ÛOS/2ü_`jçcmap`ýèéÔNcvt -ø.8%þÂfpgm(` uÛ¶.ŒgaspÔ$glyfIô†¾Î ºhead€66|örUhhea¸!$ ™hmtx\+vÞ––loca.(Ë+|+K½¶maxpÜ © VnameÐù †§¢¿postÔ ÿœÂprep-tšx:8^Ö\m_<õJSÜšÎ.0Tüýï /xÚc`d``cøÇÀx„kÁ±Òœú @dÀuz‹º ÞóŽP]nð uxÚc`f‘`œÀÀÊÀÀ:“Õ˜QB3_`hcb```âgebbbafbyÈÀô?€áÍ  '3”Tø0800üûÏÆðñû<&5Æù 9æK¬J@JÕŠcxÚ½š xEÖ†¿®ªîƒ,EA4 (È* ²…5 [HÂ"È6A ÂU0„PE`DÜfDEÄeœQD~\FEÑQ™ôÿº}CAÐ_ÿû<ïSU½Ôí®®:õªc¢Qð¿röf-¢¯ªÿ¹éE¦ ,_`žñQKÿîµlÆ\·4:™Ó˜ï¶D e°tV PÃÄ!K÷Å\=Õõ@lñÞÅ·òÜîq“ÑÁ킎ÞW¬³“ÌÐÕÑNp· ‹Àkg8ß ’ÚÂ﵋<Çou W˜qaÜò¨JBuï}ÔXÏdA߇òî3¸Dgò¹à7ˆG3Ý蘩'`²z “Ô¿°JuÆÍL'©géìDKµ êÖ8ƒø.·¡*ËSœ[0Ƥc¿:†5d"¯_I²œï1E]‡>Î'hÃ{–¨™h£¿Â5­Ô,r=z¨TÌVyè®ÒÑé›|Ž:˃4×ñ±V¯Ä“z#Óš®‡"SÇbŠ º4ûdY>Ͻ¸[­Árµ«'±Iït.6pª8ob$­ÆaÊÆ ç^S{uwŒ79ˆ‘~k– Ägj-}^oÂ^yö1`Û•ýl.ÇBK,5õÑ[çú'ÍuHÖë1D½Šæ&µL+¤™ ¨¬ê³ŸogÛÁÝzJh…D­‘lF"ΔÃ^— ;ò»Åøš4Ìv£Û”m1ÜÿÁ}9n.ߎSF‡ƒ|ß‚é„{8Ö¦êñ˜æÆâro¿Ñ>4te|õÄLó ¿éO¨ÒOò?"}A›gróØ7.åØ>ÅkïÀÞIÌáX›i>÷ö*cß)× ÄMfÚ…¢p·ƒr´•ÌIŽOÃq•€>ö]c!ûT¦:ŒI&—¹3P…ãñZÓóôS˜n.AiOá"w8û{{,ðâp©Çþ_ ³u *èƒ#ùhÃwÎ1 h;^D{÷m¾O>¦˜D>g*ûÚKX¢çqì/@¢»sôk¸Út÷OÐ.\¦Ga59"©Z…CúILÒb"Ûg¾ªÉþT“Ïçb–; myì!²T÷ÂÓdcߨžÂò¼‰ÁArTWò¿Ó[Åú´ó,ó_©;i;NcQ¨9¶2ÉccÔmXJ†¨±ìÓc1Jåpüå`,Ï¿On oþ‚+™&“æÎ.þg#lsÇ¡{(¯{›±Äò–¸¡‡ûûþS¸œ×v2O°¿Dã æ»K=¤6éÃg¼ŠÇxÕ‘Õ«x쓇›Ôk´¡_ Ic{v×ïâFÓýõ6\Ît4Éä} Éý¤éIª’Œ ŸNú’Á¬¯+ëkϺ„dÚöX_gÖUËÖÍ>ÇIŸ ®+ƒ{±A³ÿ ‰ú «Í¤>Àõ0F¨Í±ØO}LðrÕg˜Åt©:€…ÎsXOê¨ûÐÞ9…kU ®WÑX%¢‚jÇr7–›à:ÕƒÇ:!Y=îWp÷"K`Ô!Ù¤C´&éKž ŽK~»¤î Ž=ñÞ!¶Wzª/ý¤½ ¤˜TöÉŠèÍoÞŠ$‡š;ãIf(GBÑÎ ‘´dgg¯×'¹(ãüõÍ§âÆ–>ŽÓLU8ÅÖ ì)Ï;¯pl¶g?Ï1kÐÑ丒9lûÂNöšxÈâá³[,Y˜.ó‡öÐ[Ígÿ‡45•sÝ'´;O²íæAÑ6'ÿÝÃú¿%ÃôN´ ei»T±“«ÉÝêQŒ-Ĩ"ål¶}6îå7œ%uñX¹îÅ@¦I–^éd„Á2ã dÎüñÜAÚÛnü¿DæŸ!Ÿêo°œÇÖ3¿A?Žg™® õᜦ—àÀ¥‘²ó®wŽ£þÃ>b(Î}¸’ùúÌwÑ%ÐÛ野N *9/£¤óíw,mk,ÚSïdë8,ÿ-ÿK}s m]ê»!¸F#?³Ždÿc‹6o0Ï÷ÖÉœÓç°~ŽrŸ·‰^.ç/Ñr.¶ë¨TßamÓÍ<×Á,ã¼8ˆÏð&è6hI;ÞJý›ß'†’s[ÞjOý%Pkè«ésxžEt çš)vZh¿8½Šß%|,Qž]p«Òf DªiD»Ø”:‰Ïfº¡õLI}À‡óÌä°þâÜð#ÊZÝ5 ÷yƒ0Ãbu–ÕYÝ™§ÖrŸ@šƒ÷±ŸŸa®åÌ %õrÚ½å´ÿlê·75ׂmVSÚ͇hý7Žå<¦y¸Ñ¦Ô·ÔЭù.]Í­csý§ù]cØŽKä½=Ѿo³?ÅP[Ëv¢Æs'¢ŠWšsë¶ÕpLgßšÁwÊ6üÉú\ã–aÛÖeŸ)Ƕz =دrM/~뜷oGѤ*õõ½Ôu_£©K}óíâ#¸ÅlÄ-nêQ{vfŸébµ(¡þZ­÷ó˜ÐMØ?sMmÖ$xƒfœ—¯ñ¢ùn]x¾.ûû¢>€jŠÚCΙ®ÈuëR EsŸâ{|Ãv;Ž,õ35~¦ªÈ!CÙ—ú뛩æœñõì¶ÉÍœÛnã¼>Žý"LªÚ†¶ê=Þ³Š²*ê œçÚ"žºu¾èUSOÒ.ŽS™˜Iî¤^Imú0õl"ÓËì+éØÍ9¨zž¶±uô öá ¤«~´…Ô¿’÷òm¶£4¿k)¶MmØçwqÞý€ß}ß±ÿ<ŸUôò(þÏ6Ή™V/'R÷>àä"Íysø c™¿Ùùb<êæIœ'kR¥ã=y]•}þRìdý‹ŽÖÑÑêçÛü£Î§èĺðúóÜJ²–zº5óBν©êêtÑÞ'¨}¾ç}½87äR“dè†èOÛ¿ŠvxÉQ+0”L1IxFàÜ{BÝÀ¾ <Êy¼8Øw/¦6*†zÔ÷-lgÎâÇ•µ>d2z›¾ôQ–ÑwY‡êì=¬†Çù{û5³y—ý:‡Tôq9Lµzx¦Ù¾¶×›Wا×QR7s\5åõ-ø.‹ÜÎXA›M›VŠºz‘·Í?mÖSÛöDûdiÚÞ ´¹3ÝÇqk¨ ¸µiãþ'Ìxÿ˜{¶w%†‰.& Ýïé#ÓÙœʉ@&ÉùP›ƒy"^æú 3ìÿÎô¿ >ãs´òúrŒ^†Æ¦ âBeЄv“{ íTuúÉœÜì'ýp—þÑ´G¥ifº/ú?QëßL ©?ÇE´ç€9æ=Œu¿¡6¸ÿ%ú™vIJºîxú3¶54™e^¥ 'Ù¦ºÿ¥èoêši<×Ò} ­½¤½¡ï`5¼ü÷ãôõèïyi´w«ÐIÿ-½ËíuÅjŽvfµé@ê’ÚôËÿMÛK».ëü¦U8n¯¥¿M¨?Û¼õr54pOqŽã=lߨ(knä\;ú}íÿÔã}5ä~jÙ›Ü 8îÞ‡E·…§¢h4“â6)þ›þ÷ô×öÝAÞ¦2›>üöüN¬§—¡Np²Ýöâ*Úñ¦.Ÿ±2Çdoö©R¶?$ë«qUè;ü54)^+úF¥8ïl`?xµC-øýö¡nTöÍdj?^Ãz;¸#0ÒmÈöo€îVö¥©ˆóJñ›u¦ïÜÕÌjê€ÖôGi«)sJÓ½• ”šã-²—ÇW b³K®q†þÑØy,¬ÇSd]‡óKkñ‰"\ô¸[ uÝ~´ÿC1ˆbfTg´âµù¼§ia?§¸O$þûènKú˯u.H´¹ø7îZt§¿0‚ 6óP—ßb¨Þƒ¡mÍTàÜTùf/ÇOiŽùrlËhŒ5å1’i;–«3ßži#s5®c_î¡—Rw|ξyU© J ùF¦*5D9jù²´e97JZ®€ýä€=.çØÿ`™vüJÒšôPýÑÓ áFÒD¥YŸ¨nA#ҕijCjy¹¯ŠÓHy5 õŠø3ñH"íÅ:§ÿ3š}º‘^¤‰%×ù&$¾À/bý‘:9w ½{í3MC ‘뇹ÿBý¬ yÆ1˜@*¨ÿb=îItrÿA LŒá7*„ÌÍÀ銂jåR­ø|W£*Ó6Î×ì+a-—d5ÜBôÑopŽ]lËÔöCÿÁðûk›¶'id¤vq-éÅþ|‚ÄŠ¿Á>´„óÚbM -ý[|ç},ÔÇ£®}ЮÅãY²žãuÓ'‚ôAÚàyŽc¹Ç®+ÑWRõñr”ÔO"©ÚJ˜:i,weúYAxŒ÷RY¥â5²ÏA•ÐQT-÷äxèÍçÚÅg~öüš²&+ëÎÖŸŠvŸa3ÌaLS‡9~ óyLùõN7†ýNEð³m´+ÙY9¤&m l]šJ‡²krñèÃ9ÿi·«ówúŠËyÍ"žcù§ø~ËÅÞÈõÃ4*¨3‹×Õ&ÿŒü'çâmòݬÏh×óŠú©·s®m)hŹ1–Ï‹÷Xß ž»,T‡ûý⑚$–Ĉ6¡¯Y¿¥þ¼“öt´]‡Ý‡KÍdLç÷‰%¢Okñ^d³ÜÆËú13üe½WÖzÝòôõÊs®Žª–:ÐnwúsO£—»©î¨çNFóIœ3;s®i§êûŸê¾ô_‚²‡Ïʾ²€m7Jô—]‹—±Áv1£ÜÁ¹ï§]«Ⱦ/káñØ.}^Ö©èoLÓ§ýW¨¾â;Ô·ótx½<.X߯%ëcz'µ45Ÿh?ëS•¤N_Í9áyÔÖ)h¥©[tú†ñ´?²¦+k¶ÁZm&¦¦Q%R¿ßØ0ìÇ©ì'Ò/ zrÌf2Át$Îó“µ{O<æÊÚ… ë |§ý²†|÷,ñõ©ÇЛÙÇø}ø?ÿîݤ H&y6(O$w)ŸõÞóÞ#û2ìþŒp«¿« _xϦþŽ"{9ňZB.Á#²ÇSdŸçWñ—žçüÅŽ)tnÔ9îéavä#˜bvæ¿ÍüAæï$Í ï=‹@—é™|qüt÷­|Ÿ|æöÊÿŽå æ¿°„Ëw’¦r­2þ^eÁt7ÙOöïÉŸÞó·yI“Èbæ_'Ã×úǃ{G©÷}W½Ÿÿ™à—ay2ó_XÂåù$¥ÐÈÞÛ“K}±€¾¥°–6*Ž~ÎYÐ}éÏËþ\_ên›rî —çzšmÛÊž]qìÞzƒCð‚Ýë;/þýg9ÞîÂî=g}ÝSù§¼Òù'˜éžÌÿØsm~"iÙƒ öÏÐÀ?T¨œ'{’…H*V^\dÏò|{šEñ3Š‹ìyþÑÈšedõlÈÞjaì>ë/ðõÙÛsÊ­”?Ó-ŸŸcÈߤåKÞ-r¬}"jõTácÁžîoÃ_þ;îÓ&ëLYö#DÖ ­-¶ý%þ¾H>²æX@·pêh×ßÿüYŽW<×õ²Ÿ-Ù¿> Eö®Ï‚èÙ›>¿xߎ©âÇFDÖVzDòÞlúú ÔÁ/bR°çMüLÇ0}JüW¦/’Ãä“ ýøLMƒó¼ÇßËrLøjê4ÕÒ?Ù;—|°^S-ØS‡~Þ;GŸ£S–ýÈ»ä%«¥ŽSw¼ˆ5²\„ìpZ°ÿktÃr¯î¬þvüÝ¿ÿÞóÐ ­ƒ¸0胼Äñd˜òÑ<( h‰/8/“Ю îà¬øÿ%£#q¿•H¼Âo%ßð‡ò^8^"‚¬íŸ OáË ¶â÷"û¢ã¬ý)tL7£.qg‰Õ(¯ñë1#1¿¯Q(fCb'lÜ„ÄKØX ÿe»ÖÕÏvOú½‹ü‹Ü~e–²¬½²~Ù |?t!×ÚØ‹ÿ$žã|ØX?‰!¡¸Ý¦ÃmYÖ!4}IކËþ×ä«pY®¥¦¬ï?ÅsóI2ó»È+ä€Ä¢kë$æäÂÆ®1+&¦“5›*q/…c_ #q06öEâ^ #1/ç27EbX$v%¿AâXŠ#q-‘x¯"1*óÐÄ®µÖ´ëM™u(‰/q¾ ¯/…š;— ôͧ‹Nÿ8'âëÚxâUçÿñ 6 8»Ù®Å°qÅ‘¸bD|èHàïÄ—fÚ(Û£ì p.¬'s…ÚçË>ãЂ}؃¨ÈgïGꑽäMg%ºŠš„÷ý#|Ÿì9Ò6÷ñºï#±«ùHìÓÙ— âãGò1EËù?0MYþŸùæòN²NÀs;~‰_…©œÏ ¯ ëR²VµŒç÷Ðtù}%fédAb˜Ž$†×ì ã& ™_#± Á³<ÂãK\ómà«Ä3§— ×ÀtSdxÎôv!|2$î÷vlüCø]›KÓ—‰‡`z­ qL‰`š&HœÓº‚ÄK0M$n‚ÏyX5[Ab)˜Ž$¦‚i¼ ±L㉱à½Û…p¬Eß©ó¶KbÐNƒ¥‚xˆxΉv݉¨Öþê'ÿ`ÿ;¼¾«e?Xö‚#ûÀçÛ>Û¾odŸ“ÿ¿F°û›éþ²'If"}˜»èì¡OEf¿Ý«´äÿd÷ ¯ÂP»ÇÄȾßc´¬ë†Çr~ެï²îµ‚¬ó†ÿË©!ûH²‡ü/À¢ŒTxÚÕXe´U·¶N2“93çlÜaï}8wwwww×âî®Jqе(PŠ»Cqw?w÷ÒÂ]=åVî¸oŒ÷~¾¬‘•Û;_²òÍ|B€ˆŽ*\Hñ{Hæådt•.ùô¹æ¿L,þ7Aɲ‘ìªéÞ²±l(«Èª²–\#sËr2Ÿ,%'ÊIò+9Y~-§èrªî)§É¼2Ü,7È-2§lâ  1Dˆ>[ÄñD|‘H$IE A&ÂE*‘F¤éDz‘AV“eeuY_VKEF‘Ed¹E~QP4E3ÑJ´ÑÝEÑQt]EwÑCôýÅH1ZŒcÅ81^Ö¥eMÙ@YDv”Ee~Y[Ö+ô¯¤˜Hþop²€,( ɲ’,/+ˆ5²…WV\tù½ŽF ¡ûÈöz‰l-;É2²®X'›Š/e ¹Ã«&6ÈVr«î«û‰å¶¨ê«*«*\CU•wä=UMõSÕUUCÕT8±šªªZjª-ïÊûªŽª«ê©Áª¾¢¨†j¨¦¦©Fj¸š®zó%¾¬fðšk_ÙפmŠGñm]¬H1Òf¤4¶}aR'{ˆ:ÛÃÔÅ¡®ö¨•hV¦ÍØv©Æj$®²ßټ榹KimG7ß2q¥³¸Ú²j¢š’ÏVÇ‚´ÛÓN*ha‡XÕÅÃYŽp¶‹QÎâu—€Ò9¦ô.!•uŽÊ¹DÔÍÅ î.1U³_yl3Ë-P£p£¯š©Ñª¹£Z¨±X¶à7tŸ2Ø/(£íD™lgÊl»ÐDûPSãÕ—¢µš &buìCGð#5R“ÔWn±šŒUKN„sè¾¶9T+õµjmǨ6jŠZ®~T+ÔOj¥Z¥V«5j­Z§Ö« ¼OmT›LPm¶±<߸UmSÛÕµÓ„ñ6«-­v©ÝjÚ«~VûÔ~ïÜpPò<èž¦Žªcê¸:¡NªSê´:£Îªsê¼û^]PÕ%uY]QWÕ5©¢ÔuuCÝT·ÔmuGÝU÷Ô}õ@=äÝê‘zÌ'ÔõT=SÏÝ÷ƒzáK^yþùz«Þ©÷ꌧ>¨_Õo¬a #‡À  C8Çw[=–¾àyê‘0FÁh¾Ãwa Œ+ÄJ‡IÌCsÏÜ7®¿䆺n´ï&ºÉâ‰xj*»…æ7ó‘ÏŠGâ1æ›8×ãEËI¹#Â/Ä~q¿¤9¸NÄ øV"C¡â0¥G¨8JõiŒ8FCÄqq/Ñ8ÎÂå¸:wàâ¤8%Έ³âœ8/.ˆ‹â²-$®ˆ«âšˆ´ÍÄuqCÜ7ÅmqGÜ÷ÅñÐÜâá¶9ã°Ã·Ëí†åð¬…y°Â;_­†¹ð#̇Ÿà[X¥³ê<:›Î«³ë|:‡Î¯sê:—.¨sëB°–ÒQ›“7Ã÷°ˆJÂìëMWÂ2³×|B“ð+“WI¦êx¯à+ñ^ü"Cx¯ç-Rbñ ;‰çâ%N¯Äkñ†×‰·â!n €ø• coj ~Ÿh¨RIZ‰’¤•NÆ eÒ'cË82]“ñe™ÊD2±L*“ÙÒoÓa°wd˜½&SÊ™J¦–idZ™N¦—e&Ф(™™÷È v²õÛô6»L"“Ûä|ŒÙ»2\Ñåe¨Ì¢‹ê º˜®¨‹ëJº„®¬Kê*º”®ÊIxºø Sˆ2èBui]M—ÑÕuY]Ã]vWt9]Ó4v'ÝiwÎ]4MLSX7aÜ¢v„x!ãÂF¸ ›àl†»æƒy [àl…û° Àvx;àì„ǰ žÀnxj~5¯`<£™ðö ø^Â>xûá5€7pÞÂ!x‡á=>ãÕ¼ŽÀ/p>À1øŽÃop>ÂIø„ϱ-mÇv´;Òn\FÏà”îˆ;Îkøg^Ëà´–¼ðF>Jì@8£•;êNð.>Í[íÞfŸòvûŒwðI8«wò)8§5œ×.h„‹t.i‚ËÚÂÍpU;¸¦c@¤Ž Q:®ëP¸¡}꣧xHP =@`Áñq> 1 ¦§BÁ±=Æô¿¥§¸œžãëp“M€›mBÜbav›·Ú$xÃñ¦ÍŒïm>üÅæÇ¶~²ÅHØâÞ¦.AÊ–$²åÈÚòĶŲ5(¶­Iql-ŠkkSÛ•²Ún”Ív§ì¶±C¨¨JåíXª`ÇQE;ž*aªa¿¦šv Õ²S©¶Fuìtjl¿¡&v5µs©™§ÚÚcÔçžöõ²'©·=E}ìêkÏR?{žúÛ 4Ð^¢Aö2 ¶Wh˜¤á6Š&ÙG4Ͼ¡ùö--°ïlR;ß^µ]m;‹Òr,F?bqZ%è',I+±­ÂÒ´ËÐ,Kk±­Ãò´+МL×ñkºSè&N¥[8nãtºƒ3è.Τ{xÕfÀk6@AÛ’Âl+Ji[S¸mC¶-¥²í(µmÏ÷øßç—ü€_ñC~Íø u°û©£=Àù-?áwü”ßó3þ…ŸóaoÐx{&Øô¥½ÏY9'gçÜœsq΃5°ÖòtYM¬‹µ±>—çJ\‘«p®Œßá"ü—àB\Ì5¹.׿ú\‹ëqn€ÛpîÀ=¸wãNÜ˹;wåžÜ…{p7î…·ðÞÁxïã]|h{Ú>¶·íg{Ù¾”ƒrS.ÊK9)ä±<šÇó(ÇcøKšÂ3yÏæy<‹çò7<Ÿ“sýœ’Sp8œóq^®ÆU¹7äþÜϰýy2Åßò.ÌŹ(—ä"\‚‹q)lŠ-±9¶ÆfØ [`nÊ-¹9·æfÜŠ[p<à)ÏCx âQ<ŒÇù{þó2^ÄKy /ç4œÓq&NË9=gæ‚\€Ëp!.Íù¹,6ÆFØ`SÁ2ÅLynÇM¸-7æö¸À}ø3î7%LqS’ñ@îýy÷ŧøã#SÆ”2¥y*Oá‰<‡ðPþš'Ñ šNÓh*7åLYS”WðBþ‘¿ãŸ(?åãTÁ©MSØT5ÕLuSÅÔ05M-SÛÔ1uM=“Çä2¹MKÓÊäýýF¯˜×æmô-[“/ú¦­¨‰¾‹“•Lk=Äm3ïd1YÜü"s™÷¦i«/±Ñ—õ}U_Ó‘:JŸÕçôyý@?sät9CΔ³älùœ#çÊyr¾\ ¿•ßÉ…ò{ýÔ~`Á18ÌÖ±tlGÇÕñl%_'Ð u"X>muT'ÓÉu í×Ôaæˆ9*»Èf:¥šc.ÊUæ’\m®šHsÙD™+æš¹nn˜ ª£ûN”‡å!yDž”§ä1yT—'ä~¹VîÖár£N%·Éí:­N§›^¦·écúš~¦¿`šAf°é Ó›¡f˜< ÊGæ¶-eîP1ÕŽ¶Úî°š©Ú«Yªƒš­¾QÔ\5Ïl0Í&³Ùl1[EQ\ÔD=3ÒŒ2£Í!þ]€N+xÚ­UiwÓF•¼$Ž“´´Y(¨Ë˜‰µF&lÁ€IS)¶ éâ@h%è"gëÊw~ƒ~ÍShÏéG~Zï;f3mOOsrüî<]Í{ïΛ'qŒ¨ô(ר%ÏúRZ}(¥Î£¸¦k^+é÷ãšl&ž’Q+I”ÌvrËÙŽ’5‚52žõcu¤²l ¤ÚSxŸU‰Ö‰ÖS/M’ÄÇO-N?>L’@ FaŸR}€ÊQ?–²eJ‡^­–ˆ›R4ù¨ƒ¼¼*>9žu `¤2•a»|­\Ïvâ´ï î%±Nðló~Œ³… ¤dd:ò‚¥a e,u¨•8:HaïHÜ}”R#)£˜U¡³ÿgÉÙSÜA6Ó„”tËf5=d8¾Î+¥zª:™PE[´ãQQŸėb]¶†/WL^.wÄl2càRJf¢»$è0‘*W÷°ªbHÕ(9e3THh±d6JU–âìý_­Çô9TÚsã…î%£oSŒ“ú7XMÕ1.ù”¼4¼™Ç/áBS .âæüŸbF¹‹ ‡F.ÂDT­]UŸ¢¶ {Q"ÀŽ9vœ6@À%è™c×z>°ž;äܸKÁ69Ÿ“Cð97¾$‡à+rúäìs à9÷É!Ø%‡à9_“Cð 919 97’Cðˆ‚oÉ!øÎÈå±Ìßs!ë@?Xt(µý„E ‹‘+cö–½oÙ‘zhäê˜zÄ…¥þh©?YDêÏF®©¿pa©¿ZDêo‘úØøR9”âJÿ ¿Á_ãø3K¸ÈRX±ŽY¹c °#D°#p°E °(`f ŠUX°%a°Ec#b°#D² *² *²*Y²( ERD² *±D±$ˆQX°@ˆX±D±&ˆQX¸ˆX±DYYYY¸ÿ…°±DxÚc`@[:A˜U‰µùÿmìóþÞb5üÿ ÈøÿÓ¿… >¦ØxÚ•×y¸—ãö?ðõ™C!„ E(B’¤“D;B99ŠPJ¥Ò¸š•fÛ"ÀïÁõ©÷=[Ò¡U64âQ>Úk [k±mÚ³Õ9L¾¶x¶Å»~íÔiÓã°tÀµ#<ÚD è³þ—åZ¨îB©±HEôX¬‹aYbo‰ZKø.…}™ž¼¢î+Ζ{&V˜¹•4[©¯«ì½&Ïjþ¯Ãðí߀íM÷kä{K¿ß¢ÉÛtzÇì­¥ë»öß5oëh¾·÷Ô_¯§ï³ÌÇb7À¸AíÖùn”ó#|?rÿ±9øXÝMzý‰ÙùÔ}êþ3¾›qÿœÎ[ðÛb&¶à°Õz›^l3»ÛáÛÎo»¸/ðøÂÌ} ã—ðí ×W|¿Ö«oÔø†ÏN¹wÊñ-wynvóÛí~Œ{ôh/ß©ý½œßøÆ?àû# týÉóµßÜì‡e?þ?Ûÿ·_ôþþ¿Úÿ•và=àúœ¿Éù;ß?`9ÓŸ°üIã¿èõ·\Ãpˆÿ!ZvÿGp9÷}<²/Q™õdkÙH$ʰڬ=[ ï¿D’O²)ÏÖD"UŒÕeâRóÙÎH¤K³AlG$2ÕÙtv(Ù¼Hä*F¢Xq¶?ÇŒ‰Ä±"qœóâüŠ‹?¾&+Š„wcâDuK¬ÄÉ¥"qŠº%ÛDâÔÆ‘8ïéöK-ŠÄ"q&ìÞ}‰³a=GÜ9p+¶ çåGâüÂH\°)Þ‰²r_Ø5åúG¢<>åå©à¾ÂžH\„óÅ0\Ã%0]r0›1÷ÝWªÆà­¤æ¥0]*×eþ5\¦^åÕ‘¸Üùr^Ñ2W–e4«Â¯ŠºUœWå[þª#Æg=£cÕ‹ã8{ãì‡q<Ý'8›èl" 'É[ ¾@žÏØsêO†k2¿)êO¡ñTZM¥á4¸_€u:-^¤Í‹öfÀù’â]è|& gÊ;K­Ùê9+’ÓÿŸÄ\¾óh9O¿ç˱Àó²€~ ù,¢×b½Zlæ—à¶TÌ23óнW]—ó_Îw…œ+i¶Ê,­‚ù5ë×ÌÚkx¬f¯{Æß€qó·à›fïèÕZµ×zæ×Ù[‡Û{|ßçó¾9ùôj#6Âþ‘ÚðI?>û3û›aüÜlo‘+M·Ñó ¶ƒ–_Éõ¹Ýéì[g»`Ýíº‡n{ÍÕwž­ïiú½ÙÚ'׸þ`ÿGóô£þþÇO´ü™¿¨ó«\Äÿ®ÞŸôü‹Ï?%"Õ"™*ãÓtG$sÅ#Y¬z$iÉcÛDò¸)‘,Þ(’Ç·ä C#yâ†H–°w’OÙ“­OéÉSkFò4q§O¤oˤïʤïÊä™ë#yÖ²Hž]7’ç”fÎÏå_¦q$Ïï»2yÚeÕ.;(’Z—“³¼Zò"yQåH^Ì÷>Õ¬ä¼ÒŠH^Ú”Šdeµ.WûŠb‘¼ò@$«D²ª½«Ä^]!’Õ\«-Šä5ö®ÙÉkù^'§wb²úšHÖÀ½†ë ù‘¬)·w]²–:7á][ÌÍÛ"Y§+›Éí‰d=˜ëÃ߀ÞøHÞ¡F#¾wã{¼÷F²±Mؽøü›ÿ¿ùÝW?’ÿ)Åè{?]î—«Mšók.ßð?0†Œäƒø·€û!þáúðÎH¶,Iß±ÉV#ŸGéÓº$sÞ—60¶ÙÉÇhÐÖvÎÚáÕž®íí?®nù;à×ÎŽâ;áÞ Æ'Ô~BŸ:ëM—¢HvÛænúÕM\wx»óë¾/’=ÄõÀ±'¾=éÚ &ïºdo5ûTdk#™/>Ÿ>O:ëKÃ~xп§è;Ðþ ¹}'Óöi9†ˆBƒ¡Î‡ÑkÜÏè÷Qr„ÜÉQpŒÁsŒ}ßÁɱöÇ;Ÿ ÇDZM‚g|°LVóyûSÌÝT{ÓÄO—{º½é4ƒ&3Ô›óKpš¥Bœfâ;S/gñ™mþ‹è_¤ŽoÛä\Ì“c>þ ÌùË4\H?Ÿ¡ÉE´YìYY¬ö’N‘\ªþ2y^ÑÛWåxUýåp¯PgÅÑ«~®¿RÍU°¬‚ó5^Ã{µº«ÕX­O¯Ëÿ:,¯ÃýÞPãMùÖÐa ¿5=™þ{%ß²~‹ï[«#ù¶:ïˆ}Ǭ¬…u­Þ¼«ÇﺮÃå=¸ÞÓƒõž‡œ ï‡òo`åûHüGôþX/7©±‰ß'®ŸÒà3õ7ÓésëÏíoQo+¾[aßFÏ/èù…þ|i½ƒ_™…¯è󕽯ͦ÷Xò[Ü¿u¾KÜ.Üv·ˆä9÷À¶×Œ~ç÷æb~àû¬?:ÿ‘Ž?ÉÿŸŸh¸gß´É_h÷‹yúÿbÓ‡ßáûÃL„÷ ¸?aùÓõoyéÍa¸Ûû‡i©(ÅÖD*Ñ)RÉâ,ŸíŒTj¤2%X¶9RÙf‘Ê¥YWæ¬ÿb"u̶H{(RÅ‹±Õ‘:~J¤NwˆHX—²ƒ‘*‘ÇŠ"u;ym¤Ni)ߣ©S—Eê45Oç[ª$[©3Z0>gŠ+]9RçD¤Î-ˆT5Îcçó+Û?Rå*FªüüHU[¦‹ä¹D¾Š›"å·yê2x/s¹{ß•©*Õ\UÝWU£*ÞWÉq•ÜW;»šWÃwµ\ÕÔ­†£whêü®•÷º‘òîLù–LÕ8jÖ5{FêÆ¦‘º ¦›øÕ–«Œuøÿ ¿ºpÖ³®§î­Õ"U_ÞÛªDªÁx¦N ï€ûNÖˆÆwÑïî î{äh¼"Rÿ©¦¥ÿûGªÎÍöDª9lÈõ¿að³'ÕB½‡èÿðÆç‘Ú 'ïÑTKg-alEûVx½õ¶ý>0ô¡SzôÁ/ŸFÞ¡©|ü}+¦úÒ¢¯XïÓT_<úâÔç~¸ô“§¿XïØÔ˜È9ÿ@ûƒääl°ØÁæùi×!ö†òFÃgÌñpÚŽg„º#áå~4=FÚ¡™Ö3é5SÜ,\gÁ6[f»9+;Gßæˆ™ãÙšËgîHÍÃc> óõn>ß´Y÷|^æÿ2ÿ—ù/Ä¡Y_äÙYÏb}_ÌoIýH-…{)NKÕX Û2ï‘er.3·Ëôëµ^Á÷U{¯Êµ\åò¯ × ³¼ÒþJ<Òeƒ|åýˆÏGÞ=ãø±óMp~"ç§êæÙØLãÏÕÙ¢¯[Ìá>[ÛÿÏÌŒÿ©­æh›^nó¬l³ÞŽçvšlû…ž~éìK÷;`Ù!þ+g_Ãóµ¼ßàü9Û ÓN:ì¤í·¥þ? Þ]fmmv˱[þ½fî'øöÛßoÿ<¡Ñ¯Åÿgþk`; æopþ®èçA¼ÿ¢ãßÞá‡ÄÆïH×HÇôH'«E:U?Ò鵑În‹t±¡‘>vE¤‹ô Å"]ÂÙÉ•#]² ëÉæ³C‘>µÛéÓê²¢HŸÎçô1ÿµRض?Ògä±)‘>Sü™"}V'¶:Ò¥K°‚HŸésÄžÓ8Òç–fêŸË¯LÖ”Éw^šµ`ê¯Þù…‘¾ 8Ãá¹|{§Ëî‹ô…r_¸9Òå‚ÁPŽo¹6 ¾rbËó+¯NyØÊ[WÀ½ ð¹HìEr\Ìçâ–LÌ%k"]‘&•ÖGúRš\†Gåþ‘¾\Üåp]_•R¬=“§jYÆ÷*ø¯vV ÇjÖ×8¿‡k弎Õá¿ïË"}n„õFùjÁv“úµá¯­upüWíH×s ÿzjßê¾>ü·éÍmƒ"};]ÀÞ@/Àš§fž˜<ñ õ«!NwŠiÿ]0ÜEßøé&°6±‰\MvDú^œî¥Õ½ü›ò½_žû彟33ÐŒÎÍakN›æâ›ÓïAëø? Ç#j·R÷Q9ZÃÙ†_|ÛŒôcúð,‰iK‹vp´wmo–|Û§‡åñÆÿ3ý\͸wP·:ìù[Gu;ÂàÿYºS3F'`{‚nOˆtgØ:›¡ÎjvÁ¯+>Ýœwƒ£›>w¯Èðè®vZôT»'¿žð÷2g½Eº7Ü}hÙ¦|Üû–df¢/ý\û;{Ê Äg çg ýA° Rk°½Áò–ÿi½Âw(}‡ñ{†žÃá¡æH8FÁ:JžQâG×dr¡Í³æj}&ÐsRõHÈë·BzŠ^M£×49¦éû ð¿ æ¼§Ë9/ªý¢õ xüfH¿D‡BµünHϤÃLXfÑj g›‡"sUd]d~ç¨=‡ÿ\s9Wžy4ž'Ï<ºÍ7»óÍÐ|:- ï˜^–÷e~/ûÐÞBs°ÆEf`þKà_BÛ¥žû¥p¾jýêÎH/Çu9<+àY ãJüWÁþšú~[¤WÃøº|oÐàM<ýnH¿ ç;líQ£åZÏò»4yŽu°­£ã:{ïÙ{oS¤×Óm½½õp¼¯ïÃþ>ŽèÏfá¸>TÿC=ß çFØüžHLÛMÖŸ8ûD¾ÏÄ~&çgü7Ãõ9¬[qÙ®¯_ŠÙû×øî„a×Q“{—Ú%v·wçnvðÇÙöȳ×Lí•Óoˆô^}øÎ³ø=-¿—ÿ{ü÷©¹ÏÙb~ÀïGZý$Öÿ„ôÏðÿLÃ_èõ+~@¾° ÿµ~£×oü7+¿ë߸ü!÷p´w¶ÝÿIï?åùÓ¼üë_òýËß´û[ü!y½âxX_Óæ0<ÿÀ÷¼ÿ˜•#ö¸?ï‘‘‰ ¬ ;™D]¶#2É<6ÙKÙKE&Ì}Ú~Æ}¦Zd²•YK6?2¹}‘)VŸñ=®)“»xE–™ãå<¡:™Şؕ­ŽL‰ÒlsdNjÆEædç§lŠÌ©öNß™Rj!÷™+"s–ø³\ýÊ”^ÆøŸ]“Ma#sŽØsùœ«VœÊŒ`öÏ“û¼õ‘9Ÿïù|/Pÿ‚N‘)[…ÉaIÖŸáZþrr—/Ë #S]$—ß=™‹àº¸8«™KÄ^²!2ᯈŵ‘©$¦ROÆïRº\ Ï¥ê_&çe4© ûåü/o™+pº+Õ­"W5«ÊãTæ*±W¹^M;¿“2ÕZ0Z]“f™Xÿ³2×ÒøZø®…û:¾×ñ»NêøU׿ê¸V=¼×Óûú=‘©AŸjÖЛšx×”¯&¿ùׂ­Ÿ›ô±6¿ÚCŽ7ã³Z7/ŠL˜ëгNAdnዼ·à^¶·ÒæV=»ÍÙík"Ó€ ÅÞÛÎÉÙHoüþÊ4¦U¸î…ù>¿æÖ¨ÿ°:¨Ó†V¬5^þWeÚªÑνÿC™juäÛ æÎêu™®´ëªn7¸{4btëiNzéso±½åô[(ÓÇ~>ÍŸÄ¿¯ÚýŠ1{Ô v ¿°ú‘¬oOëë†Ê?TO‡éá0~ÏÐm8‡ŠÌH¼F™«ÑrŒ¡ß1™q0ާÛD30‰O®ÏÑk2ŸçùLÑ‹©î§â4ÿ øO§‹ß%™åša†g¨÷’…Îfó‹ßë…°xggëÿRóù ­_¥á œVZ¯‚í5øW»]ÿÞ„sY{›ß;ø®Åç]ܽ3ëá{Ÿß´ùŽ êl ÙFçí}d–7éÉ&½ýŒŽŸÉ÷›K18?wÝâ|‹z[ÌçVÚo¥ß6X¶á¼]¿·óÛ®__ÚóžÙáê=ó•Z_Ñ÷+=üƯù}íþzzGgvÒâ[ñßÒï[½Þe·ø=|÷¨·‡ž{i¹—>{qù¾ïìÇç{œö•atü6?Èõ#®?áó3¿ŸåúE/…áWsâû<ó.¿áù;|¿{~zÖŠûSž?õë/1óÿ[oñ;LÓÃG¯øþC+ïØÌ‘b‘E‘M4l2Ùt£Èfʰõ‘Í6‹lnmd©Ùc ØÁÈÇŠ¯‰ìñã#{ Èz?fK”ŠìI"{r5¶-²§EÖw~öT¹OmÉEö4~§S«”ZgØ?«dKËq¶ûsò#{.Ÿ2Öç5Žìùò]04²–ej•[^Þ Î*ÌìE¥_ï¹ì%%ŸJm¼—ÊuÙ¾ÈVnÁ๼0²WTdâ®t_Eýªâ«:ó­Æ®åÚ1‘½Nžêìzg5p­ GM÷µÔ©µ9²7ÉQ[Ý:|þµ,²·Ôl=¸oµ®Ûí´hÙ¼)‘mÏ4½SÞFxÜU3²wãq7ýî‘£q}¶3²M`º¾¦%üÿ©ÙûiÞLíæò5W÷u´ßB½‡FDöaµ‘ï¸[Òºå¦È¶’ãQµÕ»ÖâZãßZþ6¸ú–ζqÿ˜^´U·­~¶ëÄèÔ^¿Û[?›oçlG\;âÓqGd;ÑÆ{+û„YxßÎÎ:óé SX»‹é®v9zˆéI‹^j÷‚Ë·n¶7Ü}ª3Zæë]¾¸¾ðô“¯ß†Èödz?þÄp>@®§ø?EŸú:PžAò Rk°ùlíû7ûteÖžÑrˆ|CøYÙ¡ò5'ÃðzFîábGÐd$~£Dv´šcöGv,ãäG«ñb&ÐzâêÈÈ1Ï©â¦Á5]®¬Pý™|gÉ[„ç\½˜go¾\/ýPìbº.Qk©^/Ãç}zUìrš¯d«§GöM>oÁÿŽ^¼+×{0}ûþÕýØý&=ö˜ýTþÍzú9,ÞYÙ­xn5‹ÞUÙíðoÇÿ ¾Ôÿê¥O_Ãÿ MvÂú-Íw©¿›Ïùöz¿3‹ûÔùQžýjûýŸ= ïoüþsPÌ_ðÿM§CæË÷Yö°ú‡Õ8ŒÃ?pY¹¹DI¶&rÉN‘KUgöÓ-#çý‘Ë´a‹"—­ÍæG.W™9/fÿqÇŒˆÜ±¥Ÿãø¯¹ãå8¡8›¹[D®DYv0r'YŸœ¹SÄ"þ5K6Ü©|OÏvDî4ç§íÜéS"wFQäÎZ¹³Ù¹u#wž¸óù\°)reÕ¹Pl¹‘+o}1\•Øeìòtä®è¹*|¯‚©ר݆È]_?r5œ×P§78¿amäjÂX³+ƒéÆÒÌýökÙ¯Õžñ½©ë9¿És¾Urµå¿¹ S¿Ÿ:›#W·Yäná–‘«'O=o¥Ï­øÔW_½úrÔ?¹Û #w;n—»n äj ~ƒ‘óÛ=—‡cytÈS«¡< án¨vCØîÀãùï }£=‘»«ƒñ.µïæwm«ÛÄ^5î…±iEÆ÷¾|æü?úܬQ䚫ߜfÀó œéÁë#÷ˆþ=¢7ˆo K[ŠiYÀ¶E®Ž­ø·²~T|k>­3±mð{ ‡¶pµUÓ{*×Öv¸·wßžwUîqy:¨ÓúÝžëäþ <;óë,¦3¬]ÔîbÝź‹uWë®Ö]­»Zw³î7sÖMÎîôêŽ[w3Úþpô 1=Äôã·}®—z½`î¥^/u{áÞ[ß{Ûë c½èÃ/V¿õsùräÓ Ÿ¾¼OÂü$½s}iÕw_äúѤ=úÙï§§ýÅö§Eº÷3¶80 àõ ˜{Ês0P~ß‚¹âšÙAbÁ0XÍÁzþt5&ïÓö†èÿë!° ¡ÿs3” cÏÈù ‡Ë5\½üFØ)ßHÚŒ2W£p ×h¹GÓh´ý1¸Œ¡Ï˜Ÿ5Ïš™gŵ?ß±4‡Û839Nþñtëxû<£Æ0õ'êÅDÜ&É1Éx'ÈUCçä9üŸ“ç9±Ï™ÉðO÷¼ù|Þ¬OQÏ·jnŠ{ïðÜTë©jM…{ªþLsÿ‚<ÓùO§é‹z;æ—¬_‚¡PL!<…xÍÔ¿™îgzÌ‚g~³`š¥Þl}ž­Ï³á˜­V‘ø"ùŠè;‡6sè6ÇLÍñ?si=WžyøÍ£Ç|µÀþ²½…|º.R{ÝüX­Åú¸DÜRø– ŠÜ+ü^¡õ«t÷%·¦åü–{¬wŸ°¬ä·ÊÙ*9W}ð“$Á¶xÚ¬½|[×u8|ï›ØÀÉ ’ ¸@â .‰)ŠÔ ¨-Y¢%Y’e[Ë’·Ûq,¯Ä3‰g8‰“ÐN-5ÛqFÛ¤iÒ|þýòoœÕ¤;m·Mc‹Ðwî}ïapHrjÿlxïÞ‹{Ï>çž{.âP!ö'ü>Eí¨õ¡û^A˜A¸t.°vS® ± fX<‹0Ç`n/Bˆa3ƒ8Äk8~iµÂSi4âE£8à˵.ì& -´Ó‹º—:MçŒ}9I²×ÆÂõU;aëaS­ÕŒÃnâÌ8õpé¶X8db¡¸­­‡Q^Â×F¥`þ{Ó½¢uC÷ž‘¸?ÑÖä¤ãFΛH'k3ñLÜ«÷¶[üq—3^%IUq§+î·>Ç~þÒºVvðÒW¸5ލßê™Î¤W·×G#Î=7[êbÉl¼¹]rHŽy³Ö/IþZèHàv¿ýTš›‡UáËÿ^ø8²þ é‘5g&O&Æ< 1R‚mˤZ»Å$¨Ñé4¢®pAÔÑÏ*Hƒ÷4BFäD‘\І9÷œ0âðfIJ†<ƒ¼¤°-ÀÛA+ÎYN`œÂA¶ÇMXäžþ€Û¯92ÿ«ÃBÐõüÛ/^ï}Yg׿n+î*|Gòò¡Kße$w.˹éo¾Ä'à·ã¨=1:çÄ›“Xä}€C–ßeû}9«ú„Áb1¸þi¹m‰€o‘ÝŒxÞç0Ãó‚†Q` a ˆ¸#F`0»D[ µåtΉQ]m$äq»VÉdyÇq­9Á·Ö`†5§Û2ÙÊ¥»L¬ÃîLµö0%(Œ¼Ï?ݺ~çH{_%<ð?ÕÛâAGsÌYe7\:^ΟúœU«s™‘˜Þþ{^ï5x"õ¢FdæÊ`vù_âÿ`fEnÔ‘Ë:0¦X²†l œF˜†E¡ @¯!8ŽŸ1Ïù),Ñ,ZwA›òŸº"òGdv²ûæŸjÁVXÙŠÿsþh]¡ýœ6ây?ÏŒ<&>ôxaâ\aàq¼·ðm› ÿ#ó¹hoübÑÃ@K ÌÍŽjP=êÎu0Ëj1Ëpì ÀMÊÓìN c#p8ª¯ 5Žj·:Û‚K"%F«1å7 OzŒєü!ÆÜü©­þŽM]÷¿¿ãÈó×ÞÆìàî^ÿíg ó¯ç÷ôÏÜû·|¨iúöµÍ›W÷Ø|OÜ:õÀî ³¡ÐîÙ˜ºîØÁŽu­7 óÇ(ùw\ß‚Ð@®Ïsg1˜~ÄrˆcѬ€d/À”™„Øó° JA<€Q<ê÷¹’H§7ˆæVH§æJä†Ó%Ê_‰4qØ«²Œl†ý²É02ü£GîøácãSO¾~Çíßé¬&©®×Ê çöµwÍ~`¢u<ßé·`«alÛì®—~ÿÁþþå]›×Li ‘Ddèì7Nžzõž!“?Yc’×Qx®cý¹Ì)n,&ƒy¡CÏ qk×oÀ"îó-~…•W “5ÃêŽxNX°xq„¨#µÀtšIª 0]vA-F"Hå%úôÔžÓ97̽¯}E[k2VW¹‡†ñ°Ž°!WÏ6²W„gŠ‚ÝŒ²–ÍfðÛ‚ÙéwÔÖM¬ª=ú¥;‡® g3 $ÿÕ»¥Íã5Ù±ÆøHµ$¼ŒðDýºS†ý_øç—…¿alÓ^±Ó:ÐVÓ²YL‰È¹÷_¼|a@TóQâ‡u°,e¯ÁÚ`„³%0™rÒ¾ØÖ(ìa¸6~ø{ ø»÷¾ñá_é8øä¶Â›Ø¼íɃÌ#ÿîsÛøÐößzìÄ×î¼Ä ÞýõË—ÿ÷ øÍîzЬÂÝð3 ¹:#³ §ê,D‘¢ÅÈ %)*Õ%M(tô0õ£0†5æÈwЬ Ñ¥ü„€‰…és0Ž‘€§z¤§âE´'°æMþƒûÈüEü7…:fžGEé£Âø…/±Ïç(ú%¶ Tècú:k‰°×2ª°¯G<D^ óf'¨b"óžÐPQ®ÕUˆýLˆR;{Å^`E”€£PÄ?¨K›U0%ÅQý% ,./\Tì±’äŸÿ>‹Š‹ÞfÕØd©_õE8èý‘7óÞGZT›‹–Á››à1ÇQxYj-ÒH ¶"¤Ã{tþ‡øBø"»ýñùÀ@0ÆiÏ,hF™\ÊŠy¦ø½T+Û°áXž›QéÑ^¤G°…kjC‚#-aÕ á¹x#[b¹LVÀtƒ)Ä=·ã'·?õûÏo›ùô|põ‡¶Ö¦ µ.Wvõìðì§çògæn˜º'÷5«?pûðcúýK»\‰žØ:»ÎdŒÝ#÷~ãØ­ßzßp<Œvó¾‚ ËåG(r~Å6™æq…qBôÌ6–0@{H)îÈÅù.^dŽ]d¦æ_äCó™•ÔæÀðÕ0ž ¹P:תAÏ3X‡ð4Ò™ c§µX;PhBùÇbõ™r„%ù§ÂéT:å¦ÁJ¹G|ü(ü~öÒÿÛ€‹Üpi%™{ñ†B¡0ñgdMŸ‚ÿm…9°Ôn£l(¯Äv¬"…·^¸@PmÇ _Â0äGGó Oø„/ò‰›¹làðBWÔ€cxñ³emTFyÀŠ‘×]¤}?ö‹ €l‰æñí%š¯½\šÐßþY‡.ÿŽ ¬Í®Ñ¹LXRŒL0YÒ¥„À`â—' ô©¾š~ÅŒå$ˆöœíÁ  ˆnRŒ·aÓ]ëã«ëžel6c]hó¶ øÏ}þdNo¬ÊxÍvóãŸ`·˜žšòÀ<ªP,;…XQÀ~`HÍð˜ CÁ=ÄX4”… ©ãÜ &¨ètˆÄq#ØM„Üpk›?f“ָÿÙ‘@¸1Š?|>Xc*|Üæ/\`~mµãsÙæÿ2aµ´Ì?ì |{AæQÂ÷ð‘GîœVKi¯CÔ<‚c­‰(aòÎáwæÇ/<õ”Ü/¨È …s³^G„;bˆ9X9„KrÂ8ê°‹<•bº­ÃpÇqÀ]ø2æyç˜7?uÊo|¶Ð÷~gÓmnÃ8ó ºäóð;Ûùõ(¬{ƒ|þqDCèe¼åòwÐøè\àÑ'ÿ*ÈÙIšöò ørxœËRiS19pÀ4¢ ´†ÆNDš?!ó<ð‡ÐLôÁ^6s‚ñ,‡‰ ÉÊæcБ‘êÀ/}ïj ”±b!Cqìt µ€Ù¼Ñ¡Ó9 øG†*÷½Ï±L!áJx®B@8¯ÛgÄÿ/þ¹«Ùãmv²Q‡öퟱ߰·x.ýy]›mM\êãCZ祟ÂÚù¥aÀЉ<™\\A—fc#¿×ˆÓ„_¡€1Bfñ–Ëmf¾Ç°ç·8ü¦ù Üß·ôöKuî&;ûÌÛ?3YÙ×]õ¶K;¨~|¨ð§ÜIøA×ÎEÆ:b”‚KƒÂûʆò>yí*©‰CI„¡ªU†*k¡²4˜¾è ºƒÑ÷ªÍ¤²U7V'_\w20tÃêú˜•wôáÀàÈüY YÜaqšÑÓÓÍi¥ÃhÚ¾³‰ý¡²"XgÂÕë$2xäòïxô’ ¼¿2›¨BÕÖÖ–x½¤‚¨aTfòñÜÆþü¾÷ýâü¦Mçñ¾û~þÑ_î¼á™;ž9ÔÙyˆü½¡¬¤ÿ~y÷î—ÿûñÇ~ÿÅÝ»¿øûÇN|ãìððÙoœ€¿C`ÁQÿeåågy‘߆¨ u ÙœÍƒ9¦-UòÔ¥ùI¶ƒL´-)F釮Úmzz:gCˆø!°ÜªZÙuÖ;ËMÛt¥s—fÐ¥ðÑùŸã …ULÏ÷ þÅmÇ^{`õê^;vÛ_ :[·Ü³nÝÙ­­­[Ï®[wÏ–Ö¢Aø=‡v¿$“é‘=ß™?|âÕ÷Œ¼ïÕ'^½wdäÞW)býÈøõ¹8byA, oMzjK äqIfhf J‚D5•êT‡ãE¯¼€Ožxéæöîã_¸é[ß:¸2úУßÂÿ\pò¡ŽýOlÝòäáî‡kGötÝö~øm}ìoà·-ÈG½` ðùá§"àÁÇ/ÿyŸÇ&AK3¸"è0°™RŽÔ3Xuá·O¼Lf0wÓ·¿= 38÷è·ÿð>ôfÅÎ<(ÓádáMþn CJ¢D®VË2˜„Ï„%U^S€¾9‘#¢"ìŒHõ6+\Qž˜íÇSOÛœ¿¸á®«ˆ•Ï~»¹p«Ý…¹Æ+IŠ3®àæéÒšk2)‘jŠV4h «¼ÐÚ Ø•ãD1?ˆõ¡1&Xœ=ñ…3ukŽ KÕ&¼ócMÞÂ_˜ÃáSŸ¹aU´ðw/3¡ùŸñ¡Ôö÷M žØ1hÕûlóÿů;mg£ÛÛÿEÖ»ÙË¿c¯Øv‘øMX–~̱ . NdÌ|/L•rн(þÌ@ÇŠx4ðyÂABlB™g.8ÊµŠ åt[£ ²ë7{Íu“ÙÉlUjrÿ¡ý“©žÝsòûÃiÑa²5 íìØÜT^µí~dÛõ_Èq“U5ÑššìÊúìH*V›ZsdbãGr[Ç7èu-u¡žéÎÕ©p¬eí±Mƒw_×=”'ë+¼@×·}]å¶F,òfXä*=ƒ˜õ46â[üÃŽÍàQ²¢@a!XˆâBX,ŽˆTvÑ3†!‘%ú.ŒˆLNdÓ­Í õ—Ýj4Úˆ7.Œˆàå +.BÂÀÈÑb`$~Ó7òW€vr1Š’ÎLØ– ’çgF—EÂúEX[sÿÁ•†¾ò  Fn3ü"Ÿ0;ÁQO¶‰G"ˆdÞJE2pGšý«Â wð¡'ß9Æ=ð¤LÏê8f"­À²ç0ÕÕªŠnµ™Oë,ó¬ÁÃSFÇ™Â÷.þÓ?Ñß(Ü}`~ó±øÌ“ÔG½ü&µ =Äg„•ÂÙm&ƒ( öðæD”  lCÕ²!«æ‘ñ3’F΂¸ÿƽ#¾gwß·Ž­¾ô«£¯=41ñÐkGáóß­»w–þfáKô7[Ñm‹ýB9X^ô ý²¿\"3A¬p#¢)_ذ"LnǨ)¨ñºmVê#¶âVͱ‘U–“*-R,ÅIRªÏØÕ|ä‹§ód‘ãtÙ]7>¿W x,.d린°þ_=¤.[Çì+ïŸ6ê«Bq§ìXR\ cqoÎ¥¡ÛI+áfb›7À ²Q;¶ÌYÀaá1wµˆ .­œƒ!àz¬`û8r[/í«YY[»²†}Rò¾MývÐ~£ŽÐeÄ)²Äsdd€(P’[jè$;%ô—lÎâ•~×&ÿ.–7 pØð”+È_6“Gù+í_=ª1 ‚A,ða׹Ÿ(|›×òœ–/¼3sÙ.ÝÒœ«i÷°÷K^òÅ­®^áb Ódm ÚlAÓ¥*˜¯`Róõ!ÎkBL (ŠÇRª ‚‘­Å.Š‹«•Ý[AÄtgà‰Â=¯Ø­µX§±™>_¸ ?ðŠÝ©)ü /~­qJŸa>ÆÜaÒdÄ?7šç˜Ÿr·YWÀF1°ÿžî Tå<¬mLàV²B+ë9:[¤˜± …Ùó.¿î—’W§óH?×T»>Æçßbô¬Ëe+ð5YŸ/[/™}”VÀo=Dí£H.K.Šx9Q”&drHv*?ì"Ù!ċѲÍ?ÅÜpé¢NÃìÿ ʪÞyóña³×ÀY'öÇÔå7Ùìß ´£ûsú¨‰&ކR áZPé Ë1³ÐVq)†¥ lLA@“˜ìR‰›Ð°|SbÜiÕŽ™Îyõµ4×·'Ú#!²Ý`³ê4¨×B†bMØÄª–M¶¸m eåàˆÜɦ 3V3¬Ö[Å;c+×nɶïMô>ôëOîÜÚ³¾'é²z5‰¯]wdžda aU§ÿ–ïkôÔwX<3álÌVÓ·³¿ãÀTš=zS °šVŒ­IïºorþÜAk°¥ú{¼¿¹¯6– KV… ìk«>4~.‹"0ÄxnØÌhu,j©Ì"*×Uñ˜E Lë¬ M–ãA‡ÂK­HbCH§euÄê¥f½+oÀZ­"´4ú éÖ·°³¤ AsåQ@ —D_ ßÑÊ‘þ‰üDOWº­)Y «ýˆÁ>Üg\ ¯ŽŒT)VŽ–ªr¬Ë#^ ?¸´§XЗ£êû²ülßWAZi«gàE.¿ÉÿšâomFr†ÞvF#úÀUŒ²YŸFQ5À3àα Ã˜ðŒÙÀˆ"šÔš®¼$ŽìÖ9À­Ëõ^k?Tì–Gz}q„é\lb-FÖ¯Ý<±yte¾¿»3ÓÖÒÔ>›ÏcµõZ‘ ÆBvâˆå,º07È—ŠœXü ("‚Q”ÆðN ‚«À`/°ÂüG4OŸÉoM’ÐX¿c¶õÆ/g¼Sûnn8¼¶¡÷Ü?¼°sGïÆžFНus‡n>hûLákÇ?9Û´c,»c¨¿Ô¹5_+àßÖtN±¿ì¹åÓëª>ÿ™ªP•VÓoZo8Ü5•v‡òû;ŽlêàšŽž Õ†(âv­ŸÚ9ÿJÖN®]?ŒZ;º?ß³ ñÈþŸù‡|ý9­ÏkÚçHn‰ù‰ìË‹Xñ\$*ÁL À‡ÄPñÄ\ð^À4\DZÉÆ ¸ÆnB5~W؆_q€{¬±&R ëÚV#B‡Q"`'–êÃs]OïYߎTÇÁ§w츳UÇÅq÷Ä­©ÆCýï{øþ¡ù/ OöŸ~éÈñ×Þ¿ze߯(»aõP¡¶7õÚ«/¿r}ÖÕÁþ5 ö\¦\/I (óM}…n¥àÕÚ0ÙÎÀ‹· é†![îʳXc6™̓­ý×åÃÑ]«ZƒV»!Û¼³uÇ÷v~~ÿžu°!½Þíw§¶ß·~ÓýÛZª#ÕRŽ nàίœ¸áK÷Žæ»È¼Áç óD HK˜Vô,âóX¤pñã’ב\¸Ë#ŽGÅíXWžêq < I…I-Ýž9„Ëú¸Ô^4 ¢¯·µ¹lûujßÅöë’›¯g£ÝgÄV ÅÆÎîÊ* k\Æ™'×KëWù3«’Ñ™WqR3zÇËGd¨æƒ}`DÓ]éC½oñ¿F3è…œk æ 3 óí‚UàvâážH/¤!xÁ0p1åA&Ei°JRz-#G¦rí×ÚOJÅXNÀCã«GG†òýa[­LžF÷µ§««‘‹"¿‘€7H/`µ°$G Iï|„܈·ÔOžZ›Û¹r…»ûƒ»Öž\W7·ëºÑ;2sö]é5-îHǪ˜õjÄ®Iß”[qýD‹/à›ÿVÿXzçý“ó0‡o85Ò;ßÂgj‘Ž•‘¦c1‰Úi?ßüìÁaÙp²mÀ¼qAÒÖk‚[8ÉÓÖò{0Öª/¨]@útÃH@ÕD“ëÁ»@@F^0Î Ò³½Œ *´ENtŒ¬…dü÷]Û Z­9¿@¼Ê þ1c°NWIÓ9ÇæMë§&'V¯êÏUÒ0½{Ò(íW™°2*íXýDR«Ï‰¤Öæ2bÎTí:_ø©Ê"ùLxø#Ž€±ð_&/©RM]n<~UªÚnêU¨æÒWuµX ·›=:}T¨2[ñ#vc!¹€zí¼É¿´³ýÍèœÐÎVpÎZ|`L®ò0ƒ$¥q婦ìé´Ü!D¬Ã¢néX¬c÷ª,lD‚Á(ÌhxðN “È`Sœ|¦Uf0È ²ôª“»ò(`A*c]Å¥†|ïݳe󺵫Fóád2 ‡%‹INÑ’dã#U4,å½ölÈ`•œ.@®ø®ˆÃÁ}k ÿ“Ix"«Æ¬M7œè?|ò ÙñaÑbÐ8[Dzý{ò¡T‹7éh¼&Ú¨‹¸Ü Ó÷lZëxŒ}õŽù?íê2YMÒT}ã]'™)»mÈáïëï¯nï$†·¶vÝš1G¹«ÐEr{s߉­Ùô¦#]²z”âün|VFaÕ1ŒÌ8¿~-càf°Þ ôûdÅ©¶Ñ,Ùæ ¯Bÿ´B*cË Ù‚³E!ó$2›—"5\Ø`ÐOùêÑ(3½ò¨@:ʘ‹Hg‰!së–ÍšÊ`œ½æQ‘Ѩ ²Èwû™[n:°Ï®í[§7VP¨ôQèc!µ„¥ñSnë}k¬eQKK):yîú®÷’œ{‡ôåaÍbx”ºóâ-KÑ8ƒVûq°Ý](†Žä vÌ3U>â»2Eóî:*Ö¸‹êh jD!¸à½@ÃEsž†œ7FjwÌs:¬€\Ø¥QC»Z`ÓGIŽQõO;mnÆÚó‘ÙÉ÷íhk?øÌ®éÛZæ k&oM7ÊŸ}8سY²Ý2º¥ï¶¯œ:ø•ûÆ:³Œÿí£·¯Ä?;þO¶ß·±Žæ¤/ÿŽ{…˜ä§¥IFAö_hFK2tîͰn_¶î¨OvR'0•ËÓÊË/Tj—n¦SÊ¡P]Wëõ0¸­µ6]—ÖxâÞ¸Ëi“´"v3n½7Añ1óÜ ø¨Aè¶œÁµB$ld5Zu^1†¤7²I©UK< $˜ÁþÓhJ»˜¹úeÒÝGÒº¸û˜³µ±@c°ÑV ’¤#½+I¢M»ÕlÔŠ$§P[“`+b2 \`Ë“`ãÚô¡C=V÷üQ¦qâÈ`n[¾QÒ[„ÖàôÞÒ{_<3Ô{ü³‡n=ge¡ÜVö—G>v}oËý›“þˆ_«‘õéÙoݾ÷¥»WÝñð™ÎÝõd=… t==h}KÉ»o·³“¼)5ï^}€wÊ‚“XtE[‘ÀŸri'h¹=.; rÇUŠäÅÊ”ÌvµIÍ$^¹7pq)òVëÅ(ßß;œîlOÔÅ" ºœ4G¿÷åè—vvÁ‹¥¤}Vñ¨·ž”E饡ž¼kDS ¸ýµ w%Ö–ÚVa½é¼—Âl\ƒ‚ yŠÙÍŸFÕ(›k“, ‰óÓ¯&ÄÐ4N&èiAN䢬FÕ’#uŠöDJŽêáPî•›?ÕΟÙíð>šœ:1ZÓé㙆h}¾Ù‹ÅÂe¶×È8í#[§îžnÒi7¹xgÛúžMw\z•лü²­üÇÐ ðþ?(ãÜÁ B¼ îîJ[8=±¯Gç¼ ^êJ/ésaÑóé"!ÄO’%™òFÃ04#Ü%ÆÂLâ}ì¤A«aéΊò{Må=ÁR:¢+t#’:%"}Ñ»èJV²×ÔÕ eh¤L¯§¸²õ›Fs¢ntõT«f)® îé `]áÒ\a/ÿÁ"e«Ô\!WÊÄ­ÄùW+i›wéŒEFÈÁ hÊvî#üûPOÎg–ˆda‰„׈LãàÒDžÃg޼ãé;–н}Éö‹›N"'ƒyLH€ˆSžä¯£ AÆš^ÇÒ4ʯÕŠÍrì×:rìÝœ‹‚C2Á)}–l9­¤â¡4J·¥@þE(=®NOåšPÎÖa§Ãûprêøªš –D¤ŽhÑÝY øÞäÅìü\áÈÒ´0;u׿&ûFú­w—‚wׄ„jkYÜ ä•kÌŒ…aKú²ü•V}EŸ žª,Q¦ñˆ³®èòQU"z]¥’¬+ëRb±Ö)ï£; •:î*`NåšíÊ­UfQ¶­Õ¡*2ûQd¥7óß‘Ñðà^Ew]çð>Þrð³ »FÆ›XIª_ó­a9eeœÿѵŒGÍ~Àã|‹bùȺG‡»@½èÚk1+jŠ˜tW¼¬ÅM;fDQKnY--~¥â´ét‹•‰ÉÀ(iª.ù3šÐË aç“:Ë?žYrTêŽ †¥{Çäýt‚d2z·#ÀOsHÇpºJ­qÕŽ„ Èöã¤jˆÁ\OgGØ¡( éÝ)‰Å»%%q5½P¹ËQ©®¦¾·hoc*¸|™æ½ÆßGânß%;Eè›ÌvX>øf &Ñ w ­ÔéQ>‚3©ýZAÞm¸±!Â~ì—$[4ìÒJ~ÛGø{P=(ãU¯b8ÖdgÔ—3WaQùWNu’FQ¤¾<‹H.µˆ)³,žà)ß*g_| DÔvtïBI¼^ªítN‡­%[ˆœ/&gÎ@‡rJJE*9îrPgŒF1S•»¿—œ+Ü87ëð>ñð]®¨ôÉ@ÀxnÝkúï5kff︔£ VFËíç?Á ¬ÃùOód›’èÙcƒ*”DçrVØ‚D†Â‚Aw‚שä)$4ðœO®§¶‚àKÀÖ êÖ^#銵Ìl©×•:Lçì%u5~’0NQäÔ;ÊèY^z\d4â8 ,lê EÁºŽéúc¼ÈkÄãV著|¬;ÊàÃÍjôg8¯o³]&16à2pä( þ+¡Á/â=µgs:3Ö(48:WM‹.”¨Ói& à˜ÛéÎ¥8i’3%Kë6Õ íN«ÑÍ@#hDíÌ’ 2AU]Sq¦­¹)™¨…‚Õí5í%º6ÊÜ»*çŽÂø7h?ÿ}Ìâßà§øï£;*× [ v‡fRGQ6 }qâqxF\z¾úâ|÷çVd*ùÏ ¹džyJ{4”Ë“t>\âkUžÒj†¥¸¡@MuÉè®B2äÀþ3‹©äLbþÿ,G¼ã(Ré¡o=| ¿ôp®–zJCkðhÅÄe¦Î&í¯af‹ý®V/!ƒ¦ÆúÚH¸:U“*Ö %2´pý¯’ eÎ+2tÌ÷;²µ*‚T[‡y®‹<±¤Ê òCEç¦éŒ ²ˆÕ답Ú"‘‚ÉB±Äp¢Dô]W1äu­ýQ©ûtÎ…ñÈPOÎâl&툄ãt•¦p¢”£üJC+rifSP zb‘nJ4£¹ Í,%U–!J'0X~IwMÉpç>Âöð蜎ÀÜ£„yl•#„²IDÜkùSVõr¬Ëö±/ÛǹLú«|H¼„iyjz˜BvVPÉoAƒq&i/º&½–UÍ"ƒbáI#Ç»«Ÿ †âçeG°þ¯~ßþ¿þ}çÿê÷cJonҸШ4Ô‡Ëô–YTNàjÝ(Ú¦sŒ‡º:pƒ‹×ÚB©ìA—™påþ§À½x½¼D[ÒˆX{c­ŽíÉCމÙà3WñšúV! žz‘á%€~Ð,5¿ø2È+ŸIŽšÐ(S7êY­Ö¨Uá•*ë]nIóøŠ]ÉT[Ô®ÐV±¢‹c,Ó˜ús†÷UúQƒÄèE½=Ý]í+²¶˜kÄæ6_Ùæ^h°9vAé³{^°´OÖt©–¶?wîWÄr‡jÈ}ÕçÎu1s…Ss…ë–6¯Ë̺¦Ê­>"•¨'< 4І1##ÎÞíf´|¯‡´ZL6-ê1ŸóåìðGSùŠÊÒ%kÄ 1ùI“†¡à„âgrÔ„›Ð‰ eÍy³ì\z€YFv[yw«U;ã+ô͵»A;$xa¦ÔéNdë¡Á|_6 k®hJRtÑ Äo^hY/‡º"¾l M¾¢cnyüÉX»°ÈUÐrDª:e‘M*(xÝ"PÝòoÔ&e?ºå/*4ˆ¡R˜ä7ÆÅoìËöq.ÓGÕ ¥²‘¿› PÉȱ„…Í4ö¥~³`“É0i÷ÖP)Ò¬erÔD½û’&C'áçŒökÀþ^ÌÂù¿ñ ”V­Eʼë¡ótA{ž1ŽCSèƒ ¦™%Ç^vŒª±‘Áã0ô5™™Ò`W!×Id&ô›îÈl2šßEïiESE³ŒÇV æs=홉ìÄ"­e§vBëÇ+iø0Ì'd†}ü­ Rf9*-Ú9róKëxv1Nš)^*ÖaYRË/ˆû\Cï\«ªå¡´GFÃr ³”@—óc¼yÓÚ±%u¼D ÅPX=Dåû ôŽ70§WJw+‘îå/Š0ÉP‘®ÁE‰®|4 ˆPg…¤5-í $:º¦¾èÆ ~µN².^žÀ"Ánþ£;‰yñÞk“å…§¨£xí\õ ~î^$·¿Ä¾E²^‹4Ì/GÃübnG& (ÈdO?›ÙFéQýrE"N—PFÍf#s…ιy±ž°, ëõçº:3£ÙÑE„l “xø¬B=øNch{\vH¼`’JØž$ìoè±ë4‚ê3y—zË«f­õªcد:†óªc¸•Ü2-–|©•_N—¹_ý˸‹4‰$ɤĆUk6êIl8¯°ë&‘N'éTWlHÖ€e¹úÙb)~¾¦Ñ¬ïÙ¼ìï鼜ïÙ¼ÜÊHh宛Ţ>¼¦‘g­DZQ#j5³܈¨lÀÜpq,à9‹ê*c¾‹‘T DhûÖõ“•ÛÌíq… ¯¶Ñ¼0tØtTë+8ŠðL Ù"á09¾ø´ÝÂHQ™h µs¤ ƒýl¦ÁöZ5f][Ãɾ#' Ûæv9¼4NXÕåÔSŸ¯cÒ4Ö°¹çÝŠq¢­å ¾ 7š­…—î üzùM‘˜®ÃATöHØÌŠØB÷ÚôÚAl »pÅǼòؤãTèf^«Óêu³4Q¢<áÔld(ñ ²äUiXÒ“5Œ•¼û+z/åŽ,E²0&“‘6… ‡¦B«ƒ¨Jå`WBäë‘áà®Î÷õtµ¯ÂŽÌ5 rg9‹¨ .vÂ(ïÃ]‘z’¾úD¾“üVlpç̾XËÚ¥ÍX(¼®÷D€L@'rGNvÌ-)½Õƒ! G©È^L²ÐfQ®pž=Ïþ eÑ(šFOä,°ï›%†{I5fVQò àå †bñË“‚‘fb?¡I­|vJTÈ—k"&šÙÅÝ–ì1«Z³š”Ì[=½fš¤\„ƒn—Åı(‹³zrèÊé :ìfì”3Ìã&\*OHÜüL–œ5•ÿßÃà¶FNnÂÈez2´H =a‡ÿª{£ÚÙšô¶;×ä2þªÌ¦#·Ù”éºñù}ûžÞÛº~´®¹«kp<½í.|(š›Ú¶3ÙÒSÝ»+ß¿w(RøÅÎÝ»wnßíiYÉþªÊÛ̹Õ#7¯Kšm G8dá´ö–õ½Ç¶dëG¶¥FöømýÙúm5u÷NÜ<ÙðÎ?…»kûÝ)£/\œ8NÞ˜¿÷‘`ïöÇ—®#ÖÉP[Þë úêFvµŠd£Ö¾3ŽÍ¾rvu¾{4r黫‡ð«Ý鯼8sÏú:Ù.9„~‘9P2Wogé HZ,‡¤Cʵr@ÓtHzÒ…&ØÊ ã”RVÏÍUf*?´¸døú—¿J~Sø¿Ä×§•··S_ÿ —#ð­?×[ ²ÔÁÑbLHäÅýæ¿Á¤4ʤ0ŽE#! ;±¦fÕV'èº8øïÀºü¤¾t’@J³§ÅrÅê ™óêe26²8ù2­€bj‰¼Ü0÷èöþ¡û©]kOMÖÏíÙ;zwvîQî{ßþöáÂÏû&h ªO2›éЇ¸ß8“ºŽ¿“¬ùYóDî†ú&óR œÔs›¤gœ•óZôà/Æ›^‹9ÌYyÉÌ.àÇå120~«8Ær­DŽUk%’[^¸òú!X-uU)Íj5ýÊ«…ê4ÅjúKµ®¨«ïdpS2öyä‹%4®fªµ^âã>cæÙƒXsçLÙŠ¨Te%Ö"×ÄËÏÕ6BCxº§ð1öEX[êFh,7 ¼‹«ÁV±“‹“ú5˜Lˆ¤?šò:Zš€ )HÍ«r=„FÇVôw¬hn„‘êâ ¢Âz{ÑÆÈªå)IÕVF®®0㢰(¿à;nÛs ÚÑœpÍ5lypG`ÀkÂZ©ÊyóMÞ½+¦N¯«ek¡F5néõ4†ó/;BM5¢éi 9ØÏ?rè¤Á—0¿¾Ô:~÷Ö”N»M«eñì-»ý ~szç}“ÌaÜêi £4”F«üN÷b/ÿEá9|B¦ÊGûdzpüTãÕaR\g"kzb~Ð*-ªÌ˜`Û±=ªot¼)@‡ÔÔÒþìÂ^ð²e¢œT&ÛdX §ƒFi¨6³Ç¶ºŸýñ;ÿйw´A«ƒE2xhŒó}Üx³5ç’¦Š{ ”³eIØÊÁP( ·°\Ù#ÄÍWqòø‡®àº=s;¹`PMn–Y07™îþ¨y%kmn#L~÷G ©ÌkäÃd^o™¼ÞF`„‰åæ³Á¨pû0C3©ÅjÒiR U)7i*±—™]”½²Ä"€_–¯EÀûîpÏÎÝï¼ Dòå¾=ýŠK`.ïóz˜u4þzì*¹v| q7ÿ†Ö.@?ƒAã¹¼ 2ÙÒ¯ à3IfÞ*X©ˆ|4¯ xŸÿ%ûK”@.Ö›À¬¤5Úˆ5ìhYZW”ø®¼zèÆ‹äsÀª#ì*½”=ÛJ€`¶ÅâAêÙÒÄ-r£ÓEÿ¯ÊfS¹”~lÖîbÌUÝCk“×Ýîs Níh=ÿ…¹Ù cá¹ÛrG@îH¶=ÁlóÐÖÌêV׫/%uôç†vòé–£ùÎùZõ®ö"¬ÉV_p TE*µhaLqUKú¨ãh^BNç ðÆÜaG4(«Pº’… æARâÀÜûì^Фõs»¯½+Cæ:;´¾¤J‡{ç›å¹JæV‡^”ÏbêkFà]ÊY‰â7ŽQ½?­Ø/Ošœ’0Ò¯ªUNtå5(ßµTC’8åf—l«Vcƒ¦àCK¼@ GƒQøæöV¬}á é%a?5Ù8ZoãFCmd|jh¿kkJ²®vM»÷1wÁ¤Ô;ø-À‰ÜÍœMÇQS›W3vxÀaǨÆo;âz-²a› Ö4(é›\ºXô…ñxñD_߉øÜɾ¾“Ÿ;0¼8†ÿ °¿¾ûË7Ýü廇áïÍ7ÁßKO·l»oãÆû¶µ¶Ê[仿ú/_àŒ\âÓ(ŸËUÑ»¢â¯IzzWÃâ+\4¯[­O‘lhH'ÓQKq‘^ ⨸‰A­:Cnb ²Êî”s0]U;Ù[ºrd+Ï€ýÓéï°w ÿäçŽÞ™é³/ýc?74;rd4ŽëGÆZ »óì†þ‹tš!¿ûý_;FVmò:ç¿óÔ&ÓÖ¶ãB&µãþm™ÿQ’¬uüòï„Û­ ¶ÆÑ¶ÜL#Öiª²Õ§–¡Ð Òᣖœ›ô´r¦AdÊJn ¶¥0]58>4ÞÓ•hh¨ª¼J]ŠVÜjZ¢.E±TfEÁ‘W ×Yª9¿îû:-žù÷sMkö÷uO÷%%›¹%°q÷þ¶¯fÓß¼ÿºß’ËûÔÞ³_O{F¶ß5tý`8p 9'Ù‡RN¾ø•":XaNµD6°°Ä*%1Í‹Jf–·™ÎÙDjÚteE²{.Õå’îŠÜ†ÁîÌÆžwþ‘ÆEB`¯=À›Q­Ì ia5 ˜ºÐà/cùܳÜé-ÊÔ²FS¤Ø!¸ÑÉ7‘H³0„CäJ ÿ,ÞÆ¤¥øðs7ž:bv<]Ç8,›±±ðV¶ÁõÚ4Z=ß–‡LõäÔîr²ô _è›+´Î'‹Y è£§ åONõ¹U±ù€¸_¡Bjiù{5’–åÐÐA/°?¦û´f‘!ÛãŠýGÓ‰åDÖIV-çRŸRê@ˆ¦àP¡ Ä£A[X°)Ù#™¬|ï}±J½„“»¯½3;·ßêf,½ÏÐ-J9¿c¸‡ùëKÅ}Iæ°2?°›ªa~­j…r[™¥óäâ&Þ§ÞïRþ”õ©÷»ÕQ]ˆ\ÓH#ä4Ø q*Ÿ€ªåIÕóÙÅ­QYc¢jF(kG[(ñåm§e]ÛŠZ£ÑH¸Î¦º¶×2'—‚–aÉÓ„âÒ0lÂKœ\WÉ'¸Ò;Rå¼—ò¬£† …µ&‚áÅwpV´ðø#ö*þ¯Vh3½.ø¤÷³?všæ ÷‡‚ù0sÔì€ñÁBçŸg_GIr?jÌ¥)ÝZôb냉®Òý¨e¿¡|ͨ÷£†â¬|C*+$°ãýö*î :¯±ë_ã½ÖÕvÿu½U£·‘÷Úî)üàAî=Ïë4?ÒW…©™çõ}aæf³Ífž0ÔèË3G,öKAæ1WÒåjtÍ Ïì¸T¡êœÏa&Y,òE©æÒE©u¥‹Rmꜵʅâ"KoJ.¼ñ‚ݦùKN«ù¬Önz®ðSÜòi»GóCî+—íÃø¿ñOM†Â‡õ=n3Y ÉvñQ—¥ðŠÉIæ¡/\`y8( ÏT H"פ†È5©äÇl‹„ŸÒø¼w¿PxàI—[øµÆÌ fÍB•ô`áv'¾‡}Ðõ¬£ÞáL8˜]fz/k°ùoá÷ÌdÝô®TÙK-]•*9$;¬Û…ò[RƒÌoºB þ›ùmF=þQ!Å|ˆ½Ï6ÍÞ¿Ò²0ß¼_Þ“ü4båO :”Fksk˜å“XÁ„À[^pù)ø}dóC@ÕkPMì¹ò´©±>H/ºò_Ó•§ùjM'³ÑjÕ\ Í™`¤·ÉYÿà¾l&Ò©²m¼+ul¸mbEu៛‡Âù-ížV6¡—š5½=Þö¥BL¸»]²Ifmms2Ø9ÑRxd"¼Ž±xBöÚ YëY¶†ù)x¬Õåb<±µ¼±˜?‹!I>½ÆQs…¤‰b–±¿4ph,‘;40p˜ü=<0±víÄäø8¼~ÍáÁÁ#cõõcG¯©/XöìØ±‡üGá~†M0ù}ÀÓN´êæh")ëg'yèâHæ'ÞLR)‰ièC ã÷«ï0æÖs4ëæðÐôI ÛÏÊE Â4A–bSq™A™‹»M6.þFŒ³šöüúèÙ'{x•"Ç&Lú}{u¦«pþË`têan·²˜Ü‘ftoÎT4Q[¤ yGsÙ+tMòö˜0IcD‘ÇŠMyDîxQ»T6œÎycQrG´9Ö\ã—}M€"8R¤$Y/ ¤ŒLGñ"áì¼>¶2Ó7á¼iC}Üó»Í„†6œijöük8ím¬—"ìM:)  Te“ÕŒ¯¥Ñ(ºÚqj8QøÎ˜Ç»Ž1Ú}æj/Ìø€É~aäP] s¨ÊÇðœƒñ@IÈWÿ€¡¹FÎ £;úô¶3–QHÞ“ê}¸™ò·Ó9“ǨvG<)Z¡”Z'(•R¶ :ÔâêØí_ÕZ7Ðâ«Y1ÞÒÜåa³qÝ™”ÛÞäkëÍð ÕÑPnG÷ŠíCµ‘êIǼ^ø®¿ê.ëø† ÃT®ÎÀºXþ0Š¡Gs¦ÌÇzÚ zZ]T¨¸-¬L–F„K›Âfªª› e Ø’G¤€öÌ¢veƒ)·_©-K¦§/ÖF‘ÊË@…*¹´ýÆëuZ«;èÎõô·W…]VƒY®›mkšìŽº·tµO…¸·5¢Þ¤ß°zlÊd1éáþÝÙíƒñH)ô¾JÁíáœY¾Ó‰!W²é*_Mó&ר @æäF'n’$.P2¯R0L1~¦òuéJ'ÀoÅ•NxÉ+(1+ N\0HŒ¦fÀ²mlã†!˜ÐqX_?à8Rþ¹Wì€\¬ 7²øvœeÑIÊ9Ù‘Q{-Fp12²DãrçÜ¡ FàNÔ…êÜNå2àh—¸ ¸åäÞTx(³Ík;©ðWÂp»¿¾Æ£7iÃu7¦Û6çcÁÞéÎT§æ £'ήî\ŸñÛFóFø‰Ææp8]jz ¾¹£ÍY[m•m®{~ŸC^´;g0›ÈQ@0-8Ü'oÃyÁÜäøÅSGê}/&²à@åKx¯×°´²ˆpnp:g„ñ"/-*‚YUY ”úÒÊQÆêMÖU{6Yk,,v¸bMwœ†™=!2fS*²a7ÇçLšÚlÍtáz9Çî:æÛLätêGc¸>g±¨I’3=qåL9q/Î'péÙ¬e”½ùr^­Œ–XÔ\©‰ ŸçYØÅþî~!IH.:5TÖmñäÔäFDš™E=KÍéÑõªTål±8£ÐdþßȽOòÅëqG}8’HDBJ}ÚóìMÌF_4ENK°ØŽåÛ¿ ‹ÛHà†¯WwèLJ¢2˜?kÔkÁˆM2!·”}ÆÒñÛRÇcΟñ4×õ&œ§ëêj2.˜Áë…õ«Rƒõ…×pOC³ÏUxxbP0+ µÚÐJ¥.$|!2b†±dëŀ茔Ç1ëñN¹ÿ#™‡¬°+É‘ ÞS KÙº€þãÌÃFgµd©r’P3¿®Ä?ÿ„Åï0~K°±æâc¾0 ì¿Í¯È‡”²Àª,ñät8ü»‰äÑèeÛ<¹LŒ%;!å-ÆÉ)RvÐ|àê*§†•¢QA‚É•ïnfÒ0S¡4Ñw¤XORò[xl°F½çš‚0ecc ØS¶;<Ÿ×‰þ8þí'MêÜ1º ¼pìC[_1ÐT~ÙZô‘­3†_£&ösÅš}Œ"è[SWÛÀ[ ^ðú†eéäC>)‹©t‚)÷âL'Q-*J2ÿÞqÌâqsõ´ºÒsGLNÍIÑbò† »íÓö/»Œ:Ï[Zý×ôâß1¯SÚ¼‘½ ýæÍÿ…‘1Ë”0[:ECPúÛÓÂð|ôÄè‡ìMìà³ÂhgN¯ç²o F¤²jYþ½II±W¿Rê&¡ K­,xÊNÐFˆ”½ÿ0 ‡m![$BÒðÉškŠwÊ«Yø„ÇKôþÃÕ^†÷õ57w™ÝÓMSÓ§'­§cµžøºëõ©ú–†Ý3„ø·4%ÉŸd“ÛQxŒòç1f=¹ÑúœN ‰ƒ ’‡§˜wo’³ë•/}Nò‰¢Œ|XÀ ¥ä{Û2É÷çûLfVX“©ë­wž®­¯>=†ÿÓ`n7”µÊUxJöOp`nC~´Z–®’s”§dCÞcÖÁárrRžOç¬Ä…òºí~‡ÿ .”*CñÍ+fú"‘¾™í[Èß-íÇ;º{;ùÑüö®ÎùH$¿£³k{>RØúÜøºuãÏ‘y>ÍüˆÙzÏ h<’3ó`‹û;œƒÜ§PH¸”2LËv6ͦUžR§J¤–ø8&W»u¬/¶dÊ2PÃî0ü–³6¢¦ /ÌvTXåO¯®bÅšñ¬l˜¯m©ïp޵ºìMUéž4Lý Ws¤!Ô'›åç÷˜îj_É$‡õ†F«èú¢èÆœYN·{”£DµG#¥ [ŠÅԔÂòcD]bŽ“däõ¥–ø )¶éE)¶l™AZÍ0«ÎøG[¨9 ëkêö ¼÷Ù` –è]ÑeöEù}ó×ÿ¼&ªš¤(ƒÙªªŸ»lS‰Î¨$Ë3üyX£mþ`zÄ.UùÛSÊ¥5Ñ\Xå3§0¼æC¹‚› øçJZ+^Èj¢,°Äm+íFFp÷%Ã-kÑT™âÂÀ&kWSƼÝÂ'b…É´…=€ÿðBØ„º6p}<4Gö*>‘e4SÙ»´;Tj2}1Ž&ÃAâèE?g‰ä˜Œ’#;<¢‘´voÈŽG2½™ˆ·muKj›ßÏ› »'â«F2¹LÄÝ<ÜÔ¶¡ŠgL:½Îïr¸­¶@ª6ÒC* 4ktF³!ìrzl¶`&Y³¢Þ®‚9ÀšŸá ¨}Z–M&²!èREÓ4|Å‚ü`Z6ý`Š1û‹9d“YÒ×HËG‘Üb’p4SÙ„$©oxK/©‘› ÚL:*V\zDÓ p¨WËþ“²§JË¡æ‰ed¿»ú‡[u¦µ^Þ•;{ÖiwØ´:1êd¬êÇ6£±ÕtÖñÌtá¿]³¾Ëå”í©‚å÷„Ìé`Ø *¤‚4ñ„RÔƒU¯S¥(·“§ i×”?¾æ¼T÷ÿÓöðq×þø[¶·»]Û›¶ª¬´Ú]I«²êÍ’,Éê¶,¹É²åÞ¶1®€1¦ŒM3E4+PÒ^/¤˜B ü“ä½¼[W¿™¹»jØIÞû|þ¢-³sgÎÌœ6ç|OÐ5î™'SAk¯!‚”+f˜ûR¥ÀJN&•>ĶA^«MЦ&ÔDî ’”&ƒ†'CX3é—– ‘ PÂñPïó¼ó¸©¯ø%woøÞ°BCõ‹ä #/¢Õò¥èTÈ%Üe#rÃ-‘cÚl‡ry;³’ðñq—–wóOVЈƒ®æ…0ZSzI«[y¨÷¢ß,N»•í¨À,¯fäã×eüòQȧ ÕqìQÞ¾B¡¥*Z¬l™òl¢f„¡Å‚Z‘eèÁ!•°G, %½Bí 3¬PL‚Z_ΤÙÂé|ZOÇY8™d˜­f;÷qj]+ ÍJ!ÍLDYR¬‘CËœ½|ÇAK©A˜ï$ÑŠ/ºg! ²ô‚ßåC>fÏ!¥'ñÀÝ%vúþé¡m 9³R k‘h6>ôøv…Š¡%= +_GoùD$zD­Þ¯þ3÷…Bü¤Zq³XÇù}5—Ž(¼˜r)O-&‘ÔAUóúS+ÖŸ:Ð/ëzŸM;œãÔ Òâu‰4ì¢Eß_£d©åŒ„Ê—Ñéù‰Fù:ø‘B|YaQXì_ॅ½uÃ5ÿŽCA$øçëÒg,bøbähWiª…ïŒO{_`u¬;¢…T„÷CkÜQ'øPýÇϸ]béÇ{˜ˆUϽ5X 2)@x=ó½””0Û1‘µp” †Py(¤žq1S"¥ˆ‚œéÒCÛR%† )˜Ÿ’%‚+°CÊ{º lhv NbJ»‹ýåu:“‚»$¸"Ñs_0rÚmôRð <cR7óÁońȒQùÔ3È™Á/>T˜µ#·ÈfÛ?”²õø'?„cù6‹”Ð'5Ó‡þ'%$¨17PFEÈosßSšµ×½'Vi *ÁîOr€¬ݘÁS³’|®…žo”É>µ%о$V#Ú¦ø†\…¶<ßlÀÏw G>#O¾šÂ‡§U<<|ó] ßјïb~J‘‚wá³à¼…øaÓÝÎìÔëã7¹&B‘ûdv@ [Ë:¥»~w?óS‘öH¬:¬¡´‡©ñ+àøqPÃÔ–†=¯ Å#$x:«àtÐøýèQĹõ~ Ý«vf(v‰Dì.œÄ»gáY? –h®ˆù±ß–» í1Ò#ðµPªO6˜êQo'‹A]RîÛàØe›e¤€Þ$sJ÷ÿþ!Á»Õm,#dneÜ—¸ßé0Êœ äü¾Ⱥ€cp_Ê$„Èù]…d5ïlótR¡ÚnÙŒ¥1æ„N*vu'õ PŠû{¾üÓM­|ºq*®V[ÅåFÁ´Øè´UN´¢ºØ:`&DóL×õjV)— ˜30ãàÚX<’Ò°‘¢ãN…U‘…AŸ¬+#;és& rõÁÑSNÒëJä0„?”3/nƒF{Ž7§9n³8Íü9x¹DiO@+9/™óÏÆ‹ KJ™DÀv`Ç)#£Ó#Qº©¡4ë]ÙjCve(«&Q`Ö;õ²5Æìª |Ô²f7œ—p;2¬¾lNx^Îç¡×’/2M„[ò+ôa´sçxŒvBÏü'áA†j4d—")Ú[Šg—H-â—/øýþLÚ†ëmcÞ%?Œã~ákÜï³Â8î·V',HØ¥‹tP‡¥"ctªÑÏ e šU‘l‚ºy#„n@<Â\|alk@.ظ9u‡G€š©,[ɯÃnÈO`¹:½Þÿ ÔÑ8–3—È ‚Kð,ø“™×x ¤‘îGx4^-‹`=þ€ÃGO©îê®9rš¹´ç@]ssÝÎC¼­s˜®"¥Ì|'WÊ«“jbƽ‘²ÔŒSÎ ÈTñ“‘—ƒpE=^¿ å©¢âüæ‹ódžæa{´Öç«‹Ùí±:Ÿ¯6j_RŒFƒÙôßýUa³9\å÷UæšL¹•¾`,ÌŠ¢Ø»˜—I‘PÇæLÚ®5‚ãùwÙò+=ÞªˆÍ©òz*ómÝQþùQÁ oyNFFN¹× uóŒ¬2oêùp>L—’_|÷'|M¸ˆ¯Û¿„JÖć`?ÑŸJÇèUÈ×¼êj„sÏüþZ4LÊNgÐÈ„sÉÚC¼ùs?ùKá'ü})I†ÿ®GÀáGýx7éŸÐ0iQzÁ.s{’a‹%œô¸Ë–öüP   Ê¿æ>VAù’Æå”{JCFc¨ÔãIÆ`ƒÚ ÀÑ7eÂoi¤h?½”:Œe¸¡Kð,…GaJ"!‰F¢Vʱ@ÌèΩWd÷7eVmÃA™ÓÎÿQaÒ.xRiCR^%…Rþ ùÇ:=z– Êžw¡ìC-<”ôã§ nÁ?.Í/ð3ÕV!»Ú3©Gnx]fÖ.8®ð9À¯>1{?…¡àÝ?*åïiõ2Ãÿƒ]Ò'ÀR朙;±x?Qí÷@ö1ó,‹f}U<êõÌÀ€àmØ'âKS¾M^»¼zÇØ¡!äÐh!|€F°(ý.Œ {–{”DÕZÍÄ~>ÜGªB‘‘³'ÌÓïΟ•` ÄSŠÃåT:·JÍ«#³¾R}H§Õ½ô½˜õtrd#BᙑaF²QD83èûS©@Ë,WËÔ¹ohNþâiî"øœÁy ðc“áqP©TDÒœTb£ž ’ÿ$ì$'©YèD,•ÖQ¦QˆdH® 4hzVluŸŽJo¤# ÁBgßtR*øÙVÉQ˦C~Çm“ç ÿŽñþ ,àƒ3oW 5oMJ Ž ™¡è0³<½„‘gh¢^õg¤!_/f,³¾O€“ÌZÈ=²šÆühÝÓ¡*XßSá-ÖLL»öŸÃ¸5°ìž…káÏUè‹­p©Œ‹`x]œ Ûøä>ä…ņ,Ù£œ|œ|œ¾ýò(lÎ×°'º¾‘ÀsiKš•XýD¢ã ¶§úÐjaq-z+pÆ(3$2ðèKàaýå\$_àÞÒkÅziAFÌB¸¼?Z½޶ªÍ<¬ŸüòÄ<$׉’hº z šÆ7RÚé\~²é¬ 9| °Ïœs°„38ðJÿ ¼Ã(¶ûÌoŽ~ÿžžÞ?|ä7gº_J¬¹gp𞵉ÄZôwM‚üðdåíŸ=»t鳟Ý~òóç–.}îó“Û_=XWwðÕíðomíÁWñÚ!]ŽCºàØ2QJχD|¼:›åÀ±e<Ö"VöA„>~ŸÁ&âÆe‰H'ã¾&´Ž‚OÎÕÔi{ƒßßh¿2š0ñ)Ò™z¾¦àÓ¤DOˆæD¤‘² ¹ÐU”A2W^×(À~n³XNrl°ÚiÏ4|!g%4ù Dm°h¸;Y“F!`˜ãÕÚ€ p¤JjQ´µÙNî%­I£D4®™¼‹ùs#Üç"—ÈMfÑè²]( p„b>èº$™6äš$«³BFh›YÖ¨’CÀFáìsGs(>š&à–3° ŠAWlE øÝÙ®®³¿;ñÄ­¿?ÛÕ}ö÷'ž˜8U¾ùüððùÍå´¿|Óù+Îo*§?¾½}ÇÛ,cc`ÑoïØþöƒàñ qÿƒ;ëµÔï8ÛßvG=¸rÔí|Ñ|)A0÷Ašg"ûW¨ ôÓ¤ˆÂ‡ÃÖ\xÕbÖkå2´AˆL)V†4èv6Çe b ŒÝ…kLÃ÷ïYÙ‹O­üÚÊS‹³]Ö·ýüø×n~ç¶FÆ•iâVuŒÑTë•gèØà‘p‡)“ myùH ý’LË‘—·€·ù}UÇ(eÂD)ÊûÓ"E·Š>#u¤Î5Õ†,8¸/Ü¡lw0OvæÌtT8N´JñÕ¦6V‡„ÈÅq”´jKg^õÆ;,¸sSU^׿*®Ìâú“»É3ÿð’x°eÍÁÓ»ß=;_rx¾§Éý'§•+»¤è9tap㛊Š6=±qð¡Å%§i@ÄXFïxq¤ÿì‘Ñ&ïƒyñŽQ #09!_lžü„ÞMý–åıñ¼ x.1v3.‚©ÎǪ0ž$±ÕUƱؼÁz­:ãXà¨DÔ‚¸Jð‘D<*Ï*÷eZÍ‚ ‚ ÷{ƒ‰Lu!ópfóÖ¶¿»Êó¬9'–.¼¡+…ßrw†úŒÙB˜ˆB⺤‚…g U°¥°/QK,JJó¡4,t£‹²J^³(Z­ü†Çèœí‚. ÓßS(.d:d c^Kª*r³,&·‹a§ófÂS¤"g¨©CLáÀ*­ÏPK)ƒAVÖÚçÏ*Éd£Ý붬ëŽ&·?6²ö‰­eÏ<“ÝZâö¶_×ëììíõ–ä4/­œŸëª]UW>Ü ?4*å¬\atª]‰¢Ò†¾Í·-\v~[Eí®‡‡Î~TjŠ%›ÃáenF"]YÒ²} Á¡-ïßÝV¹­7jÝ„yú HŸÝÔDåQ'¡Ü‰@yïå«¿ ã•@1ª©4Nåà»íÎʓĹ‚ê²ŸÛëô»]"ýJ f•º#N+¡æ’Fo J$¬\žYÒU–è)¶º+_¸pèwraQÍÂ2¿ZÊ òœ]KF ºîÜP¹ª?·" É_zÇòÍSA•Xo6è}+òÒléª÷–ç˜l›X”ôyXÿ¼ÑêÛlêüâ2[dqC—ý-ã¾Iþœ&âD")Í—E¦ ’(çJ×TÐS7 /ÄcyÙ¤2ôÈ`°PCçR ô•Ä]|)5)‹p¦P"@2º€Ó×ÓÙšðØJº‹Ú‡‹´÷Ö.ó5”@æ¬ÝõØÒÂÉeZ +"[«XïÈqG·ì=\W±°"GcN$k3+¶—ÄìmAM¦×«5V,ÚÓ¹þG¯o¥H$eµ’.^Ná9ü\SÑtQ/¡{¼@Cž1ÀC«M§Å`ð©ßf~Ó;îq»5n¬žÏ@È-ð¢#K‡@vKýllÑ@펂±DÂŒ4­7$\6oK«åÅ5;kË&rÈ×›j  il©ïBÉðDÏõ_f® ÷Ùzã§÷)–y”Ù õä*¢(C?4 Ý‹SPù¨|ƒ°#•ÕšñÕ•IÚ¯¸}>Ò9µ¿3Ü#STEài¯doúˆD2µPeBô¨ÅÖOSñ o\t´/Û—e5ÚùÍ%Ëëý‚=ÿy¢Å[?RuïσLT¼zѼeqe5u£Þíiœç¾øÚ’²u÷ \LÕ¼‰¡º‹e+›‚¥Gß½·½x×7ôUzð³Gz›¨£§k0îçäßI1u î ÞöTò¡/<#Ìt«ð•«Õ'Àã;9V”l6ú ¬…]ÅVêÒDqCS\mÕH|õKKRùqŸQÏC:fó.z’¿ÉágiGN†N¦A*Ô4ã \€ ¬Œ²º.æ¸}P¦@!IåPS…4@óJ%R©ÒÉ9؈ÛÕ%ÖÛC¶r±1àȬ+/`A³:šÍ xjË£Úã"ÉH„o2ôwP×™Õo”T(Œj‰Ê› _Ñæ×e”ºBðM­“…¶ÔÄsŠÔÙaî"ÝiåE~ä”Ðý•rVìçI½K᤹pdŽ É•šíŒ‰#ˆE!ŸÎéV8^bÆüSmx N/áõzѱ…zÝŒ›u~qx:`/ 4Ét,ÎïñŃ vÆêJŠÊâ¾úDH»¹§j©›aº½~¸tϬÙ7ØIž(ÄGØnˆt.ú½­.îï>7ðkÅpä[àœu(‡JÎc “ä,hh¡uj®°g$k!y¡¶Ä|ãî$ÊÖº$c…Ùuñ ËØ4Ô%î/œòÇ|Ò–£Ô>oìß´y:7rß„Vô0´K$•%PH ø½J—“$S|\ƒƒüÈîtì1ùyœ’Pƒ¢Œ$±sídú'®©®(uzó‘n‚( ƒEXÊx¹*Ÿç£bãòò#nÿ§,SW嚖Љ£=×7ŽÖº|-›ç%;ò4Y™GÕ™2Gqμ›V'±žòÎ.Gá¼,…U¹Ïë$ïbl¯za´èFWYwA¬¹4bÒ‡ãåžÚÝ%1G[@çqXåú’…ûúr†zîm9íÕ…:àvTò4™i¢ƒ4)%š’RD0“ê=Ò&‡20” üUj\,M@RÐÿR$ùe–i¸~MCæ7÷Þ0o}ƒ;0[KeWž6Û{Tã•9KÂ-'ÖTšJk/)ÝüÈȳŒÝ‘é(nË[¹ù_Í=¼¬¯Ñà96¼àà¢<8#ÿä?èêûD”XÄ{­¸:=œÎJ\Î+ÍŠétf¨;~´’ÀéN+®ÖЉŒÚL¯ÁÀ‘:DlÑ©£Ä{" †çàcuܘ8Ý`µÜ H¡Ò`ó½‘M„ ÅÝG$´D&²ÄuКPCíZ©$ôXKV{Ý¿‡™{ù€ì%Í2&^Öí ²÷¾-Õ…+ïh?Z06–³#yÝÑÌÊ…ñHo¥— /}`ciåÞ·#x̲B®…¬¯)ç¡1}Mkk±|¬‚6µz“0~|;Æ)Ž¥aÉ– |™v+l¤÷ûp¢ Ì ‡Œ8}±¶œB±Bæ•M%ëÏ,[òÀ¦òÊ}/mµ›/³žXfVçöfÏþ=ûvSµ±ç÷Œ¾p ¡jïøæMãûªÛY÷`ùÆE•ÊðÂ#ÝO=ûuŒ…øM*õ£0ÂA£»»J³ ê~8¼!-Ö  :'Ëí¢æ*öWßõX‰” ¥3\©ZQãÎîÞÛ–hÉQ=GY·ÔVè¾{sUñúó«—Ý• \R©ÑjÌëÞZ=°}~Ü¢Çø]noçw¹¹fýc{/ìk¬*vÕ¯'ÿA…àþ6#ÞJ§]å÷—SÕºL74B´! î n´úŒÒJ9T$Õ¾d6wÊœ¯º‰5\W<uJE ¾˜—å7p™¸7²‰Nd£B9_¢¥¨Ã—ä ±ò&[Ê“f˜ñj„-Yþ: ö›Md»Ys6Žºä7¼ÏKY´²@”MÁ¶£Ý@•Í»~0.W±4åT_'ÓÈE]'¿¹zŒ»Â}¹C Ïq¸w_3|9C˜»ðÖ‘êÕýÙ >¾'¾»»LÄ}”©þþS’‘댮¼aÓ‚ˆ˜þ?¿ŽÉ¿2åÇT"+ÊÏ׳HÕã±I!NLA"É z=V³B&• R¤ !'(.;åKs ©ÍtÜv,Ž/”xß øq Ô¯ZIÀQ_â7yür)­4ˆ÷Ä뢃7´,?”é=4ÔrÃ`¬>¶GlPR…Ïcò—ÔïöÕ/+÷×âÁêLGÛM£Å;ÖØtVxÃç7½x}í¢ÆÆEµ×¿¸éàçÄ«Î6°~GñèMm$þèàOÌYB@ðÑ9²tÔ"WXÓŠ}8L®ê£¯8QœŠÙR@™\cÒIG`g-. ÷?<¹D÷R›˜G`[#²§ø-bDÌê@èDÚ*Á¬géPÆ-|DçŒ&ˆYÀ1|v+vE£ç §ž›™ÎعFý”oU¬1i¥+A}Ü«qe(•.ÆiRrJ“[£v¢OœjÛ¤¤{ùïñFNÆeR*M¨q†bÆ{„ÏÍ®ÌB…lN\³<$g¡"”ØC }9 ^Fz}Hëæê‘˜ŠeøÕ"½ÁĹ™ûtƒ[j–º«ëËt–ÉIâÀd ìICr«¾ÿM—¿†¢å(| ?e 3£"M<ûqHäŸ3.øX+Ò¹€â>ƒòµR×kiÇ ÔY–µ²ëwªÓÖÉN©\NÖ‰J‰¤Pè©§'NývãC«ò‹×=8Ì SNø¶f°Äb)¬¡OrýÑë[ún.d\%\¾«¬+ë*¶ã³ƒj¬ý[wÁEÜ8ùâô]0 öÃß–  !Q–ª¼L ÂPs»A»—#˜óyï ©;ÄT±êU6ñå8¾™ðCzÙ¹oÒ| °eɪìô¡ŒZY•Å,ðwük^lAe„†Ñn´ÛZh@•˜X\c Ú®PñFÈ4èO¤ŒVÇ 2m'KW~ÃaZseÝ´vKF^nÀ™!xB™‚‡Ìb\—G¹•F+(ç>ƒÿÿ¦¼dÓW@ǯ¿Ï=ýÅ£QZ€±+à9XÇ™@£Œ›H.ŠD@ÀA?MJðõ›XHâ R€ˆäe~_¦Ã¦×ÊÑœ€H€„_°”`#xS‚cF±*U,ùÐxiS† 6øÀ¸%Wõ¥>â\yaDe5¥_ªr-ǹKÆ|w°Ü ò·û 5—õ¾Ývù t—5E¾ bIÜùî'ŒËjàŠ&²lâ[¥I þÃhø›7_¡ˆçN|×m—¼Î‰GÉW&—ku“‰Ü¸B‘ï%åhî“£Ž1¹D QŸ¬‰òŠV¡\e¨U14#@^ñTM5²e”éÇ0Ø÷«ªŽ5yÛïôº=Y"ì Šé¢h1þÂÅ™Q~ÁíJã]P#}í9MP#ÝðÐȾ·br‹&¿q¤éºcîÊÄypoAg©ËQÚSX±°"¨2)³kÆ7 ܱªˆúͲ;‚ºÒæ¾üî;Ö'Íï2ò‡¶G:K\§,ÍyÙ Q«=Ón鬬Íïݪ]Cß×ÚLX“&£‡¨ Q)ØèÜYQh¡ ÈH_øã-(„ëE]”MGÆiÒÍN¼)˜Œ3B•‡¥?Öy¾ü5õ€·+7Pg¾²Cça\W†üÝá`½=û |v|¶ Õ“Ϫ;Äà€”ÙÉ¢ŠCäný§‡" d³§ÀìJŸénî }Ü‘›t€¶ófËýV'?Êð‚7uÖ‰œA™¬0oâ?¬&ð¡Û2ñhš8'?¥{á=’þ”Â…s‡hYÁg¦§Ì3¿Á8}©J1ºt2‡žÐ»¼,‹Õ Ȁ֛Ĥ hèÞqî˜4PTßµ¬pY©=™ˆ†"Æõå¯éÛ'¸›¸<2àöl´t Ø„¼¾Ç'ø/8>'‘C ó#tèVŸ&„Swù²Te! ¤xVðšM÷Â/§‘ ”.—+Ç•íBc÷;?“UÏ„3cêúÐM÷þ}Í}ËÃñÕ÷,å:¿:5òŸ&û M¦Â¾$ùßi>~Õ¹ÎaípÞ7Ažô>^7qtÊ£AˆE$)$È~¿ B!Xž4¯ÃeeH¨?0¸%sÕ–I7Aˆʈ¨a¹ÃV½_mÕÛ›T ·ÁåBH¯2ÃlÊ -ˆ8ÿL‚¬ÿÙè=Ërã#§—p›'ŽÁ ¸uüíòžxFF¬§|š䂉' <'æL|:nHOI€ˆ¡*ü}ÀT% -‰–â;»N›é±S†Ö¦³ù=¢«„æ!^ì´QøZÔ™C1ôÜØ¡‰‡únM ¯|CX1zª¯çÖÕU*)©\}+y뜢‰Õe[}lkÍ$Ö?¸bŃëðÝÏöò;ÞfùøÄ}ãÔOfÄÛPðFák®ý%˜ÂOBLa¶Lòª ‰Ï_ûcÿ)ÐO§+¡+5¸QUð¹•Eœþ‚Á&&—VbT~*´îäÔÆx²!¸¼ô˜$#ä¼:oÐLÎoÜ:þ­oì|r×'æ·ÝúÆŽ''>Šîè8<¥ŽÁWííðýmªóÄ+«G_=ÑI9CuÝòêèêWné¢î™X0ÿ¦‘2@‰È²‘›Z[ákòʾæyïP?ú®¹i“$&,:|L{õ òÛ<8RÙ Æ\†Úd„?xièÆq²jü¢Ó´i;hOñ~ÆIøŒ%ø®¤iøXI%ÚñÆLY‹¨W|½i% ÷O/áF¦QntœÇúS9ÿØ/Ü$Ôa¿¸Ž K’Ȇ<©°ß•H‡ÁoÐiR;_P²  «êãƒÏÝ·Qd¼dïw“Q³ öOr}ë²5~¨pÀç…|p>GŠ"äƒâÅ$ÑN¤O˦ÆI\0ò¥c¼7~3ž& 4¦ÇA¿ãX¿H2ŒÒÆ!Ñq·ÂväI‘óo0ò/+@dWjLvŒ=™~FšøÇÁ›ãàGãOCJM?lÆ“—áYzäjv¯m|üåµìêšvCŠþ¸ù‘ÑHbÃùn{ ¾­]Vnµ–/…*ÆÄ¦ÄÍñ‰Ïb±Ðö9 ÏØ>8dûœEU%P…, –v#ww2IOŒ @u¸ðq R]*(MeQ™5,P¥ß%¶`ŸômpOüï53о#ÒH ˜ê„7”J‚Pš•&µ íj¿SÈââŠSóÔLOÔϘåÅ«NqŽeˆÒÉOh;”!a¢-Ùbƒ´ÎAW•C#œ­•8“D¥ÒÙz©mU:¬A'„j>*2fÎÐk¨§XH„A˜³3ð~†¸3êä¯fâ¶Mi½gÄ ‘-Q=?¿åðÊ áÄËd…¸bš֯eUA•’”ÔgïþwNwοý'ûOþ¤¾^ÔB¡=Ûª(Ûúøšc+ήKxB¹hAsß’g??uêóg–¬h‘ñ{¸Î/ç—‹æ§(ª^ˆægGS šŸqB”`êÄô¦w— øÔE4 ‚~ÛåDÐt¬Rȹ ß¡ÏE’N»¾…N]ª¼Z|,3¹ç÷~˜&õã£ßÿªý±pòêKf#i”èOJ æ÷šI>ÍŒ“LÒ7y±{:\]®ãoátyö?³Š<Ó“òD3£V@€´)áì…Ãvâ™=T!g\J"xÑÁ#}Ù´±náš’ÁÓ«‹*w^YòØîºøÎïÜzÄ”ÑÊÝÓ-®šáêÊÕ~wíJúQí–s+² ,5{ÆF×<³§ºòÎÏŸ¿P¯m–ÙûmAòò#ãçÛ¯ïË µïlé>6!¦ã‡‰ QF4_ ;Èéd~=‘¾_š¡iNji §Cý³ ¾È "Q; d"Œ*Ä6_;:̉ ¼Ëfâ¶bNEÑÀöë¶Õì¾0¼î±‰ÇïÎí(Ïôµîèõù F‹¢ƒõ!gÍêÆÒáÆ 3¼ÁlVjwQ4^ѾúhÏš ÛÊjv=²ôþŸ&Œ‘dk$»«*p¥¯i}W…UWÚ³¥©j{,бëߨVü^ê¡Åwa2*Å+OÐÀÃ<¥Êâ«ÞœÕ:]N–Çw¤Ã¨×ªBUHãL_±¥œðλÓ7}˜o«û,ð©CMEë÷$–™Çxô§á–˜M—?¿øí—C VmNSüê’§¼3ÏQ“,Ph6¢ê«àeîeCV™×U4¬CÙoæ|*&d\£'ጠ‹üŒS%®1[„_0,‹¢Ptˆ{“Or;ǨK¯<‡Ê”Ãßï‚?y…þ6´jy¤8 f9G•SÎQ=úfÚ;šþ¢G !äøv›}QònÍ8G›;†öW¸×¬ÜŽúŠTùoûHÿÔG Ï—“¥ž¾`5ŒNP†Œõ>GIQ^À&¾UìXÀËÔ¥+ÏqC& ˜Ï}¨±ƒSͶoÉÅ£§~ÿÔù¤‡ir’Y ÇG7®¹È*‘mJ¡fE0ýbþbX$ qiÆh$ÓÉzü'‹ܦ¼¡¼‰Jñ~,Œ9@yg»B5¼+£p€o°‘̺ Õõ™O˃Æc¿1ä:C Ã;róÙ{­í:¼¥Ú{ÙüÜY®\ýoŽƒrrà æú*@LgântçÈäÑ·Úfy\Ïôr?¶ÚÀ©`\–ã[Ì:~¿dC^'§> ʉÖäËK ·ši‹7ŽÑ”H#d{péuR‡vÕE‰Ô"Ò_>8µ€º41ªÉÒk ²¨oSâÆ§U–+9äíê^c—Ã6¢¬p|×Ãñ…ã³"Ÿ©X\CýAxx<Ê5ŸÄÂM‡±Q¯±Í"¸z;yA‘qì÷†°;;‘ññŽpDýh }ñØX»7ª¥Îj-ü®)r«í6 w õ×çtðûD6ù7ê4ƸžÇkœÓ¾Rå5}¥ÊÓWJ¦œ7ùe”:Nã8I0ÑÐÖš¶–„ìbxRs¨†—¸¿¾²µÒ`^k¨Üú2P½yˆ|òoÌ%8.ı×ñ#s"ß´4ÅPf;IC×ns/©Î…jF串3ÝHQC¾R'Ïtf:KgÍbæ-×+/hsgwímçNp’f•Î[Зݖg//‰ç8Åd÷«9µÙmV]„ºÄ]Ÿ×¶ª¨rýüì¯Î–#¹'àNöºJ³ù¼J(¯èw©·‰LTÁÞÄRÿ<]¥iö1ÄÙƒëe#OÕ ?öî#Ç:Ž…d1ÿ!Ëtb‘N ·úæû ÷”ÈÄjÙ wN ¼ÿ¸Ú-¹ày]ؘÖ‚/ ª+!ò ©Ú:ñŒR­V’ lµ–‰]ÔÛ*'Àã¼ÊÕ—àIÑ>NaX÷ _qƒtx¤”²)_šw±R/q‘±1ð&Úàu®i8˜º“d>Å{ÒM,çýã6TÄ+å'#®¬œëþ•`ço+–­z¯Òª7©Ÿ«{"íéžìUœÀš™Kýæòc™¹ý»¸½\ûÇc?Ž4ågèÃÍ…3Wwz"³V•"Q¾ ®ã~͘pÃÿ"&|.JyÜiéxpS:<]àýÀmÒ‰oHo?xज¬”ßv•Âî§ÎŸ>}žüçN=ˆb¤àº"þ$D~Rì÷¥Áì5¡ð+JãjÇðinžÒ9´°÷câñyü ª)§ò¢¡´OÔ‰–ü+÷þ÷^iiF¿‡Š)õ+êW„…H¾ ÄîZ>ñŠ%¦‹/ó&…9»=Uu™ÿ¼÷Mfö”?W“r+S|$:ïÐÕŸÐY˜ÿϤiäçÕÙÑRW†S#´b*î·÷ãðÜ[êëܵ™àïÑ*¯B ÓKÜÁ+ÿ•Šq£_Â1Nn"›Ø–”¡5e@eùÿ!Þ)àwØ­–龜œ!íõ…³&/­~oMÍÞçןßðü¾êê}Ïoà¾÷ÐÝågPú`ÛÖfp÷CÌ0·CÐtàùµë^¸¾IÀ½ ª˜¦/¬[÷<|_¹þáoIhòC&>x¨½ýÐ`œ™p %ßÂñ^!øÙ÷–îðäôXÀÝÂýüì¼ú°Q—Ó›%&›¸£³Ä"½¶BøÁU^1˜´Û“ƒ©¿pKö/;»¡4­l”m:7̵ÃsóXÞÀ‘îî#ùùüߥ BLùœœE‰óîßä£u&O†VòŸ­ÉeØ ”þDÃòr«¯ëèhš˜X?xãýц, ¸GÄ•Üû ½R6ŵâüšöPdYk÷~khwäal·É¡ù-sJüX2b‘h¼i5¤ ;(-)M0šéçÓ¹0À«Éu§˜1‰Ft¼·•'5)ôá¤æT ¢ÛµËnî±ò{–wè Üv¼ ­Ðº}¸ù`ÑØ}á·¿=Ìý¢¢­hí™á_pë_,ì~òâwFOÞÒP>qíA¾HÇ¿ÓbݹØvºG@‰e8É=MZ˜×S˜«)ä 6…¹ "¬œåœŽ¯Úíƒ/j³Å|ƒ/f³E}²Ôõ þ¨½ƒßbÃ}Dʘ_@ÝIð+DçD«6 c6'‰rG–©TÊ–úå( `y}‹R¥Z6B.þ·kÌê‡×ýò—lÙòþ{ï®Ë•8­cÜ.g†û¼Ã¼ uXÁs1껀ô!vXžŽÆôå±xL ÊF–‰(L4ý‘L@‰Ð#~Ž)õj^ÿî{ïoÙòÁ/¹.LËY½r yÄqÝ'îchk¼) B»»Ÿ ) ²b-IÕ`Ÿ•E *^âÔñw¦(zõ°Â¤è!½‰ûXð›j­ùK¡Y[ û]Ë} ~Šû½*êm²µk`‡¼«•aÃÊ] Vpw›õgÏêÍÌëÿpÂ>™˜µ<~ÁǤ ö™Dñˆ>ÿ e’|E%¨°B½ƒaèÖ”öA3u…±H8bÕ¨˜4…¢|ŸpgÁí𗎘®8eŸËå¾Y•LV)5JÛ¶ù©¦©S¡Rè½Ø«‡/;›4RKÁümm~Øœ.ki.Í[<0˜_µ¾-»EËuÄ– . {Ê#n±Ø)ó„—ÅÚ‡XmKvÛúªüÁÅpô›áÜŠñÜ †iEþ°Ê)œh÷l÷ÍFV-/+-ÉËÍ DH«ÒN…ÿ JNÏ3eºâËSEŽoˆ2Äu.øšR­ôÏß>cj¾¨áMOmû|?lT™LV‚Óá™3k‡3{Êø™•{‹àÌ:fÎ,¯’öÐ w2Oa¢1©Éø=»E˪äb­8dÄ4•à×6;Á¯yV‚Ÿ¾ a£§Gñeª :'H6A]Á€æˆΡ˜BñxB°”óæƒk ~Â%ªó¡ó¬“u¸=ìÒ©º¡Öy>x޵³ðßóÁ‡Y³TA%_V;• x°qP"%)Ý’Æ!­Ýxåe­S•¡EŸÊIR*j’èN÷+pŽÜÃÔæ´jàþÄü  £.#U‰Ív¤¯>±“3ƒ®Öz4™™è2VS‹@=˜âkHñ¬ÀÉjxìxìü¡Ä±jƒ6ÇdÊѪ•„|Ü9ƒ½Õ¸sÔV[.È‚w¤2™” M¹¶·a‚EoÉ¿€[ðyO1¨KVOÑg'Ú뀬€®¡)¦= â™C2Õya8d7‹Æ-1^sÜ8­’==~ü§}qÕy€…¾P ü45êc‰,¼ú¬&^KÀB.ˆ>?fÀÉ$²g¶N¦N2[ äé&*’e50ÍÅFš‚š\†Êé⡳A³†gŒìžªÁª«*²‚¾L§ÝíD§J0¢L0 ùw:YK0]é‹s­6¤ð€*•e(½…ͱü–˜%¯mxõp[^Kg~YdɉE«Ÿ, (j¢«bå†HÇêu«:"ÝùåáE‡»—=X<ñ”³°©³7T®ÙWZ]DG¯ÏÐC1dóÄsÙ5=‰ÖÝ÷âÊêMáªh¥Éì0÷Ö‡ÊóÁüêÞ²yÛÝžåõEÃ-9%±‰‘ÇíÖf4dÔ†­Ö:´Ü“eô­Ð×¼%nJ9ÇV"ŸÄ9UbCŸ8f} ?†ß¶¢o x> &UeKO]ÍêÜ,ÿOÚJßúÚ=¼{áÉ•……+O.d†O}9ÊÜþå(U–Xq´¥ãèPŒÇe!ÏPcp\,‘û‚ðß­]J—™@`¦rÔ#ÔØ>Ö(ü¥I!RË. ÚC`ÛòŒZ.„s Ù\§BC¾Ž0ÎÀ-Ô+äï ÌE(U€«š¤W§ª¤.VSÅ R~‘¿ˆÂ…òpÜSµ¤<²¤¿Ý=ó ÙÖV]±¦%äÈtÀ£-!»×ž²a¶ƒH QzQ æÖti›[Ó¥yfMœ-!$”Íâ¸dÈYÃR&»ÉJ±Ì0÷‡Sv·Ëq ¡îý,µ|> éÞð5Ô½YÐQYÃŽæ'Å:¹„¤§‡ Ä@ÏmS¨èfdÙñØåͳ±Ë{“¬ ®šL/Ó«U@ ¤™3Ÿ#ŒòÙ×1Ûˆb¢–Ø‘TF¨”—y†!Ó¥íÜXÞ Ò%Øxˆ'ydPLê5ò9ç¶"Ò0ð“ŽFwô% @$jKjƒ~·ÓjVʉbP,âo½ B×—F…çJ(DQ2£{vmÝÔ!G¥Ìx`’<Ѻ N­Q™"[~MNþüB{U8íËQ™«Am>nuën]ôém­§·7ä¶o¬*-Š.½Ù¶î™*nѪUᖥ̶¼öÕÅzJ¡ÉˆeYíñÆPåŽnuåñzÿúw¸wº›{† –ÞÒWµcE»¥é¦¦ÚÝ‹¢—E­PÇN÷—-®HaVÿ`2C0Ä8‰b€èJv耘ì„Ú›8Ï)…T%1EŠû¥"’’`‚CA7$¾áÑÕó[ÑÓÕ:0 ¶º¸0œãõXLVÈ- E¦ eÎ@8´á ¦ÁL¦‘§¡f MNívy¼äLÈÚLœêëMQš~¿q±¢ð@SQW‘ÅïXs¤­bIVŸXòm9åÞÎSÿ¹uÿ?û?ÇN~Цµ 3†¹Ï~”{ÿ­ÑÁ#¿¶òÐg/oH¬¼‰ÓZKãaUƒ'»n{w^ùsõMÍUþÚ¡Âøú•ýU>­–;×Ð_ ‰-=ºààë«–<õ_7žûâ‘Îò<™„­¨ž¿êûÀ5>òíÇo\ï~ø“»W>{}ÃVM¸½ XÄ%K÷VÖ ì½S“ŸŒÌV"›h †ˆšd¥c’Amªi(÷7U•&!Š2‰Ò…Ý„Âô½Q0˜÷ŸÏïC 6ç’mÄÙà9ˆ²™<ùPM7HÏ:> ß+hzžPΔÄX¿øžu¥U[\\ÐÛTnÉa”2…5ÜPX=Ú4,OäWEW†òî¿ïù¡¡îÛ¿0½~ahèyôzéÀ¹_íÝû›ó‹C­ëŽÜÛÙyÏáµ-!ð%bÊËë*·>48ôЖ Ö‘e.J¡ µm¬oÜÜPéMŸâ"¾ã¯÷øðààÃîï:slÃü¬¬ùŽAû”ž¢¥™ˆ•±B› ¤€¯ï.2¢~B(†»G(蟪C1ƒ¡Õ­Í5U•N$¨W‚´§N$Å‚¤-¼Aµ§þwùä40tííðûìïÙ±6Ñœ£Vë¢×‘Ïÿ/ˆðcîMjQ°qyqáŠyÙë2²Ë<žâ€>E¦K”h"zˆ¦d}E R52RÈ“G,bÄpkI€ ¯NžX¬§«e^¬)Ö€ôÉÊòJõW£ýUúèy¢ôû¹„ºw.¡îE„ µ¬=tOçLRB¤z’ê‘3Ë—«2ä?»Í¡zEl̘·6Ú­Uû«óþ źÎ]??4‡fOsOP‹vlíÛg”XÕÜËÀ)—¯×z =ž¨›%p9›,Æå—…ð›’V.#É‹+/_3$X)ÄEÊÅH`C¢ xgdê-Ÿî›jˆ?ƒ­!£ìå¿m¨96BTV«Õcõ°¬Ö©-`}(ÛWoˆña»¨¬0*=)hHEÈùx%(J½íŃ"•j卸i…UmUŒ‰Ö¬d•¢?=¹ûI:œ‘a1§B0ñ]°¨‰ûºÙ *빇ȄP¡æ²ÀO‡y>¿ò1Bf3a…Kÿ¤º½N!§S ìtùÒÁDF´’ ÅP€•„X' øV^Bó¥µE"¥(˜û܇BLÿ8ýtƒžûUA…gzÆ£$„D2õ dþÔODbH¼â_þR€Êq'•m­•ÉÌlw&¢¸_ŠÃ¥ÿ É™©Kù]“zB f„гx]DO)llj]RîF&i­^“7ÇW\Sì3Gæ…cÃ.­I Ž=' œ‘WŸé±Óyò ¹Ñ’^ÃFîÓk(ïd Y¦ÍhÖé2Kòü5kЊUZUŽÓbÓj3Ë %9»áPbòf;T¥}(‚è«UyüþkT š-Žíy›*+7wæåun®¬ÜÔ™·Ç© øk#Vk¤Ö€c “äå-ØX‘Ü€šm(‡ßÚlð[]Ôf‹Öá=×L¼C=Mn"ŽîQõLx&œ:% ÷pØŒzm<9<.I»ªqslÀàb~‹ÉO«Å÷Åù2Ɇ¸ÛT¡A˜BÙú –5wçí–“½·—Æ–OöÜfuÚ­©÷K‚'{OZ\Å-YÙ­ —+ÑšÕRì"â¯`SüéÉà²x|Ið6øÞa³œìC?ÍjA­Ñ¯Š]®âÖÆêßè{˜\¨‘ö'{¤pN@$Æ2艄bÚ¬rh#L¿ ¤@(§’Y´h«ã­«“T×Ö 7Ma,?ìrëÝj·Ï£R@›91-îQìÊwJ¥ø×<øzd*ËÐçŽF¼ÈÔt»îÕ-¹íåu5[–-È,*å*ÕÊù÷üÈMVddL|íðß&=7ÏöÃûº×ÕYµ¤¶ág§¶½zs{çqøå-Tñ¦—®¯5fUø“"™;à~ᩨʬöhf|ñÒ×ýy~yå¢a”P7?Óx¦còj’NBÛº7Ù%†úO䪴IëAW¥Ñ4ejª!añh8‡§Læ Êè¿Jiæ³ £ŸI™ì r;¨;÷=¶bëÕÙ‹Jé/š_xðaø¹Uϵ é|ñ„éÌþªE•B'Ë.ÿú¾á‡w¶ªönCdÛF>¹èÔHaCeDª6ÚŒ‡ö¹ÕV•“U˜å.µõÈ1‡×¡PD›»ŠGïl“›¬ƒåcÈ:&ÍÔれqÜBìOÚÑ%jcÐe‡¦PC2$§ ªBFQ4ƒ†f ‹ÎÅi˜X•‚´CÞK|‹Ì,8â ÷b Û¹Í'Ó=Õ™7òâÂh×c³d½UˆÉingÐä0ˆÕ¥ª!B¶)LûòO¾#N§¢ÊNì×((ðäÈ3ɘШ`C%m…®@{c2×YrË"1iÒ%å‹GBj“Ü“—ôÛÊw--®8µXJš´`¹­ Öo_³‘ Yò*½ÖøüˆQ*[Tß*U¸³³ÄàK…-o,¯=8RÅÓX_•·&m»]Äåë"­Ûz»ïÚÜ xLc!׿Tåy”\@qŒücVU¾W.|E©úÖ['­Ô ú%"‡çGRHŸˆE£‚â*?ðZ ïr#p™9Š ±Ç‡ÀŸæ']øÜŒÒŠšCŸÔ®¢Ê¨Ô&ãiDÁˆHó‡ù‡—¯;7"ƒ3®TÖm g‹” ©Ù›ïÊÖå¹”R5.|hDWqqtI^ù`Ò!‘4ï{zͪ¶ÐOh,ÈJsÊ$•Î œðÒv‡I,,­©=¢±’?¸ë¢‰|GP8p=?´”©Ÿ1a"êà(AøIC"¾îxZñÃE‚°™!T2½n]V&±CþàiûbÊеþ0épŠ BÁD±8QTŲY6„‡n]R3ÚQª¯6d±jwV¡7Ö7ƒ}÷L~–9f!k%Z³êÊ3¶x0ã9bpÿw×*âËÆ.àáÕÑ…8áÒ¹5. k¦àBE xø(rü¢-ÃRжvêȾfý;ß)òUEÝÂæ½qµþ§¯[­¾šX¦PtäGÿñìÂxFaOù‚ŠñÕ?øÊKƒVƒ±°·âž½üýH&¤?ÂÔq¡ü>¾H2$½“¾O A/?‚¢e…Õ.§ÃΣ d9UPQÑ8ÁWŠ%;qR*Ÿéÿ94Ü-7fØÄŸ(´BFñ?"‡á&îPs?yÜåÞîM²ìÿÑö€q×ÚwnÛÞ«¶÷¢Õª­´«®µºl«X²,Kî²Ü Æw°é1z %„$@±! -ä%$á’¤^ É{)„ÖÕ?gî]i%Ëà¼ÿÿ ö¶¹÷Μ9sÊÌ9ߡߴš§Îz+ŽJ/Í ¦\«W¯Þ@žÕ«q_S¸¯o²åù}«Ç.Ô㻯–`%éëõˆî½ÂSýÈl—ó6óËê2ßá>üíQ‘ðã{„WPÿý€® ˜„ƒi‡3@[Í©ï¬[óúëkÆÈYïÐô߸ßp!^`NµEŸ‡Óm&C?sör¬Zþ>}v:‹j²Ú,R¢¤âK£b¤c‚ ¥PòJ? ŠEÉ«&(ö¸U ‚!²¯ ¥ ÈÅh"°÷s¹F¢a\û±—“ •JùÖWçܶ¥($WÊ‘ròz²Ð]²Mÿ‹ܨÙB``Û‹“ƒý‹;Z›ê*Ê“ÃÅáX, j| ìOÞIQ²§¸2ÊBűGYMíû¢döñ­å”‚–+Ä ]‰ÐmLô96¿l‰¥•ȶ†9×t$ë@TuU&]Q^\ûEf“Š-YEÂy“B܃³yC蜺ebªj10܇6›Žþ#ÍÜ»ÒâÖNÙhú>™û™ÍÍÛz‰žmÍÍØÃ<1ÒݳbEO÷ˆPA/™:ÍTæ³q{‰™¹óƒ_iÌOmE¦³«¹€ðÆ¢íK‹‹—n_´h{_²xéŽÆå6,_¾nðÄ~Q—áuóöSX—Y$¦UÓ¬^ƒ-:È=‚LSm³NEcÛ§”Ó9Œyà lÛ–Büb‹ €zšˆaÑ$Ķ‚²!”„¨xÔï5›ô<ág¨d(Tr V‰fkyåváº`é‰ñŠpÓhFx)1DÇ<³÷¢GR}Unwå’âªþš°Š7™9-·V½hÓ l¶lø@gíäªáÂ?)U!Ǫµhi¬»»/Ç~œ?Ý1ë2v_SEtÖ²éRæ~<l‰f—4"ìkã!X°/3‹S塈i@…$W}^ VDÕÕt,êutT3j–çÆÇÏMÏÍa¼QV.8Zsÿ•kÓ»7Þ_‹Íæ5§Ö7[ûüq…®°q¨ºzUsØßuÉòÆ­½EþÚždº¯:23ôƉëØlɲ}›o ùî9Ôqr¢¾dpo{H«³EÚÓþPã²òª¡Zï%ÈS=PU¾$åð¥Û£ù´€3†F¶”ÍPzÊ õUÍ9šíæ)²I‰5Šúà °Õbö‹ðNÄŒÓÐHÑøÉ~fâ{vó§nšúƒÕÒ{ノç„$úR­0Û„¯¢v‡¹‡©^tTû¦nk⹘Ä:¹øŠC,*JÐ k'ƒ#uGspU´WÊ~eèvs¬0$yº¤Ä[~ªP¾ÙfËs£5ù™Þ@H¨)]‘\sËúжƒŸ]S½qE·§L[d-ÙÛ¾hó’x¨mcKéúô¦ôfæî¨ðU•ªzâšþ ŸÞÓh —9[j¯¬‹Øš±¦ Aç¡—:®Æ¾ß8dŸ •ˆIB¡r˜EÒd,Rtu4·ËàŸ‰Ì<ïíOnkì¹vS⽟Yˆù]ý¨ÁT¸Y8òX¤m"Û4Ù‰¶¯¯kÞÜ¥ßpûSën¹ó¢¦Æ‹˜Xöüvô°Ã-&»Eݵ٠máâ®Õ@ït#sÓ_Ø[$N "TÝÍ ^'è¸ú`„ ˜¥Š•é )ºÇœ4Lý]YRpŒÖ:|S{m¶| êÿð—f?‹RáÇF¬§wbÚ|‹óR^,ØÛ!§Ä.§BŽr‹KræÔ$S<¨Â’²²ÂïCTc}E{e{"îKùSx¥y‘Wªg—¦œ¿î¢óN[Ló>ÓŽUwíj8¸wíú̆O }bCÕ†5{6îüÔxíªæPaÛx*5ÖVjZU[»,ípWdÒƒUnGzP[±úª¡C÷#·nê84ZY9z¨cÓ­‘à}‡†®Z]q2”]^¹h¬Öåª[T‰­z[¤q (Ñ{'=‰¢ÆðɾéæY¬ܘO’ÙBF\o4EomV:!!: 0Xª=.Œ…EfÉÌ= !Ïø¦3U hú[ÌPz¼«ýÒuµ»ï»mkípðk]Ò{3m‰´¬ÌÔŽ·„Nìi\ÓDžŒ³¸¢tìªåƒŸÜÖX³ùæÑßÛ.L˜Ìñš•Hóhå¾ã…­˜‡©Šé¿3¯ÿ¿ÐmÌë‹>´fõCG;;>´zÍCG©¨wK#Ô+ê×Þ"Úyõ÷¯nmÅÿ\ýòU­­W½|õ¹ºíSXå<ƒu›b¨”-~c{ ¦_̱0C•ñCv6öî@VA~Å/QŒ¡ß´k+ÑW!ý”÷8}Ü‘vß‹w!ëÙÎãb|æ*|§g±®Àvø*X cÏ Kª$Ö€VõÇj@á… ZTF—ñ1Žs9–yŸï¹î¹–Xe±&Rà[Yÿ=MD£úÚÊT«"ŸÇ¨G-t‹\zޝíaÙŽŸã„¦û¾CŒ·\ˆŒ7@È1¾ÇÜ×ÁgRþrÜc‚ŒŸÇ’:Y„W±ýÂ%un)Žÿæ{XVæÆà_ÈLô±23ÈyÄ7jdžû4×_‚œóK/sbFY\;†Ÿý–EøÚ1øùàÿP ‘ $‘Ò•?² •鎢BE SÜGû¸÷z¼¶Áëß{=Üû¾7dŒ^ØpbMxšZ:ýÛBpK£ufvãß·"[®}rýú'®¸æ‰õ럼và‹•£ZZöƒøÚßÒr`´’¾á‘©Ïôõ}fê‘G¦ìë{pê‘[^<”NzñüZYyèÅ\¾.íá"Øû d½*Û# Šè="RxΠ„ÕvóUŸc0g å\äƒ×™÷â…‘¹>—|?ÁÖ+Ëa­§"õ–È(!»†`èÍ“¡@ΔäòLI4kJõ™o_ãŠ85Y›è\])|)ÒD÷9}g?£5 [+j|ÁºþÒ’¥ EJy48¬îÜ~WÉq‡woË÷©Ô^džjCT¨\»n¢NáÝ©ö€…ÓŽ^3^Jâp0?}¡ ¢©Ë)ÑŒù2ËK"™ó!•® e4¼&ÔåéBíG2>ûÀÕë3ÞšþÔá#-ßµ|âú‰&k‡»R­Ö­lmZÛäwt]ß¿»Ãç¯í+-ímH¨ð`—k:vàÁ–.?ÔÕ¸vqJ¸gÿè éÔÊC‹½je£}Q2Ù¶¼°b¤1tëË«ªœsÇNc’XƒXþZ‰ÂDø|ŽDQK˜æ0ÁjlyÆd¼ýÆ©ÍÖ@o¿ûF^¨DßB?_¥³ŸCË-†A¦OøAiõÔQ‹=˜æ—sl3f³õâˆÍÈÙÄø5†ƒmØdçOŸï£Úl¡MS,Á–£ÌzË1RhM Çñd¥pQuùHáú;&+Û?´¶zre·'et“Ù¡º¦Í‹cÁÖÉÖ5Û&'éõìkÁbᕲ~ó Ë H -Rîj·»‚®¢¥»Úê×¶EB>z£T[¯›ðX d¥»ç#ž’]=]Äòâç™^7ieS懲‚—ÐV»gj³ÁÎûâÚ¿f²™’€]¿[”';ñ3NcõRe¤~Ò ÖÍRù§2dª"á¢óš¤÷öÆq“_Wz ¥ãÒu5»°½qëÖK¤* ¼¨Nºï  ÂÍ+35Ø9qQ㚦 ­rÛ"ñòÕ׎ݺ3Û€/¨Þ°vuJH¬ÌõëÚÂÑÖ5Õ‡¯,lœ;<ׯý¿‘…ÌkG^»ö¡ÃxúÖ>|¤ã‹¥}›kj6áeÑ·©¦fs_é…ÉÂ/cz>†e¡ì+ÙHDhžµ; rvnjّ²ø§‘ùµ=©¨DG¯ü`ªû³/Zøf¦ |¸â˜hs¬_ë‘• G®²P !“XBª?BBÒ(Ì™œCÊsZ…é÷œx/b¿\m¢@R5bIhlö T4ÊT&±¨ øÌFÔL7Ër÷ŒÖ Ü;÷¾æÈóÇÈCÎ]|3ìKºøZ+šÇ+l Îþï®è Ìzx-î׊÷] ÷ývuZ]Ø20"c0ÆJ9Z—á뾃i•«ãA£S&b™°»b$åMSWN{èÛÑŸ ¿BdÿŒŒïÝ+ñ,CµK,þ–¥ow¬ÉT­nÇÛWWeÖtÄ[BånOyÈl —¹\eaëå+¯I$F®\9v¼^1Öµ®Æf«Y×µd]­ÍV»Ž`DÐ?ÑÛXNã±(€ œ5”q—ºs|K1m’¹lK22€Þ(A·[O± ƒNÃD¶å°÷rô@ဌad\úÉ©Oвlýt„îÇc/¤*²e‘}N@Ç1Œsõû5·Q>Qé…¨0O¥ëˆœSépê$ei‹ ­Kõ×øìñJ×¶bó“f›`Tª~RÚT]˜¨ÔÉœŽ6Yá¢Aôgwåâd´©®Ú}7/·šº 5.Õ+¥Se%Q£–S¥zRèó:<_KqŸ«Á ¬Ä«’ÉÍdUÀB ¨tçß*IÆcшǥ×RÕ¨zF…¤Ÿ7šÉ²¾*ï®ÆE±–á’t_]\—±Dä¬Þ•ð¹“>Üh­J&ÓZ¹ÕÑ$K4ö¡?»Êš#‹ŒKê*û3.; d(ä…Çm6y"fW¡S³ U——–‡Íi 4µÏ͵è÷dŸgñiq‰ŠÙ«f1Æ{ø/"¥QÏë Œ<1³ õ9œ}ˆú\†0&E‡&5êÞ-ÂÏfZ…:/ÙB³¿ÿM,«Ô}þó:uúW ›ðm ÿfLÿFܯBˆr‹ADR1œyîʇÊ*<ñ.D ÀÒ4±žaõ³Ø»ˆZ†¤ÎH–^2C¢;VætÔ{º ‘È V»£m««›6…"j³ºÀ_ä-(Zë *=uh%ú²#x£Â›é*®k •ÒFI§÷•ªUr+Z…éz÷;‚ü×"jè4C\±އÈÅqÉŸY d$˜‡Aíά;ÙôœßGÎD‚qQk‘lþF‘x‹Dý+U£G»Í…&GÕÁPÓʪêÑÆÀàƒŠ u‡ðJS•«4`ZTí)õkKØ,{ª§²l8 5fÖïüµÚ Tµ›üÅκfs s³ óL;›é£÷†L”)ŸµP˜Y …nç9áïŒ\kÖ­CZ½UxBeD¿oÖZ”S¯Ëtj9­ó;jØZ¦.Át܀ס[u P?Æ&îÁœöÍŒg&lTºwÆ-£©öÒŸK¹tICiC,â-öt”yγO4³M”k5Íù„v¦G…êË˰Ñ\VÖWë[×XÊ®ÈÔÖ˜¼ »=á5ÕÖ¤SgÔáˆ:Ôeo¸Ó]%uÃVkoÂ–Ü©ÖØÎ"Ëp}IWÚ½¶±Ô[äT«EÞÒFÔ\Sn :õzgÐT^8ç˜ÆÛð¸½T‚ð¸.gùgÕù\F‰ÎëRŽœ EK.¼ %—‹jÈ;WºVçÖØ2îH{u ظ<®÷«íAë ò€u7*Z”ªÊÖeª³n“ß U;+–”$z¾ڊPCm­ï‡júkzQcº¦&íòéãô´û‹EÙ8$BÐ\7›“%#¼°ÅÑ­óê,)KK“`eª}æ©§6|íkhË·Æ_xaü[¢Ž¹…ê Û©?"3ã·øoA_z'yÞïÐYjj&Ï;€îÿ!̸÷¿?Lö°Äà‡‘ü#ü‡õ@¾ðg„GÑ~z1fpþq ä/B¹l’¿HBi }Q‹Mˆâ¤J¥¬êhhl¯R)UE%GÖ??h¶ñÁo:ðæŽ€ÌbYö Ò÷bá˘kóî—©€LëF$x‘ºcè–|;U¦£¡ßN­J–Y÷e‹,°óKþßÎj|^´¿Ú§ÿE/§¿vN;ÈŒ[§Ÿ¥^§ªEö1Íßž#‡q¦6íFNKiípOXø¢Ÿ‡{&áž·Mÿß|~–aηÁfsâÉY“ØŸ \{ûô+øÚ’lQtÆrͳòìV¿—Ø­ :1c·¦ñØ‚â½Òp¯;Ƚ&³Ú4Öѳç5âX] ¨ê@açGüLp•4*-Îä ºF&íŸaþ¢éç fèÃÓã>` ×r®…›ÌÌæÐ¯'´¼kú›øúíY]œÇz.†õœ…è9qÁó«»°/çÇ·"   Q%Jöp÷#"öƒÌËÝÓOâ~tfó´UÁ|mÅÌÒÏ>OQÍbR%Ú¾ø9°)`ŸS ÏùÔôñsð*c¸S4:cw›‘9X˜³»a®ýys}ÏôSøº­gìâ¦\N.  ¤9ÂÚÕ¸<ë(+õûhT)m,kŒG}%þ£yi¯Bšï.Üÿ¨Ø.èǽÓ_ç[²ÿó$-ñÈ”€”%@Ó€»x·‘RPQñ”\%JHT‰áVâY÷ãAHÄJúµ¨ÅXì^ÂüÜ tWx-\Â-Õ×84ý/n=· útHŒù*A9¸ƒ1S¯Ã£ìÌeÔe­[BµÑŠ,âeFÄñ^¼N8)Ò¹ wI)S(Gð"¤d<-…b,¹F¥B9Mú ºƒ™ 〠P%ð¥R Õèùo‘ä…ÉUr¤šÔ 9¾P~ŽŒd}m­jÕ†u#Ë—õ·îhÛQSU‘*/‹GªuK0¨u%rôZ€„°Æž³÷³¶2¼¼¬HÍŒ×Ò*Ù&ÎbD)s©¢XT+”P¶ƒåv„RS¼Lͯ¤d¸Ó2•¸ÿ«¡i)Ð…Ï¡K:w=4‘Ößùî”u¶M¸É™ È4¢/ìzL%ZµzÕèÈ@_OWG[C]MU"îõ8íªJu¥H#Ð3Ôç˜ÿd'þmÝk˜‰¸2Ð-èJaÿõÂ%èrvBXRX‡î>‰îuðQº•9HïcpŽÒg§Xú{R}õ ó([ŽŸÇ?®ÀÂ/¡ e U Œ1ŸΠeãÂÃÂçÇѰð•1Ô…–0?ΡAá¡14ŒºV £%b,i1uož­¦D6&iq‚÷Âúļ,wÐo"à#îšn˜÷¹˜ùÔÙµhÐä/räÞ˜æÆ#C·ÏCęڲ >Ò8z¹›ÙA02üÄ®“lŽy”ËfÅǙƳ߀¿tá ôÝ“›L}…¹›ÂöÃK>NRÞÇ)žãøU˜m…–>Û ·\ «s­ÎlT”><ÇoùÈ–#YIÌN…þ@þ²?“’n î“,¸ƒÿ8zmÛÑmo ¯¢ãBv÷ÑÝÓj&˜o½uò¹ï¼sâ98ëD;™“Ì”[×e­fÜÄc3 ÔO³²’Íñ,^Vˆ¹G2Ð¥2ÜG?ªÀ3‰û3œKWÔÑfZÂä™iŽ›ñ0׳Wå& |· Qå‰x$äʸ3v+d¢BªRÄèÊØòÊÑDEËtžïéo»*‹ü {`k]ùúÕc w:éW¸;Së׬L|ÄoLƒÆlSí(Nú¢>Å®ÚRVâ‹ø„¯âo•;Š‹ð{¹[Šßá)£~Ët°e[F‡£†Ø†/³Ú8r#4FÿIg#åeQô“nÊB8¬/P¾fR+ͯ+ ô‡Ñ ²æNã{݃ï¥Ã÷Ò½Øn̈́әH4\Œê±QvÏ”5€¾./ËLzúôáÐa¿ñu“Jmz]i×áy,¦ºñz»¯]DÙΊ9Èž£G°È’ñÝÏËú°Œ—aÁ¢ÓjÔ3ÂE1W¸@[ž€.Ç^-Ü|BøZ{m9ÛAð¨Çðs;ØM˜fX7zr¨³ó×ó+­;—PðDL,C°"QYC0±þ„ÿ hË>½Kõ¼I­6}[åÔïGÛ÷I²V¿‚Ÿy~¦Û ø™¨êÉ¡,YhÝÔ n‹?Ê[ˆ'Vަx€˜¬CzÐRêHÇ„‘yèŸh¡Søä~½Sõm¥ŽW›žW¹ôû>|S’c¨”¹ËTŽªÏÕÐ'{€cÝRMŠnÄelIo™ÿýÈWÄ"½&Bä1&{ËÔý=aYD›þ ÷nö¯0‡Jƒˆ¥8°Y7’ ›$ÖJ죨hØçÅ—xbѰ̘@¹ÜU ³“ÄUc:”*g=ÈöϸÏ(Òvâéý·üaùª’½7ŸÞöYáݧ'6=…¸»DÔ£ã̇Ǚ›³{?79ñÐÁú™C+>¹ô¢}mM›:£ÛžFè¦[óܶϜ½v»gÕýûšÛ~n-ô?>ý7æ5n?öOj`/<•Làžñ”Ä¢Ù?©:;¶KŠ‹kJj"!gA”#µ2ic^È–€TÉ`Šézl7!Xð2ë‰×ÞôÔÖÚ¡*çŠÞ>…Œ¡Š@°¥!c½ÞÞ·íäâÆ m‘†,ý©ê‹Ö-1n|äPK=(7¨nGÖŽ·W¬~øÆƒ¥»fë Ã!ýPe°ýðx:uñÓW¹ Æ2)3'…vk”$çÒº=´?/PFæa8¦}ϺÁHdpÝÅí]WojhØtuWûÅâ7{Ú»¯ÙÜаùš³W• ,nöû›”•´D£-#Ìpj¤)m)Ç?µøý-‹þªë),ì© • íïì<0T2ûù@gçþ¡’ëÜåÍápsÊm+n-.nMÚlIx-¶¹Sð}¹x`ùô»¬ŽÛKRu$'³n© +ƒ] i±8ÊÏŠ…|Æ?%Ò‰º¢º`4 ÉáˆK¢=‰VÚ¢÷Mù4²¢žpG×`Ùðu™öS?½nðÚ U‹OœÞºþÆÜoÉÄ4*£joã®±“2\3PÕwhYb§rĜڎ+¾yð›Â×þtifò“+w>}ÿ`_ ±à™çMv“ÚðáéŠ-ŸÞQ¸´>\»éºÁÃW.Ö=—˜&> %3 ^¢\Øç¥Iø&šR*µtª_8Û°¥¢‰¦¼™ åq}ƒ£Y*jãÌ3¢8jDúRÄÞ˜³Ù”`ôƒ•¼é¿¿2uô̹7¿ÿ&ï‚¿7Ã[„3¬?ÏHµ<¡åXȇ•| •ƒâiÎC;ƒ-–s1ÐFNL3ÆfEü/è ì$µ0ÿ9U‹žZèo­+Ñ_½B»15”šº¹QãVÓ¿§£vk‰_1}ó÷4Ö& º^£äÈ …À™¹j+Elˆ£”“˜ *¢~¬@{.’^—¼ðºÎ,Óºù—ZfÐÒ2¶³8ûÆ&È|õ¾ÆzôÒ žjRozÕÄ¿ÃÞH–AN® ¸K¶€`>ÎÅ ++$&e·¤ä?fD(: p{‹èXYaþR-Û^ÃþæC_Ͷ2S¡2S½•Ž6˜£FáoÅ^~Œ×…mÂ_ŒQs‰· Zø)öf¨[— Ù“r ”Å=hœUVƒû½Œ‰«„MP¬1ÿA‘tÀë㧦^«7+ Ho+4•m«ùÐÇþ¦w%nþl”ã®l™z w~èd²{d2ÜÜK±/hït„{û¦‰1«+0*$ë„ZãFúD½“úÚQ1¢O79¿/A̽¹â»Âežbá/†˜¹ûÙÔ«@d˜íOͶòh1éÄÌ }Â”ÿ ‡øÈF§EÌ<çãБÇÄ.ÀáäKú¤Ö¦„,¿WÌ1ÒÙ¢óF&œÉM­Ð,ÍHü;&Џö3ýî'ìÛ”°œ XïbÛ ÑM5M5žÿ¨ÀNÙýpT`ˆB—*H=ô(Ì ä‘Ê(÷“a—•÷tÄÎ^»“¹(Þ+Í —8|4ûöÙ¿c۪ϖDûcgOÁ¯Yó°zú|1éöyAVÈ~#î݇[f—‡È£%)©ä—‘*j„7e0[V ¦¶¼«íw‡•FûãÌžg¯…çY\ÃìÛÃN±‹ŸØÉìŽDK G ü‰tÁ²è'ìßq?`äÓ…ÎB?6C7¤z³bÂ\¦"ò—®ÌÀlE0eÒˆûÉÙ¿à. ¸ qxÚžî‚Õ5ŒÐ°Ë‚»gvï:û‰x¤4‹Ž9|¤ „Ó˜ïâ>$³…shAŒg ú@÷33µs³â7Ø€WÍ„Klpì”N…Ë­ªÎ=й pøiF s"%dvïÄ}!sâçä»T“ü$^«qðÑ=5$g¢&3ŽÙ¸•>Wl¼U4p­$7òWêÜElåúnŽSÃ^æ iáþ‘!…d!1{ÃBª>ëõ…æK—pAÓOý”¬t¸ ¬ãeÎ`Nâ½ÞÁk®¯ó8ö£,ñ£r +oYK >BÖûÌ:så$ˆÑ«‘ó®RáϰÐé":_ Mb§ñg–^´ž Ȉuf[Íöòp™ð?9ÐkõD2Eñx>üºÎo”–#¢tl5ÚÄ]eÂYÑwЂ‚¢cx~¦É)î¨È·jÀ‹ñWÆšç7œßfÿŸbŸ…x¦ÑØ À3ÐW“r1à¨Ìb"“Œ¤ÔÕÌ…hÓÜëÌ _wÞK 7§ƒkt+I˜·Àê9fm8{eåÈ@¶\7TõEL«Q&WrÜ «£­¢¤ÍÔ{L±Yr•\Nò.˹§Ðíü¯1=_[rÚÿ5夀y®]/) ät»tkל±Î6¢¤6fÉ;”œÀ9wøè‹/ˆXåîÕéå+[SÚe©¢xÈì-0±ø/ÚM%imÍ™ÛlqÊ•@,)ØIö8 l!Y2f‹Ì(4œ‹H¦—ªÎâ’eÁ s …>ŽÀ“•€æÅÊL*L¢§ q•Û[ =ȰË!Þ¢¾äÕO£©ß»ÓVûv½ÇÇNj/e•œJv/Ÿ¾ýÎá¾ÒLJlbùcöRÙ(¥Ãš:œ XdPZ¼i~?ôz}ÞnˆK$ߨØS~¶Sû Û¬f»Á¯:øÖÝHx º°Cïöq?V˜¯Ë W˜åˆ?þ²‡Íu¢§ßd'™±:Yþwt2b:ç£db“¨L'»î +'t²ª0µÏ­ßa·¦½ÿÅNê.e9:YÍWºœëGb¹3[d+INòé4¯Ò‹BÍvbË1•ß°•јR>õÁ·ïšé„û- Ô圜Iu)éëyYä'æCòQ’èjé$Ó€8&‰xÎ+£Y›<,›iÒOô¼ŸŒç¿Ê|¾«¼`VØÀ±Ä®æ$ÅÒÇ®RlÂÖ'Ç3ܨ4gbŸœs[`?„Îob$G}y7£Yjrá»™‰0˜Ûdþí|z»lb¦Ír4väXjBjËq$øöNÅÈHVŒØƒx¢!°f>·er›)ç 9ìG–i‹»3¨MØ¥Õ­©ö ,z*sxñN«Ãè/÷–´âkd‘sNc3ó凲OcžH q:t0Y 2³ ¬[A²¹~›ÍÑÚXÜj(ÌŸXÉñZßÌ_Fy©bê¢ÓÄËsÑKRzab š¢G9„÷€†Lo@IŠRrš§åü$þE„S™Óñ¤}ûHÖŽ¨âdQaÀçr`Û^§Vò$‘)su:fÀ(µ´%‡FivωÿLŠþ¼ÕgQnµeEMUåNø´ÍV´¨°¨¹ªìå½6ÝA¹\˜·¨.yí~ÞmpbºvÕ}aŸÁ…ßv׆`ljîR^mN§¦§A'1»eÿ€¸xO¨/m‚m¯P«P'Õ”m”PO¨%Îv ϱ|¯Aµ–áyO§ÑLD#ŸÛi³J¥†¸_éN`}–]Çìà×QLáu"aÝøJÄmJ!²_F"¨fë¾Äß(DÐ=7“¸€^j6Új$kµZeõZ=΃žÐÓ‚,þ {h©\ê"ÖD2±èÔÐÞÅþC–d{yy{Ò|Èß¹ï­7´ó¦SZ¯]WØ>^  Ê®\9Þ^HSïëÔ°Zˆþ¦¹—™ïÈ~F)(§˜{„´™ÃÄ‚bÉ,µ9/P…6ÚUJвYÍF½NéT9ñuò  ©2pŠ; AO¤›Ìw„¯ëœæu/郾—‡ötxwnØ0ºÛÛq±ìgŸ4<’[TSÿ殬Ìöôd+W¶FÉ^ Ðw3¿v}Y„‰Æ!(+» }áwüÐSñb]¾6L_Ì­^›÷cèË€º%«—®Ú'Ò7•£ï/|>ÍõëÍíïÎ¥/š6[ÿ Öý‹ŒãK˜¾ëe?%ô­ÍVž+ôE˜¾̧/Ê£/ÏR ¤ éÏV±L®Í#0Úâó^·¸ÔÂóèÄÐÅÞÝ@á]@áŸ*-Â{r²L½?‡Â9úÞ‰é[Š>)y6Vij¥ØÓ `0ëÙo4T€Ìy6ç44Ïo8¿Í¬°P2Ž&ayn5f2^ÆòX•ÊçL§øtßÌl³ sl]ŠæßUl¾Ûš/ð¶¾ç¶ÙÒ\K`?¬9&0b…11{Á +ÊEV$š3cqQ<ê÷9ì³ì¨XˆÑyõÆì)¼^Ù`6…½²˜/šóÊr[è§ç#6›«…d¼ÌlA É"t…ÄF<¥ExJ=søƒ|ë™Ãç44Ïo8¿Í,”,HÈñóÒQêF7ç¡ýd®ýÌÄž{‘ þ›& £pîƒà‚ó>éÇÀ…±pÐí²š5j¥3…f™bv域%Ês’v%–´{…¿`¿sd¬µB;T‘,Œ˜½vâwr/ç ôŠÕ±¨¼Ð Þm¶:%qFV\†eEe£$ @Ô]”ª+Ï_9wM$TXJÓ“ ¶žºR’ÍÍå¥E‰h[Nk¥­rvQ¨Ôçµ2X>lZ¨¶X‹$CÃkQnÏ Hñ¯-lkŸ÷el_ü¯2ˆ¦>w”9Jà¡òì3kä[LFXë±(T·ÛŒe¦²ÙÉUŸ;¹1´ÔŒ!²lê-¶dS¢¨µ¶Âiõ[ÕÛìÉæBü)5o†Ÿ38F<Ž ‡Ñ0·aì~Kø4ýéécŸ>[e©€j!%J¸y©4ô§Þ¸Õó ž˜Í÷ÿ’û€_FOŒðMûô;Ü>îÓT˜ª¦–ž i–#4DÞÉyøl¶„Œf¡HzÙÌ]èÇ‘3±d"‚Èý9}²J÷•µRᓝûÁðÝ¿ºâò_Ý3<|ϯ.¿æ·½Õ j­zíJN¶g{YÅ×j¶Þ±jÕ[kkÅ×ú†›þùغuýó¦ß{|Ý个.arØ]–¥æ ×Î;}/ìÞÿìÉöö“Ïîǯmm'Ÿ%û™'¦ÿÆ}9I­GŠý’í1JðÄx$æ=²}®‚6ÈÙ ÍÎm1’5#* øœ“A!—ñØ}Ob“N˜+ )ËÜ‘CÑäiHi_š>°d_âLÿ˜F§^7ÆóÉá#}­—~mÏž¯^Úv&Ñ¿ïú̺+ûÏS¨953À–½Eh껟?Õ×wêùÝŒçìïú¯Z—ã:§ßaïb7R1ªlF“Ë&s†8'(ºÊ#é ÕB·Æ!X(^SXãóè4T Åd¢I“ë{z¦ó³é=bð|| +mÛÿd]Ý"ASR:Ê˼u#ÕÎìþ‡6o~Ê‚?¼¹mSk0غ©­}S[ðæ·+úU‹Šô6«ÓŽ™SÎÌXg¹:}lñœ:áwä—îüÂ7ð0Nÿý>_’Ú–ÅæƒB2(y&¡Âû$îQ¡ÈŽXX™ÅãfY ůlEK©üi…SÇ$• ûm S å dùgòæt–.†|4xJ¬Yßq4ýh{Тl/‹uï^¼Ùh§u w¬ïÝßg~4uÃöí ô”ÍÒ qcW’òL[—U®¹j€ÞAâþz§Mìrî¶¶‹¨Rêú¬Ú|(¨aeSää¤X”TxUR*¥j™‚«Pä?]³ÉåyT(û¸ö„"pÑ,I¬~_iIq2ñù‹L±°å4¶‚ £)8Sž‹‰+¿2ø¨Ø ÙD@Åþöèu×Mšm´©å¾Í#ׯO×íþôÄØ¥ó «*ßÓvý×/1?;gŸùÁv~ø˜Á´¯g´ùà—¶í~êŠÅõ™÷Ζu·¢o4–ý‰GœÚy>@­c[Ø“3¹‡çTÏá.70pýsÛ·}ãú뿱mûs×ÜU³áÊžž+7ÔÖŠ¯5ôžüŸ¡9|‹[u÷\EZQzj˜ÚÄnfS*ÊF©ª–jÇrrµ…ÚG]F]GÝI}–zœz–šk—K!œ£JD«”Õ"l;Èä¼lTä¤0Ê£fd´ “Õhµ#k²y¬¶Qr¸‘ÓëpŽRÊëñkHy5žjˆ°ñsoÓˆ•\.ô!”Çéõôæ?Ê〧aÓ ú™§ž<óØ£<ôà÷~êŽÛnúä©k¯¼üÒc‡.Ù»gçöÍ“ë׎¯\>Ô¿´kq[K¶¡¦*$ý øì T‘ž©jû7ßç_+»€÷\ÞûðÿÅ÷ù} £›-¿ìßøoͽY›{ƒàÃÔÁ™F¹ËÖå¾ æÞ0—äÞý|îýÜ»©í¹+ч¹ï„»rïÏýȶˆ_øÕâ“]ÒëQéõnéýÇÇ4¸Cº‘ôÿ±yŸ—ÌûlÞƒ¾}`ú]ö W‚¹ÿqQJ©Ân r2,!‹Ï|ENŽrŸ |8!ñ¸KÍ@¢9©)f6HwðR°QM¼¼vÄtÍ5ƒ`©ùaˆŒ3KR_:T³0sEÔo‰Ã9ˆ„¹Ðà†\Œ˜ˆ%îÍÅQ±g~öRz ÈÆ[Ô2“3â8°7Ô±¹­qËê¡Ø—œˆ­šœÜ‘¿kg=ºôÙèƒ˳{в¦-‹cÞêþò³í\Àe»¡®¯ÔÜ´çž•@;á1‰vŸU†ÍÑnöJlàÊ?Œ,WöR%Z¡*-;VùT0Hô’Z}½ óéþÿ@¯E"¹ÔÇí=Âtœ\˜^»1¯Ý…镦dõ‰ &NËË<‰F"ˆý‚ƒŸÃ3Ô,ËÌ´=Ñ…sZJGC„cf!Ø ¦Ñ*Ñó›O{×ÒSßÚ;|÷åÛãµj\©hY¾£e饣e×\Y?9Ö}â2=&EfÃÄ–†•·o«C?Ýwú@ƒ¯ª+ÑoPD’‘‚xß¾ôøÉ;€"BS–Ga9Ðä‹ÓÿdµÌ/¨8µSÔ2*€`Ÿå!é&ðPÙœµ¦›·Ö`[ê¼Kä°Íc±)Laÿ9‹Ç*òB®àøNÐ~å %Í!+gT©â±ñÛ¶­½¾ñ¡ƒZbŠ‡Ê»Ž­L¡¢[îÕh{L*E÷ tÑŠ }mS‹™9ÌcÉ¿&µ|_ %ŽUMÆZH­Ìj ½6€ *ެ@¬…D ç’Œ!÷Ìoç‘;®ð…Ëp!kºå^µ¶×¤”w­ Ç×Ô#¸O—Nÿ“ù«„Ú#-z% jGu.KëfYZ÷1,m›aiÝ9,m biúüCf>X´÷õ‹/Û1HkÔrSåÀÞÁËNmÚ¼êõ⸓ýýËJ—^QŽ6^=^Z¨ñwëÑ’è®M(Þ 78Ì+“Õ3ÓŠÇŸΠ›˜Qnj7©z†¡ –§3«¤¡|Bá^»Ä½ºfâ{š9ž–ºIX¤D%Ôì˜Ìk“»"1’FDÁ>ÄïË8ÊÜØ â$ˆýA¤Z-#a¿ Îé)_¶fIMìÑ–¥«Ë¬~Q½ÈêêÊV­ 1?:[¬rGŠ B…2¹ e³…þ_‹y´-Ûl%'RþYÈ2œäIØœ“¡ â0™!'EÃn§ÝjÐáþ¢Bâ¥yçÙÛç” JgÒôF¥<8¼¼÷ÈHIj属ž}ñ$§R©ì¡t¼¤»Êã¯î.ªhñ©Y—¼6]•^}¢§÷ÄêÊT<Ë+ fƒ¿aEMÍhƒßæRÂ|d.¢«ø¥ úDÌå}Ý‚ beŽHE<‘š&©‰sƒðîYŒR°LA) .•7%Ð ø5]%¼ˆ8áÃ+ùÔ¶ïmÞ¤ŸßvýG¦ ÓÏAE *½‚ÁžÄBô'€^úÓ+‰…ÅÍ^f;Ç5™ -Ì–4?¶£ bØ`¯pkÅø‰¥}øõÚ='.¾xbãö•ñÛJ¿©1îñ™Õ'º»O¬ÎÀëîãÇw_tLªy»Ïo÷¯‚ÚUVà™+€Sƒ¦ÜAú¼ ƒAëÍõ“<Ÿkd£[FžHÃÉ @¥ø .e‚Î*ÍÊ“®,íè(õ7¬¬i>qz…Æ©¥ëÈ×ÞÚ¡ªÆÍaîKfQWñ…Ž¢öÚÒ†Pya“\i¶›ËKC‘[Ñ’EÑÎê`4HÆmÇ<ñ"æ #eÏZäÜDb:6ãn¯1Añ=Ó9èáAû6ƒ»WaÉtŠOq6Ãvá̓|J¯Æl·'cC÷«LS$nό﾿ ðlZðîÄ'ÐT/B³Q@ñ@Ì 7AI\ì¢3äd†ÉAõOýæJ­†û#ã®Vš.}ãí+´Fî,nãôš£ì/Ñ\. ƒüWFáQáz­©x÷…–" äÑWã~˜ ^¹ŠcpE$u>M Y (ÖD0Š„WqÕåÂô~ƒEvØ¢áôª#¼Õp€=.¼ƒ{ô˜^ûVÌgšßR™Hœ/~ÆNü D9c ÝGr!×:HN’»fƒ™3Ѝ¤$¦:ƒü•ôN­p'Ú<õ_j4!<ȧRV³ ÜVi1 mƒyšz{…®æ‡?"n6õ»ûÃSü0É¥Bï²;è‹xóG]Å­ß=vŒ7 £$wMâk¦ñ5pÆ‹ßSA45}1áì)êZÜÄÚ˜ÜJ,È ) àÐT#»Ž>ÍíÃ’ p0ÄÈàh©ÂéµJ“Ê$À|åÜq`pæzÓçWŸè4»ÔÏ=êóè¾0hvjÿÈí³Xÿ Ò¿oµüA£}dŠû1ýCþuò¬¢lœ<¢3ÅÇ\ µ_è3ÿÇÓž4;4ßý/nxlÜâÑýÝjþ…Fû'ñ_xf/ßÓ|Á_÷ìmx¥³4ðóONAA°X†NÎ69çä+/XŸÝ‘—-¸#?o«]qÎ>|Àä-´Ù ½F£øjZ`ó}·-¿Çmö¸ÏhôÅɾã ÷2ý¤ìLÅ0 À)ªX™„婤*ic#P>ËQ`[±Ìp€Ùücj$ÌïÕ9G(²šÙ‘Wáð¼3kDbwÓ×s{1̓Y¤ñ¡-3N¢Pgi—ƒd Vämî£+O48iüéxaƯ9älœàöÎ' ~Î]là¾OÀsÌÀ'” §Pƒðüî¹ý÷bï{n°ý¬Pi VzÃý¸ÿÈý»„oäú‘»‡ŠÜByØe¤ }PZ¤¢äpÛ¤mV@‰Ùƒï‚ÿåLŒ{¥û<…ï“Äý†3" ÕÅ ÝãÇ „†Wv‰6õmè/ÓÿCYAº*ñ6¢ºffv PK0†Îs©l ´ù¤«z¨¦}»w@¡f:‹ßY”2M” 5Å÷øYduXºA¸^LžÎÔ*3PŒ°Õ&9†( [,á Äçr¢yWé¥%Uäô[u F£ðnï¨YVí:qI:‹‚X_ïâXÓPÉ„ˆq¹ÐI+§¿Lpo.Ÿþ2A_Ÿžþ€ÿ¨}a0AñÝõ®ÐN_4ýßDw½;ýßDgQ ö3Ð"î€Qø:}ÛôW1`ûGŒ¼¥sÉWy P‰ÿéË÷ ›÷· ·Þ*οQø9¹6Du–C‚qŒÇîž±08X9dkÆ1û5ëÍüÊÒ¾¦ )h ߯I36CBlçB-tÌBس¦ñ7ômŽ’¦Xi§ÍÞQk*q½~úí÷‡G+ºÊí!¿?d/ïª0› KJ„]®p¡ÒB3é÷£Ò˜ÓKÕC0¯¨ÒÇñ?!‡ä„H©ßÍyßœÎ[ÖX0øûÑ-û§.i¿ÿ~á=Ì„kñz(Áë!‡ ¹× +¼X5·® j8JUé!…^|ë-Á…žþ¡­Ux¾iúIÊI-Ïj pçLF{ Ä{XÈÌ— lˆÈ‘Î7·°T+jŽž5F²zDÙ,z­J‰=0'ròà!bŸ+åäñü£h„çÁöCo¶Ži/‘×{}˜ª>¯7ÀÔŽµ–vØl=¾v„ #.W¤¡PÐ×Ó Ô¦$]µ”.æ‚!§Q,¢¨‡g“EeQ¤¾¾ã†¤ÿ¹ø 7àÿ¯‡‰½¶£kø!OŠp¼”19–¢Îz’ε€!ˆT–§Ñ3.¿ËSÕU² ^3ÝÅEØM°–¤j‘l±cÝj-©¨õG³É‚­añ™ÜWéÙÓ˜ÎõR`7z åÓÉ®‚QDÓ9Oo0K8çõG,¡,v2סÂÄLøµÖp¬<©/´mCX6„ãåþh}ܺ9 oeõŒ‚û.¤Æ³V£?ÇOÖ%hF: `•,ú¯6"ƒ`CÀÜ ”$XT-"âÞ2½,¯á¿"þ%‡HÝ!c4d$5ÈÙ–TÏ[*[˜š)ö U ѕƀ6B“ŽâlÄßä[;°ÊàÑCh¢ ™ú½kØ—åÊ’ÂâîŒÇ¦»µø“29þЕñ˜µ·ã±È„8óþô6l[Ue• ž¦²&=é¿•¤^Ì {gV?_H<^`ƒ¤Äb©UxÉa­A) Œ´øÏ¿Ð‹ïÁë{B¥ðY™;×®9‚ߘdW­óªÆ§Þ§ïÃ2UE Šók ­YÒµ´‰DdÇHv¬NzÙL:jCzÍe«¶ju¸{< ®‰±èlŸqxM°¹5i;qSÇ«¯vpE7t¼öZ'n×ÇnBqNÀÒò#PHÍ;‘9›Ó"î xéÒj¯·ziiQUU» J®$—TºÇª++«p§³íôóÜo‰[šMÎØ¸ØÂ…ÀRv4gäª/ÄÈML²A Ì=q›U ѰÚâãŽÁý]¡‹Önœ\·+ÔµŸ==/ŒY]“ Ã[¶ 7Lv=ô¤ C¯N?—G#L!ìH· oßÐñêkXG¾)hhvúY¢#ßœ~nÊøþ×ðUƒâ"&Ôm‚†9.µ¹ ^¹GÐÀô«ÐvúÛ¸­ü´JUQœàű—¢Ã¼U¢ùL4&7Ì7,(Ši·ÌĘ‚}‰3Òòþ º´·Úç«î-eUUYY³Ó=Y¼¸Ò cú¬p„f°ýP ša¿Ïm1èeJÆc 9‹—¤…#XØ“ä±Ý9°[DP~J©R§ ò.Á¸†-0a2Òbƒ|C¢y^°q&ƒŽ(¤Bºª`y¹EÓíëÖ:õnW§¯SkUh±¼ÞÑãëÑÚ´G7~µÈU‘S£2È“ÕErlLé™B¹IëôŽê‹.™)–+hFŸÌÉ j›wTÄòùªpý¯éWþa†*Î&SÄNö‹ÍØ6Aì)d„a) EB±PHnKØfâjò?E]ý&q6ïýWmñÚPñ"£qQ ”•¶ÆkB%ð©8T·¡Á°Ï ù|áwàøð‚¯ªÐŽß„í…U>xñù"ø½Ç»=ápî“ý6Nñ‹(-5!üÙñ²cèq–X«@ûƒK †nFHØ0ð3µ%ÿWÀ¹žý€A»AèöÃÖAÛÈȃÉoÒÖ”À šH¡«DkMYÓßv-7«ø¦wv/gã6ÃQ“ÕðEái‚Z"´ÈlÓ›1F²AšÇ´ÍŠÖJ/Ù€=2D)åïÅä@rxÖï E3†ùâÑ׿ýÔêâÇ„¥Âûø¿¥¯ùú ¯ /X 료\­/´uÓÊŽÉlÜedÝà÷x½`ÿ“/€'k52~Á'ÓH¥@FÚÈ@D°÷Ê.$ÙòÒÖZ²å!“~„èý!.t?oD¸BÆL«ÈóF¦UøyØÿ-ªèµƒhÄ"ÊÅnàŸà¦ÔƒäÊÓ0/¨Éù qhш_i¦°X8|øqŒOqN­$ý‡f!g’ržÓÄ%†[ÍmÈ”¤rÉü»gZ`ÕË2h4¯%Éßx™ùÚGR£œ–”¯xà–IñO|óÅwÝpõµ7ôO6ú”J×Ý21r× W}⦎u‹‚Š]Ü×ÝãN¤nwI½¿}gwÜ(O¸]Å BÿVö2<‡ŸÇüRFd¬k=xêܤ!È8‡„Í›ºØDTæ³ÁQJ‡ü>Ö&+že#ô(’áÿÙˆ‹Xâõ±‹ûW­ê¿8V·ˆúnDˆóïLoÅ:·ò |ï¬]¸Â]¾YM?ÂÝôGV«ùã[ÿ1£Û±œÎ°åèsÜ¿H|ÊÒl·1\ñ2(¸ÍÌB‹pÃrfÂô 'C&#N›YÖ!* •„K<.»ÕdÐiå<DAH2Éí¸G@ãe*+¤ø×$¢ØHª$ç)èb½žSúõuë—´Ùýv«Fz×–h}Âú’»ÈVTÔyØj¥ÖíöÖÕÙË¢ÚÖ§ÒªÔòP[AqcXøïv‹­…V¬‡l2<¶oqïa~ŽSDzJ¢3 +Y ì ”™‘‹ÅˆáÈ…foE˜u náÍx Mqs,›{¥yš…„›PQÑH0Š;ìF½F¥QPÌÔG›S¹y›Æ†åtª"‚¸Âö”K©þÁו„\<6Ÿ±¨´ÌVÜ[pgz+ŠÊäÏ$¸÷œ©%%ccZ£VZÜz™§ny ¸î/KFªH->¶šúÿ8^C:ª0Ía‡±ä”³›E3hN¥Uã ÃêÙ`AP.œoŠB_ßy/ŠxÜåNp¯ «Ð}7ºãq7þ Ï(Åôý/L_¶ú±-DðMÁ¹æüvLgÜk‹Q6³ÓâT)(2åÕÛ$öÀ Õe¢VVÞA_ùÄÄ$÷`íVÖø|5ƒ•µ>á“×à?¾îÇw¸¿Rª<[‚g ³*0f2ÉA9ã´„ü¦ ‘™ >$>‰Üjx2t™w¶Z8…£±¨c ÷WᑸՌê„N‹×á[ŒläÙAüì[1 "€ÞA`M+‚ºÙs }Þ9Î\¦‰…‚!±’[^YzëÜêÞ3G8{9¥B®7»-銪’Y¯ÔÈ}¡•%±Ž´×•^ZQ¾ÄÃ’ñJ•rñ¢–µ³GÂã©L—Ôý^1>´è…yÃLù©ÅÙvâè;8T0j"sˆÝlŠ`/ÉA^Â/€»ÄÜ%«Qn§Åoõ›Œ: öƒÍÈLRtò; üK‰ <ù‡NX9•³9Ù¾ÂYÖZJ™w~ß›0b¶de1[m‰¸Ão)[V´šÞœº| Àþ ³®eÉâ:Bç^¶ý÷Ù˜ &¬YÂB°á'¥ÁÏ‚@ßËO2v( d’!dS”9ÁÜÙuﮂÚx(6ï Ád³Õô%µ¹ ŠBÂ-Œg[øyö ü“(G2Ù ‡Õ“a±Ø„ L ›gP± ÞRLŸ\Æa‹—ikLS(äWXáùx.s›°Ý ݪH¯2ÊV+'³¥Ã…iµ§¨£{g«§pWƒ-¦ÇÂâqøŠB‰ðò^èZO,B^,áпNÜdËp  ƒ©U<ž@QÎÁ¦6')z–=Û§°¬D0ì)Cðq§Héo‘4M-Í[2a·Yô²µ?¢~hôÉH‘v™Ž ù„w ml˜—šù‹(µû¥X{0“ÓR*£_ÚMdá|'ÅõB\5(ø–I m ðÏ‘3OăÒ'M©Ù–$ÊÚY`5á€^­(òa®3°AW"G}dwÔœ·}øÃº†B…¦ÕΙk.º(¬Ô¨ä¯¢-¶Z¶¼©æÏjU±ú"ÓKçxŽ—U›gI‰‡ ò„œeæ×J†¾"?ÁN ŠÕ¿Éi¦tÀçÜðȘÚH/‘«8V­ìfôª ™ZñKW¥ÃQáú•\#Ê+þú|’inÒð`í,x˜ DÉaf”<+M(cKÉÈwžÚªQqËYe‹J·ñº›·ªuÜ+ëætªõœó'2ùuÃï‘ߤÑŸÏPmÌ›ô¯¹w$ Ñd¶žŒuðÑ›ršbþÁA ÿ¾dl~åŽùníc#袂€}Y«5 ~j»#z¾±EÁƒÚàn÷ áºÑ^kÔm0¸£ÄáÅMË*¶É}AüuÜçßä÷6ˆ|÷ouî‹ÔgE6f^Äï|$TD¯@ì4B¾„Ôgð±…fòèu“oXjíZ!üRê›Mê<êœý‚tžBÓGuÍôO¨ET«¸5 ÉvmĽo‘Ãq²+†¿Æ¬Ï@r*;ûÓHVÕP_ }ÞVβŠržÛÃ’ÕV¦…ô_)í ‰ÖV´˜9n†5*½ÉnôE£¥QoØë bÍÉ,qúœád8œŒxUJ…J© z-ƒœV£©5¼Êïpú,j™LipØXÔ«ôGó¼Ïa.Ð+y™Êè.(ðYtr=Ëáe«1˜Eþþ ;H¿Í£¬Tö+ ‰AœFqchXŒ2g5ì[rQøy?¢–‘ÓæY¬k”wvIx,…úLÞˆ1œ-u*꿸³¼·ÊsˆkÈíÅM‰†í}%U#YðÞdѫ쇵ž³ÇqlÎÇèú· ìq0XjRì}\ëú2ªj¦¨¤S³XÞQœ+ÒCrû?ÍdwS&" E±Es£eÑÒ¾CØjò+ô ¿AT3Ðúx†ý9 F+aNÆ«s•Þ$ÚmXÕ/§±q²I§+<´]¾õöuÅŽô@Õtzã-«ÿ»ú¡´}ëÞ7ž¨Y^ãÞ¶çYöÎöb­ÑÏÛ {ŠÛv/M”Œ]Zº¢«Á”~oðêuizH¨ 6 §6ì=æ¯é/¿¢iðÂc9<^=TìÌi5Ëe4·JáàLñ·Á0Å|Ç3³¸ü†¨)U0[Œ]ÿê¦;'RnY+l|ì“¿m®°¤‡ê™¦æ]· ¬¸cW–YuµL]7õë²¥R©UI‘¯ê¦ßa}\•ÚcO…N ²ú±¾£Å¸)ØÆŸÈÊQ¹Ô¨Çìuƒ‚-ò$JÊÄêãyÕir®ÉÜ •Lú…Vî©mYšê¿~S}ͦ†¯o.Ôë |]Ç·ŽÿÙíË–ÞüÃã7þ°³CŒ2™7éÖ¶xî’Ïžl %Bù`×èÚÇÞ»ùæ÷¾¼vr¼GMôõô;Ü8¿ˆj¢z³]UH.+A4ïÅ&ØŒ`I‚ 6^ÄÀÜ'(%çer x.U„«€‘Å¢±P8 v¼\RçDaÇ+}®%™,í3¶ÕñF¡¨}KÏØ];íº}yz¨#ëuÛÑõó;®øùƒ#÷þöŠ®ÕκµÝq»Ìo0„L‹êØkРBÇÛvòÙ—arò¾5囨5åª_ÝúÆïZW×¹è?£Ï\÷Á¯ª6~rEÏ¥ã•Â5©¡†ú¾·fxÿà ãþ¨`¿QÒõ#Ú'E@K[Mb¾¹éQ¿ØŸJfx S¾S(D?z’ |ð«›ÐSס/݄ǻqú]ö§\%쎓J6âÉò .Åc3odv…ÊÁ‰ÆÑ¢Š‘Òh©ÛiÆê¯Ô0 ÏV²IùE,R©t8^¦È˜+zœñC&ÝÑ=ÿq]ŸÝÇd²»ïß0th¤Þ®³ÊÓ¥{Ž_Ó~â·Ÿ^Ù篮Y~íDV5ŤÆOjzn~íÚtøÈ³ÇÅËâEKº»Ìºò1áìç¯úï/®¿­jò“+F>9YMdOÓô»Ì}Xö´€ÇÔ‚,ƒGV¯iv÷™c@+I±Á´” P2‰' ×·4eÃá˜? È­³Ÿ¸V±õ:ÙZó×m¦˜É-[f\mSé ›×u4ŒÔºBÍã5ßüQëD³?Ò¹¹¥aEcLmÐÊ´\Kß'únß×îoJ×ô•[kwÞ·ñØ“MìˆYé¹ýU‹ãeKÊ N¶gÆ[cžx\¥åd=›Ç+Ö߸ºjb¨Åj¬j[o;²ºjé""ƒ§ 1»1ÿ¸€ð*åH)TîL€š‚jÉüؖ°ºƒv7̯Qž®¼š~{êèBÈß²©Ý]‹˜m2ŸQ24Tq›„ꛦ^˜aV¡–‹Òcý%Õ徂åh+p¶Œùrðð ³cqáÃ’„k¢x `±Gg¬Z)¿èÒG 4l Çbr[yÈÌ™ƒˆÝjȇ^P€Þ–RÇl¶L÷d;¾l>ð¹õeƒM¾gŒVtMéäÅWô^ýûÏŽ>ô×[VÜÔàlXßQÑSH$hÄÔÜÀ^ÓoVj5a¿½ãòg÷úæåíJ“Ë€|fÝFV HoÁJÄï̤Ärâ±®>çEKÙ!H§æ!–˜x”hŠô‹2ÆjŒ€O¡ ¾¨‚x. ä‚—ùrðWBìQ³Q‰¶¡r“ö‹B;» =«1î’Ñf¹–§ý Òi§n¥7ktgÿ9õ6úB àGj%§“ã9yxúŸtŒ R)BL’pœ”Ál­M#}ø‰šl—-Zé©Y^çá‚þ£gq½Õm”ÇolÏÂñØ’\1UHm_òh+%×+ÀÎ,¤0À^ü0"þìÈ«£Œ aŽuIÁ'9$egjjJ F² ƒªŽp ØÑâfçYÆh‘ 3Á<ÀZ„D{¦Py¥;nx>}Íêò¬ÃÄy *Ÿ>Ñlý‚!fs$Ìè¶{ý>ð!ê8s†~ÓàFûFºôê•9ËðEqáQgH¥ð;§n´hÖf›úmž#ÓïðGð¼&©±' *± ‘¸m•q4è^N“ÓkìýÖÞ>Žã<ßÙ½½ÞözÇõ†»C¹‚zˆB`'Á.Š¢ØI‰*T#)ªØT—-É6$[¤-9þlÉ%±c'–m%Nl–";‰8‘b§HÄò?3»{)éûG?ÓäíÍîí̼3o™ç}^®LŽÿÍ7÷-5†ˆÂ¨w&!UÙlÔ¨¤(® !JVæC`BÄ3g )Ê\ þ§èµå÷D¯ÌX\”1óØÖ=ìÃ3SßúæoK.“™n]±×.=zæ4õЇï˜-Ïm>tt{Ëd½ÿJµ_ý³e''â±Ü‰åèß#ǧªðžrˆ=/ºû@‘J-´ì8¼Â‹.Fé‹Dø€ÀJu„£³›6phbÀ‘»â™@ä¥yˆ¿³œ[i1kÈï“ÔÓF—z¾Žre7Âe±ˆ=:& ok¤½ ÃÕˆ¥Â@=öá¯Ô:ê'±ÐÕ¾þÓÕh}ÉiZ¼n†ÃõÀ½FÄÕíAfæGðr&›D‘q’¦r(>Æ ’ò(þ}íj/”¥t>޲,œ‘p¢®1!gbƒ`òÇmpC¨«IU|^1ÏŠ‰ßµ.6?ÄE^Rè–¶/nšy`m*9¾ù–ÍãÉCÛºÖvUèÔ&Iºë/¬}|k-ÿ…==œªk k4z©èóm] ku-Û7š)×.iœÝÌ.ŲáéÚõg—-Ù¹¤6^^;Xé­q—ÊTod( eȆNOuK‹¡‘x…•×a¯à°ÃQÇxxLjÎñË(ÌÞwùÁtOLÏ ŠVŸ82F{/~´Wt—!Ô2Ç‹ý6{žúüm ‘{U(¼ó¢£k :é fRÀ Y)ÌÁ×…/nBZp<ìÂb x|»‡;€(ldbÿˆ=ÖÜá *äfZì)¾¥ËÃEoÛ›Û¨R{mÕÄíý¢g.r2Ð ÷'|W¼›TË¡½6 ÜK·ÌD€wÖó5ÁQ™(”9õŒZI4€‰`fC‹E±½mDgs…ÔøPW›G·(Ôäçnÿ»§¦kÖœ^âë÷ýÞëo´ï^VÕ¾ëáññ‡oí¨Zº§¼áôüÞ×ï_rÇšš™§þN=ñä»g6}õÂV§X2cóüX;ýÒª[_ÚUW·ë¥[W½t:§þ±Ç6#;·^øê¦3ï>‰sï¿ö¾èÇØïçÒ\_ŹìÐKÄ^²ûjÂNrœ uÚ0œ¯È3,ù2w90†½\ÉoŒÂV‹Š˜9`¿Ei~ƒE$¨äŸbƒOœkØxöÙ+ÓCG&«.Û*»ãMG÷nŠ+\ér&¬Óy$Z«eþòÏîøÏŸ¼t ­zóS;©ãW÷oj+K­=ûå·T-o @ÃMf›Aºï”ï%p¯qÝY¥ ™å”P§ƒ_–…mA¢Kè–¿ž{5ó2"}>ù¤(Õ¦Ô~!;^eoyÒìÿœ±Êe6æRúàx¬%mnFæfü)©œÿ#©¤àXÚUcwÔ¸À®uSŒ6-N‘¬ºÍI-\ƒg¡¼™Ð™0Ëï?*ƒ#‰2_Uð M„‰aŒÐ0ÈL\ÎJÒèj_Ú£‡2VÏþÏUöËìûä¶«WäRàø/Ðó?ó/ÐÞv·ƒí8ÃfÎuklJðîðü9øè“Ý ß¡UÄÐñf¨¥ ÄIÂÝoA"Ïd4øÅ#€ >>^…K¼ l1 „ˆÝ—påäºßY!âD¾ç™žý–‹«ãG>?}௺ºåZZ®S«cÍ#ÉÎÙnYçŽ%÷)4 }þmøîZŸŽ½Ÿj\s¢Óçöe7O(H±F X+F¶4¦rÙÀÚ­+ÏåýÌ'`Ÿ’DS¶Þ…¼æ6áüû64àŠ€ó½áièÄÀ«I"Ã÷ÆP% O¦™KJ*82  Â=Ô¡ù·A›FqßÈŽNW {¶#9ÒR®Ö0 Z+ëö-ßpkãôçŽ TnxêֻŠENÙ“ð‡Ù\ªqËH…5ÑjĤb$Ñ—td÷}nÓàkëÀNÆ«mG} ^{Ÿú1ìS-ñg ë+tË ¹“ˆ“¨¤ˆÂ$´ÐYp•·èƒ ¡£M Õ[’”TB·gc„X‡gÕ"…Ó%£ ›ç²r}gºãÐ7c0‰‡ZýÃçbÅABTCYìEÈèR/ÅSìÎ ÑßW÷7ŸúâªK¿îk‘©¥Z­6Ù¿¡cüàp06¶¿¿î`ݦó:¿¾)PFåD?÷%Ø/KÅM[ÏMlx ÄL¯B¬RGCŽêÑ­uM+[½Œ® ü–ô&P>!Ü׫àÞ׌ânĵ@B)HB!Btü1%—‘¤Pq‹÷V")’Œf¢9ŽAÉ`ô… 2çÒâòrq€¹ÖÔüz8{ß}•÷ž¾ë9WvàØŽép‘ß»¯ù½ÕcÐï}Cg¯ì6h§ÿtøÕbÖ\Þ\ÌÙ]¯g&ñxú<¿Å>Ï£¯2‚Äy<B ×€”ÎÉð“×¼<½BÊÁîÚ¹Ó‡‚N@ŠŽKî*iŠ€‹·‹U\Sn sPqÇcpû2†ýž WŽB­…úÓb„(-S([ØÃ$EwŒÕ®í¥[ .¹S¤ñ„{fjL‡ŸZÚuhEf~ß•+ä™+àÀ‰CM·½~dÅ ™{F©H-¿µùÉ÷Ÿ‰NŸßò÷óЊž¿BöBãâ<ûÑtÊÛ ²Y™ß¬‚.% ] .¡$\3ð×r¯ÆC>]!Å¢IÜÕ€â$]tÇÞ¿eôõË' AríºMukßR{õÛýû3KCÝëšv¼6@íy˜ýŸ÷Þa#Þà2ß“î¯0õÝvéêŸùÊ_ïïØ>=°–³Oža¿‹¹,\Ä’ËzNßæ^Ù,P¸“œ5׎ø‰æ½¾1”|Ã¥ »WÈc  £†qL0Ôµ‘JÀ·™+b’(w™/ ¼ õ  ÚÔ?ÀöÔ0²<[QU*Š;¼!Źx}u5>OB¤Å!f‰ ?Š"v™øRÎnƶBAÌE¥p*aQ_,ïY•®Û£Ñ8µŒò[K”2-¸Àuüýü!òé²Ö ÝÁ.¿Ž6É~»;áÒÌ»äRòäü=Ø~øèýÏvï]^©ezJ•-Ñô![B¤;ÇÅÏL×Þ§+°­@´\ßI&&ER Ê‘8¡("²Y Â]f Øœ-ƒäJŒó@+žÁ a#ð¥÷¾h¢_¼L¿rþ|×ñ×öìyýx×yòÞþ}cåó‡ccûÈ$濃þœ™ˆÝ&ÏÌÑ¿Mæ0½Iô°¯Ðw@{1 %óŬª"áÓ‹Ä9ʪ?9ÿ¯•+'G¼Ígg–_ØÞR³ú$3¹žó*s·õuÕºÜóï_|/¯è™Xe¶t‹-³´<¶d¬óôwxíp+ÁE‰úáü;‰Šëè°¤‹)¥IÊ.‘pY!í"À»û¡€ÛÛ;»Ï•yDÅ´ ”xüÁ.ÿÊËçÎõû¿´3ÜÑí·é§¬>ûCÏwàó=[{ì{_"½ó¿"½§ÈÛßß¹úý&è÷ôà-“>ïøsï|7|g3ôÒÙj‹Vh“!Ù•BÙ…20ÉÉ.œ7 _Ùé€Í:Ãøô¹‰K“/ž¦ ENÁ>Pž4@ Ùþêܧ|'¸ç©Ëüä¹×Økÿ÷ò¯¯d£Ìª]Ê~°Tk)tþå_´6öw'Ù¬¼ž9Éæ ×ðÊ+e5(vvûŽï¡3ö{04*ˆ«Ñÿæ)3ÜG~ ð¥j²) o­RAòŒ8üÅéj¸ë“ ¥), zé•Tl¤ ¯&‰gÙ¯‰àó£DŠØÉŃ=€Æ>Gx$&«-šC¼kóŠEð‚fh óD¸ 5{ƒÖD¡1´Aè¿ûcnh7ê=×Ex¦Àq× "ð,X½÷¥[Òáþípú—=°²?Ñx½{[:!…ÂÝýÞr÷/?Ûµ¹ÛÏþþEÚËÉÁÊN,ðS“~þgSy!?Ô Ńnc¿'ê£}ÐBhÉ6Z&È€³X¦h5!QPX…Œ|>ÅH:áó¸œ&8²J…\&~à—iÊ=Å!nEÃ9¨“ÀQU-øµËS¦fŸ×m—/§œ5X„œ³»Ü^KHÏ»mÛŒ )5ëQX{þ[:øü÷_Æ©ÛʺØí'¨Ãçûì÷¨×à<ê9Ä*…k"e>ÇHÕÎ) øYOèŒÏ€ C~àÞ$¯¾¨Ÿ?N~þî|ª€£ó÷ó*€E¿UØøQwí‘Ú×1„)@õj¢ŸSºf£N ,bSðI kÕÎú¶Áªá»64Ô®»w¼gUkD«ÖÑMÝÿçöÃo]¾ÿ‡‡ïûa7B¨¯ N– Ôåƒã&C||*‡÷*¤cG9 #œBLÊk¸C\¸M5U¬üòʼåøÞ92ùá¯àwÂg!Œ‹14ä«ùágáã…|Ò%³%A ¹fþ³Ð€}ï~d¤rºTÇÔNŸ…&Zœ¸-+óy-R‘L*PË9ÚIO ÕírÌJ‰ØOõ$G[„yW£7iˆÍsÔZ ¦Ô¹ËB²¸;ÎøÃ>ô’ ¸2RÊÒñ— Qñ„säN àT‰.ÌýáÙg7MÜ¿®¦ñÖg6®½+%…w^ §&NÕVïí½ÿQ_ûÌEɾӽ¬éÀ•ƒ[¯œêïm] ±ÿ1ØÍf[RW¾²òÔXöÿ~h;þößuÉž¬Ìé0¢šï ¯Ÿóµ¥B|ߌzDI ‰„¢²¨šÅbÐ)ƒŠkJðç9dŠxʬ![þžõ]¦/Oâ“Ö…Ý^Ðçû)ùüs¯»cUª~ëckfŽVËE!=”Jlï8u¿§yŠ>{”­ëËÞþÚÁ­¯ŸBý¥– t±á–ä_YyçrÌ#|í'Eý½åû"Ȥà~C«™ï·“¯IÊ”˜ÛqÝ]1:¶Â®»gá÷ˆ VÕ.TçÅþº~aoõRìlHŸüØÎþÙïN4>¶žïëê•\_[Çoú:Iýb?¨Ÿÿj÷x¾·m°·ƒ]l$ß[@ CÙ¾“n%²'Ÿ&$â\q4ªPŠ ©¤ ö-ºÂÇ'+àÁQ”L@ó OÅ{6CH¤´TBÏ.D’K¥7»1—Uxü¡pÀöù&hDÐ`Aá<›#ÂluFj_…T£Öø2ƒµý»†BÑÁí­Kký:…V\Ýö+ŸÞßÑ|à•]ɉ°£y}ÏÐR=mQ¨+<Þ„CI­É*§%¹òÎ¥gfª\~—Lšîé8öúþm_;ݯU7n¯ò ˜Ts´)Èí}PŽžrd…ÖíÎ¬Ò Ä”Ë)¡P‘[N„<(ˆË—噯r‹ü2r5‹µÁ¼pš†DIc·yݶ°=ŒXlÑYb‰¸ž¼–ñ€"òZù(%ߤ·ºæÏÎ"Yªƒë&w¸jþ9°|ì¶tÅöv,JôÙoeô{úW´~ŠÒC 5à3íì?çE çO \û#õ½“¨&ºˆé¬¢Ö·Îj@áÜô nňP¤{VˆY©‹nP¦œð½@a]øÚ(í­1‡Í祙òZ¦–RŸÚ?Óë6´LÝ>Ò¶/—._²‹×޳Çɵô!"A|‘›{FYßÄ"? Åx_‘àuƃÎ9ÏScþ#påšväsÒãp©Xi!Bä‚×ZLO,vƒ‹’X$ÞÕ,Ô†´äFí »#‡¾*côŒÈ ¡ˆj²x9©I'È#_Љ_Xk´=˜ßßßT#"ãþhG¥ HY60¸o$ÞÑÓF•2èë¢ZT¤ÉÐ3=~"W¡Ÿ0Ó¦ÔÒæ‰£WŸ>1dtp ©×nD¶ÌƒÐ‡ý%\?!bÃå ÜW±Êñæ·^Lu‡Œl~·@ÛhÜ÷8’Æóá™ñˆ©J÷䛇ˆP( ùP‘¢i\š):7ÈÍù4‹çZ5¼o$²jffÕ\lò®™•wM–¯ZÛ{8MŸÿvv±\³Ü»oß^rÇücgV§Ñ Üv°»e¾Êö½ìeêwÔ?à˜×XVf·Å"2Ôè18R?†äƒG êà»Ã}S üÎì·øÑ™DÀ#fŠs °I¬<¸¥p/ZïLËoé=<®_wÇŧ†€eÙ=™WîZâne/ÓFkÿŠxîøÒÜgÏì^=è`ÿ zb kþBdË©ÏN×-¯wqvùQh[ÝEï…2}ˆ“@yˆi"øB8–ðr5=ŸP……wLH•ä© I®è•ª0›ç5‘HiÈKh @ (Øñ‹ YŠIáAeXoV‘Úe~‚ý¡Ú¡eìjÐýY£[ÅþImµÇçü}·Ç:ÃzÚ¤RzÝ÷‘}rôkuìU®(cX‡F0¨Ø¸ÈPØï£ñH\ûOêM8)b<«N%#fÊ“‘86X…ô„TK|š$ XìáSXŠ*î[~㻜ð•‚!RJPå@ØìÊ@Y\!0i^ˆJTšBÇ $yúy‹Ü\sµÊ­åWS5ãø|àEÀè3ÉšŠ¤;Û˜Ò?$•‰h¹ôMÚfz̸Ÿ‘­½Î°EÞª²@µÌVµmj¿úãÚêî€Yé©?4u:¿~þ9µ‰:¸2KðØË¢·©w \‡Nì]p׎+äT)úÃ|=ú#JV…êÂuþpxúÃÌÜüÁä+¸‹Þž“Ûb^_£ƒ -==³ò‘­õõ[Y9~¦rGwv¦Ù5/®ï (îX·ay½s\x?‰ôÕxC  üU|¸Œ1Ž¡]ýCϰJ t¤ó7HQ4ÙMn„ޤp_.«ªª|Ð ùÐ1O€OÅG`m%‡bÅAhÅX¬ÔI·µÖ½éöÛµÆÏE ^Ìûó–*WÄe 9ZÖ÷„úãjh)«*½m5 ¥úørÜ ï2‚˜FǾ|tþ«MFóëÆmãUŒ® ÊÉ(ÎÓõBÙàtǧÓu tdÐþ‡¾4†’ÊàØÁ‹¢yï#J@H%³"¡ìWì„á¸Q°K&ðµÈ¬P¹Ëœ †}Ç`'©‹&õüãÆ¨É3‚ö·$ìjm7”G ÇÓ ²ùWê-ÂŒX-)LA؈œ@¯‡—ÎsW‰;4þÑ…ޓö ßG„yŒ†Þ—ÖWØ$yA%Ÿ{™eþf~F¥øÑ_ÀE >Ó§÷©Ùÿ:Èþù™^•W 8Ì|”}šú:üýŠîÚQ hÜÏPF ²ŠPU 5ÍÇW¡}n úÂF+1aÐøu0†ÒÔù„ïÚAYæ¿î“©Þœ82ªž>9Úµª-np2ïÐôö¶cß½³«nÏ+Ù{Õê­3ì…Y[ìXYß±¥?d-³ÚÚm±2]ûîÏNŸZUKƒLÀ ß?Æ>IEàû§ˆÕ¯úñës‚ømÄ‚òIšB,¹ÈSbÔMØPTÒ°¨›¹¬,ð%ŒŒõÓ·âQ§‘K&–äi¦óÎ:G 0äñ}3HÏÿù–AËþ~ÅÉñðŸÿeãp\7sXý]Þ³ºñÌçbƒÝ£‹¦hšV™†uÌÞmŽjT«û–r¶j ÚfØ÷T3NC_"8PÊ¿CSŒ®ÐwƒÅÐ É"Њ¼&¸ZA^‹§Ø_ –5¹¦ïžN4o¾odËëmI™F"UKå-#Õ Ëë‡wG§Âw)LŠpÐEÎQç}±ßÓtùàæìð¡¥ñ¡áZ…X*–—¹ŒîtWpéJ•Ê Ö¿n„5ÌÁ}!¥ócóTÌ×ç©D#ÁD(ñIóTH.M%ƒ³TÀ{¶7ë¬ä½TbÉ-Ù™ö ­B+®òäÖoKoxéö®–}/n¿í>fþ_¼Ùêú[žž8¦ÎLÆ~§Lš Õt§Þ<²áåãýÇÎiXÛ&ѵwØ'Dû¨wášïƒ6Á Pdµ6 “ûä$%›‘*†C\gu ƒÛ•ŒB¼**B¤šbÔ4E(¥¤XAˆ'¹}A.ç½&-P(ò:•Ãâ4B{DHùìLjװš}‚zBØõøÌ>O ¤sÑ” 4íEœŸŒ1ì † \ ‡$åìdŠÂH@œ‡š2¨Ù{~yO]×ß;Ú¶czÈaœ†xÛÊî‘“ÓÉÐ葉7U2pßü7( u§9À°§©ÚU§†'/íéЕÅàþkuYÃý[:êWukµf8†ËUïcŸ¢ýôY¢’h!ú‰KY›ˆáÏÂ-HlÕ'¥’h€Ié6®@%'‹yÁ‘á•ËÙy%“‰þ“?J°¢ðG‚ äÈø”LÝäÞ ¹¬¹5[]ÕÝ™íoí¯«©j©n  C¡0Þ\ð0ÉxWÌlâÓ¨><ÇP $°Hc?GÕEþwóg6Ž^™ªÛúèê‰Û«æ¨ðòêÔ`Ò§§f¢©oÖR”YÒæ*»Z/aÿyô¶TŶ¶“÷yšrä¹HW™¡«"Y[ùHQø1C:>üZÔëôxkºÁÞzº²ß,eüKƒáÉdï@'ø;!0y%è©I&ƒÞ ç«\{ŸzƒÞˆq0YYÓ"+I] ‡Á|Ÿ¨FqÑ5¨èþQ2ÏU o¬mܹ<Ý¼ë™ žÙÕœZvKc톡 «óˆQ+Sé"ÝÓ©Šñf¿N%×¹·Ú,+†6ÖVO´…¢=«2™UÝQÑ#}Û—µ¹Ü½«OLL_ØTW;û@nù‘•Ýee]+öeÖ&=fWO¨e¦¿ÁnkèŸiõ9-žø¶«/toks9[G7µ·¯mu»³7"‘e/Q^¸Î¼Ä.ŸlÁvô·8OvÙ]plp’e»ð©rVF($9»XSÕª$Pæ ŒŒÙH‘ü*ÄVÏ"É™ù+JøÖJΩ䬙4Ùêg: mR(˵Èh#¿…­¸ùÎ×QhžÑA;R3†™Voc#ßÁµçâÄÝY-Ý3Ÿn$V °^QrIWb|X‡èH—ö¾2«ƒÿ¡÷ÑÙŽ±¤ÍÍ7|ÇqO¾GY8€óCJWÚùå=PXPÐo¥¢“éÔp .![m®åže2­!CÔœ«ËûܦžêLÒ¦Ï~ør¹×åõÖ¢•RçŸÒz³„ñ‡û<³ÐÑáÒ@ëƒq[5ŽM>J·C;¢šÈ‚ެ±J Mº™0)Ü$]0n€ôÌÄd b.¯Dv…ª]— ‚þ95.l|]”û  6.h®û4Ï6|ºg{?ù³³‘B;lÏçÏñ®kË•V6¦’Q_›Ì¦²pôªáHÀ#7 ˜rQ0vá Í/^¦‹§Çj5ÙÇ6.98¥Â¹djÏxM®ùŽå8ß4ßë6ô¯¡¢Ñ.·±«2U«´†¨·ý¦®e(î<ÿ×hŸôà}²Á?™EÛd¸×³ù"©Üv°§e¾*‹6Èêã¶hPÝAöQòW´®Œ: É ;L¢ÊÚUœþX€¤óÓï'Pe]\TÁ\Ðá°Äù˜-w2¡ÆÀ¬…MPþ°T˜ ¢ÖºOñdçxò'{(žGmE"LÔUÔ!xqÈ/3–›QVÞu“E—j&P¤öÞ5™wh pQèîSÁ¥•ÕýI+Ü2K›NŽI5R…Ë’‚Ñp‡ËЯJÅ/¥Ž‘¿DQÇñ“«×Â^g™ æ&4.MàÈwmª:„´…lq/ÜÏ¢DÑCÜ›Õë„lnªH˜(±XÄÔwÂY=Š*B“ÑÝ¡`:´ $Š1Ò@NñùÁëÉFŒkŠé‚q®IÖÛP+СªïièIV•×Åêü^›ÅhàËùDTqsÝ äugÛðûRÛ¡ô¨›JùÄLóg7ŽÝ ˆ59h@4>ºŽûøØšécU2ÊȰw‚fCû)h6L’¿9”LàÍ“þ½yË¡1C:ç¿V‚0j v·³)Î^ ÿf “ áÍvYÜCý_¢œ¨'z‰»²Ú–æÊŠYMS Å.Ë‹‡LJ 7\Üb[ ³‡åx¿ÁM¹¡4/ÖncC<ˆöÖ†ÞÆÞTu¬>^ðÙ­&S¢*¾^Çu1ßÒáFjÜltgYÞǰ}Öô?¼®ž>V-':öÍуéŠíØ*›$‡†w{ûI4¼h¯ÙÓ?Õz˜ц »üLá;‡zÐ_}a pV&ß^¹€6G{™JA½³œxŠ?mÚGC£Ž1ÏGù+€¿ÂGRQŠ%ãúBT_HDHDÄaY›`?«¯k$"°ÑX⻊ YU/*ôéK<1©·ùvö‹Aq!èQ-z4mÂ\dyÌ‚˜üE¨{\̦p´ÒåŒ(ZéˆfZê|îÚdeUµ#"51:²'o™"mK'–¶Ebã·-YñP*¤4k|Õí‰`sÌÆß„A¬yËXZ¢*ÓÛãZeÐ1ÆXWe¬6`µùm*µÝc·•×»Ã連,Ô4ÑR·q(јi1¡w0é Vû®pýÆ­]¦Cû/@lyUÇç r)B6RÁË*æa]ˆ%<šoEŠP>ÛÂÖD¡1JN7ž@Á7£KFTTpÉÈ…¦^UϦálSgÜÂ#êˆt¯éϬí9¡R€¿FÁ<êN½_5ßA¥ú–öTrÃPÕ»¬·Š·±¬iá¢î¦~AøQÑH …bF⃠„›-:‘À[bÈå² à¬#ôU †Ãª;9u¸nðÈTµ¯c][ÛªîJ©¶/Ý´¿eÕã;ÉÝ}G7ß-òŠþõ-Ée->.⮉˜Ó+OAÝÛÏ>+zžú„Ø%¾Çkµ@bå%–”ÃqÐ%²øRŽkêÅ'D$àLþÜÈXÉã,   ÷òÍâ­Ñ‰r›go|CqüêÎH¸2ÎD2MФx5к>?´t÷‡ÞâcëÆïäΤVHª$¡M©FwB¡0Óšè@eçúl{uì¶L|gÇéû=-“÷u/ö÷ö¦~ÿGÓ¨LÝ:¦zl{ã‹¥Ò”P*ßõ.;Gý Ê·•ðË.;Ì$fÃÅ"Ù¢TB"ZÔIŠ ™‹ÅŒÅ¥4!B $àºïsY…YçÃ8d­ÌRN% ‘}\¨œòáZ5zë©}ÍVñ°{µØÉܽç‘°'d6ÇñW÷3ð!£hdÛÙßoIýûßkŒìû»¯Þm03¤‰ý(7•›€ Ùëè«MÓob¾Û3Y9ŠôÙyNX7íèq c|D”ÉwѸ…íÈ·ÃgЉ©’6(õ—ìš-nFÁfD¡ª›ê<Á°­eqq´zL¸¾¨Nx¦v vE[Ñc´lè Ÿrz*„ïˆnïñ‚ºd²¡.•ª£^ Ezg[Ú¶ FËÇö|´õûÜòNNŸ=54:ºþÁëñ´ÿ#]WQc¶Ž+ÚhÙ§•D¢váÛ¹ ¨#œÞ‘±èSŒ9BY\Qtœ‰ƒÍtJÀ% Ò»8{˜éÂ9õ¤É{+SÞÆÑÊŠ%Íå iÈ·\Õ³ý¾±±³ª"ϰt°sÁA¸dw_}NÀ Ðt`gÈW²Çúë–Õ9œÉî¨×H«§îž©DGÏC§šNô²O*”e¶õuLH ž!iê—ÿoä»øÌ±.›‰Cõ†ÂØã P½YLBƒÂI„H„Ù¤ŒdG(ê ÆðIÀ)B‰½¢P.±Ç3PòeíþìƒÊÛúÛÊQúAŪ‰~kRaÔFkz*–NÇÚÛb«´†»¡‘«¤¾y4\Û•®h]Õ?pdEÊJ»[µp—Û:ÑЗ®h[3,[fا(yËüýwýî®ùYòâ]¿ãl’{Ù9Q|?g;@* 8PÂð Î vƒ˜Z@Šx|yž°õÞ¹úû¦÷ …_t˜·½>=Ó} IŸÿNv#ˆLìj›,!wl;ØÝ<_ßg?Ü·Þ€ï@r­4_UBàK#ƒå¸ð‰H܈·dK‘·†§Éfô›¡ª‚{õë °1Orï, dà»rñ¤9 º¤„ÕfÂ8xô¼Ï <_Ür!ÞCi¡úדvøó_ƒÊþßX#ù¨ìQ€‡F?*hx’h…ëÿ‹Ô»Ðª!ú²ò ·SŽÒ¦óA>BÀW™y8—ƒ®ÉÇý„빬<“ŠFÂAƒN¤)×ÃÁ щUM(ÒE„›³©¨$eÀV9oÿž-m3Iä.…IÁ¨Wý²ºÚ±Œ­¬¦?¾Ú®“4¿Ûuw³g.}V¥07vÅ"±Jð-©’n0Äp¹Ý£ÑÌd«ÏÛ0T^5ÚQcúiMo°?DÞª1^MóŽ´%Y6^Sžæ9…á€î†ã¼7¯¹9n~܆jw½0ÿ÷g@ü xè*ªÏr|Öà³âæ57ÀͧÀkl× /Í'Ù.x޵v\û@ô_øL*I”gèÈoÐÂ[Z$¿€(½¡ªÂiGÇN¢"Þ³ `¿ÇP™T&ò”Ïé4’^|ç\obé¾Þ„˜«$K'£GW\¼#÷Ðæ†žsïõÏœ †ZF¢õYõÀé¯m[þè‘Õîn¹ÙÈP•¯íë=´²IFVöÁÞ£_X·ÿ;wÞ+kYwflàðTõ’in°@[øOôI¨>ÏÅãujÄD²0T*è‘`œBÑE1w‘¯ß)YÊdzS!—Rh¢p©²bÏVp7q˜ßCnÉeõœÒJ§Œ~_ˆÑ|&¥ÈyÌ&Rºf CЄ¢R¿ÉÒ*ÀæK³FÛÃ÷7‡¥­…ìdô!·!4:ôJW®\º²·¾’ÝɃ7<ñ¬H¼J#š=z5[¨\(œ½úM3g?#îΪátÁÁBDsÜz,G9RFÌò›X¡ó4-t^Ž÷Y|¸ $4g…ÛnzC‘ÔÆÊ£á2'Ú—áÔªNH$×ðš€kIä„ãá'Ä™W­8°Î…â¸v£BŠ‚êv.*|Bˆ° @Ëïì".> Û£p×Z:†2:k›Ëº‚@”G‰`¢ÌeA¸D¨%• ./JÉ‘×óõ>Õ”0 >¦ý¥ õ@ëŠ{lÊÆŒ6õtOÜ@̱›ç6mã#·‚—Üå‰^ƒSù蓴ʤýg¥æ$iLô¦øjÉÁшŠ+\š—),'ÕÄßq'»r~QÑMŠ$‡û㾎ÈQíRJÌÊpn®RB.… 4ü<¹à,2蟒1%'y‹ÜšMäï‚`Ûᢛ¹ÚÁ6—“ ñHÈçuV»ª b§ú±»¾BçËhX<$ÏÚž%ûçØ-àƒ¾õ¨h§;‰¤ÝªÛíÍëo(|;¯+扮?rFŒì”QG GKz“ñF‰íUj r錤ÐÉäpðäreñX¨ ㎬ºJt£t󧺽O‚»6ƒí ¹l…dÑ‘Ïß•Ms7üþP¸‘¸Ù}(æërÂ)ëëÉ6gR±¨3àô{Ê Ó¦ù¸iC X(ì³°-½àóc×ï#Tó[öØÌau™û¸Ö0ýv­Öî7-û¯¸+x…¹Ñ<‹‰·íß5šØ{ac66ÂÛá㌠>óúL\ ×Ù8ÅÍŽM hh}AÛH”@*i‚“L·yÈÚ9fƒÿ-ÖJ åd±/eÜ—9Þª¯Åæ²ð°gáŸj T#ˆÜN†V£Rˆ$=Êǹœ‘wð¿Ÿè7‹uÃ'ºm«M„ ê%™tö“?…(zH¶!?Wômªðœ½‰ÊU"†úûz{º»Z[ê°¢6&¦XWuq‚ŧÑÞèϋ댶‡Kö—5Úi2ŒvVÙÁ“÷5ûµvK¶‘lPëCceÐä0ð*½zéêþú${p޶!”’!—ñ9½ŸDÉcEõ¼ø<”«Ñ $ܼ›,@F[XÖ=3¨¡Q[ÐY{Öÿ’.ü’nË‹‹Z,«¡‹ò!UJ)…áÿ(c@ø@âêr ‰Ü‚¢¸\)nÂSE÷KǬ¹›É›Ü›­nCí±šO°øMpvíÑÙÑÞZ“†O%«+âܼ"ëØRîa®W¤3ÇB•õ¼íA—³·Áù™ã&÷@¿»Mn€›Ü³pr‡9ÛD6°E²F&•ÊÖÒ&ÅÞo=-LÎ'˜Zd¥œTjµÊ“œ™ÂÏç8ŸõDyˆS FK"¸¬Twˆ.‘…Kºë[¶*i€ܸ‡7c”ŠV…Àü >¨qBœð ébDÂ-:RNI¥*iÿ>Õbðÿp§îå÷ ÿÏ¿oú_ù}$ûu¨"ÿ²ø äM€âŸUÜÅÉaÔP¬Oxâ n„ ໊š¢TiJu£ÛrÜٲƂ€›bGc”µúºÚèÌ0ÆO¼A.j·"SJ°W¿°Áh;ŸXv°ß¶Åx0ÚWÎs—.ºb«òùqƒU½/Ù“0’-sì¾9v•¨ÞíVé58”¯Ýpá`»V¥>Iš=y»Ù[ûƒ–[?÷âý°LqS¬+R“RŠÓ°YÝý)Åž#’¥z @ T8Œ1JAÐÄ ŸPyA)dÔ@A¦ Oà@¸‰øDO@òÔ^ò„¢ÉE÷“EÏ"oò¨l*ÿÔþÊJÑó¿ ‡'“&ˆ®N(õé¾L·ÇVU2‚þÔ}:ýÉÜÈÌ^\Fî;†vWKkS^6â½k›l‡Te©P(]¦:doZû 6×¼t,b‹°œ¼)FN| 1Fl?àeÅ5 '^,x«}#ZðpQYtQØX³z¨ª4ª%XÔ ÓµZ3oÓ(K'B·@˜ZÐsT›ÿÿ<IT|ŒHµ¶„·õ %XÄ.»á#ÐÔq@»\ S‹>êÀ£î…M’ž½þÄÇ<oZš‚X·frbÉPWGMKMsC…IVs²™ ˜ ‹É&éýXáü¤ÎÄ7m Ì¸°/æ2}9’ÚÏÝÌ€_[3Ù6Çîžcgèõ?‚.Æmª2÷1­½Ø¡°kÙ¿`ð_>tſɻ‚<+7èOîvÀav³—E)ZC45gÕëÀ’Ù¡ƒé$âúFæ!4 ‹¯’@ ¦*h!˜V•¯H •*ÛQŒC64îˆcT.FŒáí %ÉŸî0@8ñ¬nT W„â¦O ŠÕWcÑ­b…x‘[倧ôwT¸¹–€h¨«FîXÀçvYÍF­RJh8Í*Tm}-]’D)ô¨È I f|„­H—[}VÎ&Šíoݱ«Öøp˜20“ä1KÁôÿA‘õ^œ\$dq¸£bsÏŸúÃù›•Ü\›¥ª´“"ºÊAÍ ¥ô: 4‡L~kd€Pä>)Û’¢T#PE¨ UiåˆECÕÎè®ŸÉæüí:8• I0ÿ‚a óÙVü1#^üZ<§è1âq9‡&+<ÎjË—b§®Ùÿ(]1‹„ü¥3< &õŸh†?vÖ¯Ÿ|ñæÂäÇÙJ%³s“?ÐvëAv¤øÊÁ¶[n»Ec|”ƒ¢½ŠëÈC†O&7–XDVHâðµ?PǨwˆQGœÉ**D¤DìÆøÜ†óYS 1‰ê°Š‰1g+JœÂœ9±…M¥8vƒnXW ÍsY[9œ˜ªÊh]y"/·˜ô:¹”ˆ€ˆ—G*+Ê^‘,B ÎÐè¬×È.ü¥Í!6û–LÕÔ­ë/o9û›gWÍ4/mŽ›u6©{äë;ÇŽ,‹ƒ¯ÇúÛ{žÙPaÖSo1ÖI_MÀàήi«ß2– ìÜåºõêš¡¡Ìê;ÇÈ[·ê¼•®ï‹\•­á ”…"žê0OÈó|U^ I—SDqÑ8ô‘>òx+f’¡€@¤Ã•³Ä> †O -bÛ)j„⊿c_ž°ÆˆÝd·ÁʼnE,& =³«3i-öalG)”ƒjXÀ-²ò8†s°ƒEì"%Ì"Àq5q¤”ZDà âÇ辬U~t9… ß=,¥T<¾ÍÇðõhnÎדËêí6‚ø*–°ßƒ {„j˜¥ Æ<Ð'„Ý¿)$Q&¯\óØÖúÔª;Æf/53¤EBÛ©C£uSÍžû©·üýMíCw¾¾õàë‡[WôïÑ3ÔŠd ìˆ,¿såWÞ(Œ½ã„ä:§r+×"‰ üL>óH9ïM2ÑIÀ³‹L:ÞO…TtÍC~<€±,tLOõiÒÐ9Þ¦[?q&:f­2Ü8ë¯03'Ü“¢è,ŸãE£A(¥E3Ó¢…ý¾0.h[ í»> ww5ârWvV·­o÷:V5Ö÷U{te2{eÝòó;Zêw<½yíÃõ/s|B«ŸÐ4âb8:¡}Û¿zG{Ÿ«RO„ï\GtƒÙ¾:hã·hI1󒔘j‹ª%_ª‰DX’žÊ™‹iµêÛ³õÝ Ý5ép¹Ï‹J9S(™Ì…]JÉgÕgòbÄ#‡k¦’¨ÞT¦w{°+k®í©¬_Zc+=8d $J‰Èåèh¥­›úBM{¿°sÓ¥¦?·Dƒ^­Ò¬Z¿º‚òr½_sÏäáǃKoípxë‡b5ÓíAŠôöGÍ‘¼aCºlß¹bWjoœÿ½±j¬ €®šPŠß3ÙK¢§1"Eœà`rÑÂÄ91&®tSZ ›úãnYm(˜ˆS¡TÀãÓ/†sCXA¹ý©ù±µãyÊ¥ê¹d£;®D·òþÊÎu­.vî‘•ÛÚO#y/‚¸5ÔΓŷ—ánù·Ya·ÄúDôs¸?„‰âž¯í !ã1$Œ#D#U|‰ß%|E»„PÝÜ^¤¢±òHÀmQÒm…ÆR..(i¼OØ¢tÈ©‰Ö|^·ÝŠ•I„å‹*“ë‰+>NµäW¡-#ÑÕÓ&n˸±ši¨yºxÃøèoK5N!ŸÖOÄQ>­†Ë§² Q‰[±hªEk.®Â€[£x0ÎUWÈxúÉâáE¹‡5®Æ´-GO€÷¶¼¸¯¥ëö—6¤·­ÏyªÔzmEûL¶ó–‘85¿ËªkÞ~aiʬ—4èÎRïôxyÑ7Ouèu¡,ãô;ã“gVØ#›žº¥þ("®8|â,úã•ÿ²îä)(Ô°_–O“'‘ <ªW¸F½V-—‚ D¤¥$Ñ}í:ŸßL _áÉì6B§ ­¶¦L%Ä¢vèÝ"Á†¿A_ {z{—´ „!T亀³HÑ i(쥷C|!ÈMÛã‚Q½ÝÕ•å‘pÉ Jü°B®sM€#ë-‚öškÌ&'¸ž•«gªOC4—õ ~6{auc#§]g74çZã8 Ú½|Íædïñ­£ÖoieÑ”Ë#—›%ZGWEm_\? kÚõìç`ÅÀQ·¼©}c§OÝwËéF«^ö÷Û·2¤Cý;zº¼K}u‡ZiJ«þÚö2½#Ó„Ò¦Á{j›ê5ßP[yõŠpǯý‘ÊÀ¹È½Ù®,’$œå ø$¥ýÌÅ¥ýšC¾àMJû•Ò®™"µAþ·œQ)}™áÚÔPÊVV?–Ú¾ýÀž²ºÑdÓh]ˆQ0pxF§×T Ý»©yíx¬)ÄTÌÜ3³íñ ÕÊLv³)”t¢ˆÙže=Á–„£Ý Õù™ðÀÖŽñ½eºêúfWreo9BÇ®ýQÔB½KL?à¤È2¤ò‰åVJ"sB:qÊÜÔ’¯@á+^3„L*‘Ê$¸òtf‘"†~ ®<ŽK ç|Ø,Ø5åo”Rp݈›ÝŸËjF†ûz‚ž /Xð„QqÄzøÉ*Éd–ƒ4oX]GÙ*©¡ÖÐ"™Ñkqºd “DkëŒgzËuoÊ5“;š7œêŠN.[RV­Ö3þT_MEʾyÛÆ«7›+º*R=U¥J+'Ïm^zb"N!)7+ë¬1·NÊŽ2íM;Mul3xK‚³fK´z–ïis&C&³Ý,×¶'[c=3Õ]#§Ïûáþô[(³V¢Ub¤¡ûƒ+1Â5yÖ³"Š p8èG¡YÆgcð,XùxYÑ×ü)ÔOt¡æøøÚ?ÿæ‰ïno?ñ­Ã³M—³Šs+6Xj–·L]Œ™j™ª·[ª†2çÈŸ;›k+4®ã¹3羨DßÊ ¶8Ra ‚ê.¿kUÒoúšã6ŒãRô<Ô¿ Dñ#Nœ ¨-@Ff[Ä”T–b,1 ^ç…—y L!é24[¬e2^Å"¬NÎÂKÖ*ÆçÕ‚bæo/RÐww.ëjjDG[cOSOm¦ºœ-sš J9*©¤D± Äü~ÃÊ‘‚ªÎ3£™ÐémøUצnYkßҪܽk9Ú©ÃÕeÙõ•QDŽ£H¸SM{>·õÀKõ[®[:Âcýý:Ú»¶n°>dl½ýòÞÙWO 4Լͥ÷rÔB½;q~kSgªÝf÷ØÁþ.ðͦôë/]~ù+¯ý‰ú)õK¨Ñ[³Ív€¿  @Tðm-ÓHhü)cE}o…/ %ýLÔËü°Óçô4OÔÔN4ºÝÞö1Þh: µ ³1EÔõ˜Ê¢“k]q×ÏÔv†q¨âJ¸´rEÕ¦0y-ì?OWNE*Çš|þÆ%ñøtÅ `5{Í ÖÓæNZÂI…Ñmr¤Ë8Ÿõvö2õ œk#ÏF•ÕÊ„+›çq à5¼%}ŒÞ‡ ˆ`c׿„“ñ5ëúŽeææšÏ¯^²4B½5ÿÀöÝÍä?||¿cÎÉøw‚ùîöñS¸XTÊà; µÆâ÷*‡¸ä‡P‡­ Åв$fKާK[qzŸÑ+1åC¬¼ïúhi:¿fÉÑÈÜšõýG3˜º.Á BÐú§FÚp¯æ@z”ê\%àú ×êk ñÏŒÀ()•°ÅÄGJ.st­2@_Éñ¼!C7¹D>IÜŒ‘I è"8Z*–ÒâÙ›ÜVD•€ˆà ­NT{„»Qʛއ’³`›¢&à DàŸ€Ü\ž¼nP…Dõ…¬ ÂLÜ·pœùYpµ®í¬ì/G\pʸ»±OJéˆÿ ?!+œ–gd ¿ÈMg0ðÿ¾ýzá#6eåF !Óäó³Üp[¦pi6ì|©óÅ; P~J%D’©ëšò˜~EðìVD—Ηº@´ê¨Ìb‘ O¡ª¾i´•‹IÓW¾2×Ó¸®+T–éÅÛ­?èmi sÀžJÑgç—žŒ\ïHVÅz’v»|þùpt‰ÚiÑTB{ˆ½Dn‚}2í¯*‹ò)ûáÊk RHÖ?Ã>!Zß#IteQD¤Œ/ãD‹E¢EŠÙ* ,žQËÔ+¸h”/çx°JétJdBò&W8J0(Ù·6œ[S‘½ýÊÞ¦õ˺]©ÑGÆ[zvÆ|=· ÒJArþ£¹9òo9Z“Òº™Ã“OìëÄ´:*•Õe n먟i÷×j pßÕ+ôY¶¯å×Þ§ ÷˱ìŠQ#ák ãZŠR M¡}S Ô¾àŠ ƒ’ÚÂøÔñéƒÞ‡3g kiaá`šø ~RêäªéÖ[*æ0yCí™ëow;—®ªºô¥¹«cà >´÷î¹µ½üÛ« F¿®}|ë ”ó7_A„Á·îqÖ/¯Ã[1ê;‡û$RÙ*’ëTqoø÷-è „oŒTÁ‚7f>æuç2§–ßäU?üøiË ÞÉøô;Ð~o$îå†\Ù Sq>÷±}¤óÌ„n.¼ FìËHŸÌ[»x9”Ã…LK÷ıt ˆ—âÊ7ì\‹-¨F¢Ñg„û[(B¥á’… Z\œÓt£­M]¬nš5˜I£©kI|ý»±s|Uõ_*kÛØ]5Ó ®ÒÍÌ®Ž 5øæVÏdoI ,øužšˆiûtf ÐÂMVò:¤‹Ðp· pí,Ô=—qLú7 Úˆ…¤ÅQÑBÓGéJ.‘yö}Ã%a K1ï‹7àõ€ùºÅ µ.-Ÿ'à‡Z—¾~`J¸Kò£]­ëÁ.löÉÍ ÇS2|p$:·îü5ÕÅÝ1>¬»[®²X¾ïa/‰.ã³ÂíYEXLŠi+Ÿ¾Ê}£ðÈ3ž«J³W³a‚‚· ÌTþÔgQ®o«Àõ s(àÿãóJxY®«YË÷õžÍ:ÔµG7BM5–Å• £]Ÿ-ƒ==ž)&[-ŽË’O 𠮏R¿×± Õ‘qOqlÖŒÙcFYúÀ¡ÅŠvÎß›LjRFRç—ìªùÿ&ÉóÔ[rÝü*STOnºšP1äg afþAøt ûÑÿ ÇSOت¹)Tô–âã116¢ó@z#èpØIu™Ëîqx,fƒÍhSÈ=Ð/W"­ÈñÆ=OÎ-/íomÝÿÒ–-_8ÐÚzà [Nß#ÿ¾äÞ;OÝ#'SŠ»©w ·¼k7ô–¯ùê£/<öØ ä/Ÿ=wþ¨ç–^ûOz ôÇD%ñmŽŒOn„XŽÓYíøƒ„ûÀ×Íô ݇-'Å^+šfdëPW ŒË¨Â9E% qâ³´@ëXz 2©>¶5WÚMŠŽ›Q Åò€×gÁɦPÕ¡|8Ä“^Íö€¡z˜ÝÚØo€‹`þ­š Ó3nolÜþèÌô…21ÿ ûꦦÕ~ú»ÝOõ²¿=ÉþêÌáÖîþSW¶ný**Ör„5žš©®ž95:vrººzú$gCA=ØeÀIÔ#¶KPb¨3 ÑN‰ïEžÎ•'š‡ŽЧ¸\€pÕ»êÓIØ)ŸQÅÀ œ|d4TZUI(DU‚E±À&@½]=ÖžÔëSí£•Ã'Wezù·çžÿ·‹ÝéU§Ø«†pSÔÓÓúõ{ïþ6ÜØë‰4†õk¬)hŠY{¼°fûwZ¶üÜw¶¯}áp7¹þûëz¡±ÃË¿÷õ¯{Ù‘ñ*kþ}8]ºk ³Pâ-Ä=*LaFTUJlÑØ Ÿ8Ùqp¼ú½MåIî¤Ýkæ¾Â´dTÑÚ@Æ$ò/jQ\o™«Ýh!,Àƒ]¯"Ã…?°Ž­aÈìz‡™TVÞ¹Ú]kVSVuÚ ͘žzëjÂd{`ݵú„UiOV“›ð¾ˆû‡kÐ|ªþ©oÜ?õÇöOý¿ß?T9¤´"¦c/QíØ®y…{G ò()TŸÊ…$9XÂ’»’×|H¥åÕ[ ùø÷0èp¨žýì¶DQÓlªX´ìgKZ Ú±¤1¢7€ö·ÑãGîiàºþ/b“ƒûãÇV»ëLÂhÔÅõs©fwwÆ7XÙµ¾µìµ;ñÀ8ÐÄÏ~ôoÅÚƒ³ÿîb/“_¥þ“´*”PyÀÙqrý á Ñ$@“/>ÊÝË‘ÈK€¹P›Ê÷YѬÀ]\Úà:9(*1ÁqÛ¹‚¥wqbpÇšR1@E%L¶×Þ¢Ò·*\úw ÷/BüŽ÷¿°&öÈÃÉ÷êÂeZ¸Œ®PÅWr|ÿ- û_$œü,2Emâœü¨âöì¶%òAå¸)îè‰BGO¥cW">X~Bpô܈yñÁ»N„n0š)¨BRD½U:¶ó¹ëåèT ›©w¡:‚ÏWQ66ât”c‡”ã0Å]åOz- A!ZœBÏø íž@y¤žÅ‚3É¹à’ƒcè4Å KÆÈ£%Ñ@È‘²ÂïÔ_.vÁ/Î36ácUçÃ½Ž˜éµä«Ü«ÐÚ÷¢Tt¾rqá°ƒñ%ã¡r¥Ú(ÖÇ+—Ý>ž#Ï»ÅóGá+À±¡F°mæAÕßá’ÂÅA¦«Rƒ³€p¥LO™Õ [£F¢Ãl žÄdÖû¼ˆCA¨, þ°Ìœ^Ù²Ögú§‹QÓ·6kß–ªÞJó̪íà${ˆz+Ò»±y݃e/{¿´1šÛl‰ÖàЧÏwÃ÷küç^UòÌb@Ã]kŠg¡áI> =i±X¼—‰BY3pÕQp˜—Êy˜¤U€Þø€³ÏfU|[Qf¾øOÏÚ-‘e§gØúüŽU3æÊÞ*rHÃÀbØûý:÷ëš7ö…©·XÀ¾²d(”­°ñÜ]—È_@{c q!«Y€´ÊI’’pÕ ƒ¹xÕwKPÍœYe΃"22 ïp€‹ÜYÍÐ 4Ô¡4NeàóÈžŽF ßp.Æëto@ÞE>¯´h|Uí‰Pp+‘;±´|دwFT˜˜¨Óß1Ý`˳v%8Ö®fí:´dê\Šª\ȵÕбk¼R­ì\^X®Þª õµ²ÏNz7á#vð5wlÐ@FÞ"o1ÓèC!2k.:=*uý„ˆ†„Ný‘§¸¡èœ¨È?¼â ¡w(6–x‡éëKë :[ ÍZïÑÓF¥Âd\½à£Zƒi­­ÿ¿yŠ‹çýÚ ûµ"+·Š–•6çeC]`s³’\'(´\tBümùvD¡YîJÀ x~Ò‹hšjDƒÎŸ.^.7„ô‹KaÚ[·¤ÊµU¢³"ÚëÀß»s(ÚDUÈÞ²Înö»¡FÆÞà‰…bkÙìé ù©¹~Ü_ß t×xíVGGý¬Xe}ø™ú2oƪætªÿÚä=4wå†l­ˆÄèL‘ÆÜ„A)¢‰YÌ3 ×cN0põ]hYs%Yܘ‰ ±HqU›0% ŒÊˤÉþ #Ð6û@UR5¤mçAð-To…ý©F†’=M ƒM°±}Àù^Ÿïœó%Ûç3¾ïuàüžÍ޾ªêîÙÙæžçÓóHš™®î®®®ú×?þ~>>°ø^ɤ#Oz|=}‹ïÓ›¨×ý‹G¬Ik*EÞçÇ}VÁqyã‚6´ 4ˆÌŒZ˜x €ë»ÂàD¤N’”Üé¼ø(¸î‹ýWN¥æ~ʸ¸÷Ò¿5Yë7¾uТ%wGÒ‰øâGô&Anúི·ÌÅÛÕc/ %8:ÉG úÿ꜊·­ó‡ü­E.7kï’9-çëÀyrÚÌÕ?ãÌÚl9˜ÑÚ+ü£ÔKð^Aâ¼ ÍTAd)±äÄ߀ø­¿/Bbÿ>&ӬЎ¢JnÁ9h«›6+H¼ÂÌ™‘…£«Ôbÿ D~òÇñ²+ŠMÐX°w¾j_üo €¡´‚ït7«´ùepü_Ç6MûÕaêEìÿÉ lJd7ä€%UAì­ ?Žè'EÓÉh>–ç̶XÈ„ Po KÉëPÏ@¸+‘a-’Püv­ç7æ"?–*Žçmá‘ÃCJívõ+Ôõ]jøZ½‡¼Ía ø<ÚðÌdXD—Ûqó:o :‘hÝÚéžF¹éÛÆÄZ¼bÊMutW¢%‘ŸäS87G䇢lE"ˆÂµˆ“–WˆÄÌ™0kRÓ8¿O |pÛâ×)I[¿÷ÌS§Zn}ù\Ï‘-ãþÇ”ìÙ60yq>™:»ñ%pQ mb)yødoƒ¥$2r³”À–ëQ(ŠP†6î9^Ó–6!“ÑJR¬Tdkdä-=MãÌQ3ŽÎ-Eî"¦4!ciYÓ)Í k¼±µÒÕQéoíGF[ÀBaX=¬¨F± jÒ Ëˆ—´@ùCÖ¢­¿ÿôgOµu¶sn-R å:yÇû7o¿m&€¿wWï‰/¯KÅ7œÝðÒ3½ ñŽ{7 wÙò“KCÑÃ!«îxÏÄìÚ­;ÒÕ6u0ÚÒ³!®ø·öoiß1^üŽÓQíOúJ ;´PvlfŽ~b‚WÙ–0†… ̶jXÛp·X‡(”èæ%gK$k s q¤Q‚"`oÎo¿cÓS;îÜ’úèü¶þŒÞ¤Éû7î^(îyü¦!êÓï¿qÞ^/‘-šž]çÖ=òIwȭ† í§C}nƒ}¾÷y#¯Ò@úP¤>Ûy'|´W`qLPwçÅ#5Êkx=0g ?'¥{È"6+ û'k·oÏ}|oñÐî9^câÒýÛø;>šÚrçð ûü÷o<öØ©vœæŽ@E?ùȺs»z4‹ßÆûêáxï‡÷çU^$,upÐ%\[ÃÓ+Ù·3½‹ HêJ‹>Ôô{íY[0Ò‚†{Uˆ§\”+€_lºvù¦Ç÷voD]Ög]~ŠzÈ6öþÙ¦>ÝŸxdÝù=²÷y þ8Õ†â¿ÀÍ+ã04нBÝ~h%Iú*jtÁ¯«~Wm…|#ýÿຆÿÁuMïúºïî’Ø*|>ë®ÀZ„ï„s½(\w•æÓC‚áá}…Þ4®»òMÌnŽoûÈ þß~°ímbéQ×…¯ßqï7ÎUqáJ°yøOOƒ 0××!ð4õ^ýý*“…zXêÕ1>Œì Š¡ç%S3KP ý ‰¹ÈÍ2ÙŠ°_xÅqúÕéû¿{ñ·¹ÿÛ.~÷þéO·-Ü¿më‡ÚÛ>´uÛý mõï|-6KÌc0xb[‹Ï@¾ïý|z÷î§ÿøþûþø¹Ý»?÷ÇûnxùÊèè•—o¸ñå+##W^¾Ñó Þ˜Õ÷Þ8Åë¡ÞýÆR~50˜-ƒ<Øäüš:r$ Ä’ð™ŒM)ý*1°2¯Z¬%këwϤº£\bÛ}Ͻt±¯ïÖW/½ø·¤âý剜ÍÙ6Ïãüêõ7oîž«ØöÏ¿è6_ðêÒ­]îÜöÑ´1~?èq¤º‚‘®”½°ãö¹¹ËÛò¶XÑÕ=ŽÖÍ.h}~’è!øÞ ±i´×@[ŸôнKÈî8’$’ Ò«ãÌ4Ô&bAc0CiLËSv o›Ú»,Åo6ÔÏq"oºc¢8wÜâÖ‹ã#3wrF0:þÂùã_¾2¶îö—oè¿&k)Ît§z‚֭׸µ… µmerïê`„!±h5s69MÒ}ÝuõùúC¤‹¹uÓ"ÑK<ª¸XÐ ÷YV+ƒƒ@5ØY$· tãŒKô׉=˼>‘•m–\?ËZ¿»‹¢šaÛÒÑVê-÷æ2Ñ0ì«/–Q`~£æmœ]¶‹s+/$¿žVàM›Õš0•m^•Ü ßt{÷¦VăSœ(æãÔ·MÁ´Ýžô›Lþ¤Ýžšê²ÃßH³««Ðú¹wüæ¢4âdVΧߪ"OkÀï Ä©û@Þž ˜L¤Ý‘Bÿ§þû·…J ¢³û¸HÍ7{ý9*õ"ò¾k‘£±N-¤[ÏÓÒ€ ¹F÷ÄMîGv8‡9P±$°¤aKÓvsŽ<‰ÈÈëo!2rP@ ÃNMý 1ûÔ@ÉÈëÏ‘¯«¸úsz¸ ³)Uø¹ÎP?fÒ€oýéß`ß>7Ü„}cP^‰`ðžm °1ÄqÈJ #/Ltƒ÷-¾òØ]w!t^T?ùýæ4®Ÿ”8­€¦ @FÆ[H©F±œ6ÿ´§usÙŸTð×`’M½s™àŠöM…•þ0\Ÿz¤ä¹¿T¸œÞ„ú«§:?ÜàØr!…ãP£ªÀéúØôÙRêš¾[ßç?~0<‹Ë_¸2Þ×öÙf‡ß[s+È(bàêïØ,æKEpL€"åœÐ*U!Ëg þƒ£–žeäKrâfùÞÔô4„2p"îԋׂȥ¦̧®@•ü`F4|Å;¶ÙÕK­¹ümO‰ìY͙ð½h|­jŽ:žÿKÍñZÀŸ%Ðy'ŸyÛ“ˆ¥SâÀ‘ 톴b!ñ¥ýiÎW“I)Z|p:¯®E¦Ëç×ð³Yhõ¹À¯ßzÃ`# mØ9qóæ\eß½[6œN?E¾ðçO]“KìíºþOuøÉRàçu½@"2p…3žBÄ6BÉhµL:êýo=™lD² 9#cÈ—pýRýöoá¼$fÁ}Â3š€’\å(MªIÀ NázÕ!ü+½âWéõW !'dÆ’P¨ż@Ò”J‰£I T*b!m„²)¤nÙ·æµZ:ŸxW—BS¦çí/EˆW!ÞßöBü!ƒæþ»¹†£+I²bé:hž„†‡19>4;<;ÐßÝY*dÓ±ˆÇPŽq‰ñ Ô­YbŒ‰Þ!èÊÒã\züу3—wÛŽ|x׿ó¹§È_@[üåƒìÉu^÷ÙkÚvNõ»¡-nŒV&Ûúµøú F«#‹¿©¯›9[Êë¿õ^(–U%#vªz¿`©3ÈìD|¢œ[ê^{l¬™Oô­çW€dÐDßÕaç`+ jà?…©c+Ó €¢#DªuÃÐÊ¡{]€ìg†t\¹Æñx\ö6ÇsoœîvòÎ净œ¤œÁGk Ñ'à›Q@5SϕآDÖ>µFN!IÒÈØº\\~±tœ7ïpnK°Õçâ©bÂßgæ0Fã»>»¬ÄoôŒ q:ù¶wq°tºˆ$­fÁ$â]“mÕÙé‰Údm|¬§»:Ô6„Dc<2é¬k¸Ã–þ,¹ÌX›FQ×-•¾‰Ly²`ŽeÔ ãv 0*Ùâ LåÚîÝ>~S-WÞó¾ÍS'SO‘Ÿýó×ÉÆwwœ¾è©Lo ~\wk ¼º–ÚlLr¬k8Ö|#-ȳ–™pÆ“£«dîº~ðci¦Ëh¨ ¿T74¹Þo!çÇQ‚3ïÓ"ò)¤¦±r–’Àâ¥ÄG”7*¹¯àV4-ð¿ôtU[ ¹dK0àB SÂ(±ÄDeV(+Œ0!£|z¦X r¸0Z̈¼{ƒ³¹­,Gå:.TÈÆzÒöxÿÜü\Ü3÷ðùWÏ}lkð˜Þ=ß÷vÔÚ_ƒêG|`nËÜ@œzBŒƒé¹Ž2¯V¢ìpÁ·ÝÞhût{ëÂL!;}¢—T˜Œÿ«¯ºøÇpOÎe‹åá|ÈåumÂ|ŽÐû˜SD1@ü°,^@ÒZ((JÃÉA9Œj¢×: hTDçáЩûš ½T¨pe çÌ¡}HLšC•GKìØ<Á°‹b1rBAÈðEš1E׺\ nlÞÜÑÞÛÓ>Ð1P­ T„0 4¶àÆrœ¬•ÊáªdÿRçë¡°$y9u²¿º©ÓwL/!cÂg6¤GpÎ’:|xÅ×§…TTWÿiÏL¢îßÞF/Å*74å3üùG“_>¤wøôäÎÅo òPt\m£? çé1Kì1/o¨JÙ:=©PͧšRè.NÞÑt¨ð E©i|P„ íX‚€²EAbÙ¢îS¢¡˜)ƒ?7©PP3b‹O«"fÕ5–rm”ˆ§bÍ @½âÝŸ«SÑV¿TjSãsG†·oÙ¸{n÷†©áÙ‘Ù ¦[Á€Îö®àjÊ•Ht-ä4añ ƤÀf²„‘ˆ³ä!GÏËuZ­'Þ‘¬Îµ»3 S®m ¥°.k лuŒŠ¡<®~µ®;S¸¾ûб­:ómVœM¥çß[^tÚí^_*õ r§Áçvjä:ùäº@ÊÍ©¶l8Üã”PnH20ŠQnNmõf»[o½‘œXšHz®¯cÑ ¶Ý³qß“½‹/uõ}Óœ›îj`ßÂRÿ(“dÎÀ&`êq`™÷J!φmT-šð/PÊy@*”èN=Yõ³LÐ4mxo]Ã)Ž`4Ìi9_­‚S@ƒ¥h³œ$#$¤ýÆ©Z×o&-VȬìÀ»;y_+"™ƒ»·'„ á‹æ`a—}¬Êh’ŠMý܇7™·&ÖŸr—4Ùât%­àÎFiZo:2 ŽŸH DY©ö{oß0Û½yê*!"hn]†»‰3NLã ‘{›äyñÈ ‹ Œ¡ˆ{àvêo<ªÛpã¼L)÷’*›30ñihäpà ¾½ŠEŸÓ^’鑟¥i#Z6kî_€wWg[¿:RËéc½3›fzc‰™'çï-¢MKÌõ"¢I+<4¹g<ûÕ®˜;ÿüØÊ §»º2ýŽ{='Ìá‹ðm%"|ù±E£½n¹0‚f¿‰m2ýPQºu;@":«UÔGÀ‹‹×öí =ÕZÔêmäÝ’ çèܳ޾üçúÛɯˆ¹ž‡™ýЂÑ›…à@€E…¾ J.¤pY%–VJùt¢ÂS©‘Œ`hS“‹Mj¼R¢“[QJì¦+üK¾¡®¼žü•ð/¸¦~7©CÂÿ8¯ï<ªõf®%¢Ä± eÄFTˆÇŽr³‘7Fú ˆ?I‘àFÔ"ÎAxöIáH04ÝHŸ}›–¨Œ3F…H°quFí…å7 ÀU6F‹¸÷ðÞ]{Fo®à:óÉë§âôÌÔ†¥P𢱙‘ßúÓ¿¿éÇ5ç¿ÇÏSý£ôWàó·ŸÊ„¶==lDdZjü š~Ç!²Æ8àMȼD´„µ’bc4X9#g™5FE 4Ôñgv’:PŒMÕCÜÖuh<ÞÙüç“÷ çó8?ª œ‘¯ ŠV̳J’’RN‘5B <`CNÚ3<‚ð$=‰p0à÷y ,ÇàR~µ!a\JÕRáKÁRný(QŸ3#RL©¤í[`™›³óŸ™¿e¶~ï{Aé¡Wçw}â¹ç¾VI›¶ìCä!¾Vž8ãK}®óÀºøMýè‘‘¡úžcS§‰ªgq)Ï]}“ºHý>G?ßc4œ/PYF5÷M¡}eŠ2û–lS©ÜÀ ú £Þc:õ$HbØ/X7³28Z)SQ­²¯ýñÓ‡¿¾gàÜgøxµÊèµZa¬­÷Øúdfêhw¸³˜²jÿI522NG¿pit°:)WÙÝöìü{ff.ÍgF·Y-¬ï{á;ùwøNÔ„ðà<±(BŠ‚B‘âùCb†BÐÇY†hêõïƒ7?s¤þ>pìÈ37>™œ:5üÊ+禒àW‡Ÿ¹0L½>|á™ÃÞ3Ÿ]솷ð/¡Lðžz°n@°€’J¦•î.4d¿ŸãÂ\Ìv_cA|˸#Á°Ø s÷dì4Ø{è³7õ÷ßôÙCõ‡NS²–‰cƒ_zã/ ›h*⳨'¨OàKiÔ¡ú·wÕ¿)ô wÀŽýöI…¢¨RÙ( 0N¡-‡P*Š3¡Â¢ gdŠÅ¡ð/ùÃú<«?>¹øtCp¡¸ ÒWÿ@Ù%ì¶`è1<”8§XHkDÐ+8 ²òÎK™¸8¼)äâÈ–a·­†eX…Ö ¶•|¡‘èa•^£ VÖ·•Ö—ÞöÙòñã7žñµÍ»§Û"Ämí›Ùº';yס©®¨!»ý®íG?Ry\¯°:-MÈma~rÛu^® ¹ á­/€½pÊŸ%ˆ±Ç×^"Èÿþä}èY)rŽnJ¾ƒKĨR €¯q&ÐØå¯¾ ¾ˆóõ:Ä(ËŠò?¡œi)¤©Ø¥|ýáÆü D»€X߇ÊàÁýý{û:;Iš¢ü ·¹½í1PÛðž­y£=ïð*w Ï zípì‚}p!½çáôD¨ÂSó î<-}sáH؇ôÚ%2VT>š9X ²x”¿Ðï«ÿÜᎹAèCn·êWj;÷ØcdQ£[L\}_H§‹×‹=8Îéêÿ& ÚáLù1ü¡Ô©Pê7K`XµåaB+gAiŒ³I'¤lËJÅn¸^ÀºŸšþ7Šî3N?FöÝt®º¶þƒ»À®òµeü+Z£›¯¾IÛ¡|CyMrÁº";bi\ß>7‚¶œ{zaáésƒƒÂÿOÆ×_38tb2‘˜<14xÍú8”_¸eݺ[¾€+ ×Ýúüá —æs¹ùK¦áB•Öè(sOJ2×H:'ÀÖ/ Þ챈ÇeB‚̭䭾㚀zÒÙZ%Yht^ŸNNî©®+øtZ=ÓZýèŸ9ÛÏ_÷郧mëSþ“Úì6*²ó—ff`Ÿ¡´UÉ'«ƒ£—¾pôèó·ŒNŒŒ¨Ð¸n#,ô(õ8®Ab^à/4K“u©Èp).gÀÕq`®Ñ†Äs@O E´%©Iñ, ÕjÏF„—b\–Ö‘&ÃÅFNœ˜ÛßóE”18ˆ*ø¿øžAê@aû•Ï}¥°ãÊÜs_¿>üTúÿ™Ã‡Ÿ¼¾·÷ú']½ÿžáãð_an#î¥Gàs„‰Â3A#It ÝUI Ü:´&õ²ø& ab¨öŒË%õ‰p±"é"ÇÍÐfê]VP{5‰Øó_záùXB£šêD]>‰ºÜ;¥úƒN“ÜÓs÷Ç?~wÏž¤F§UŽò;öÄ™îî3O;ö(Ô,¾‘#´ŒŒú!á#âM؉DO! :U,IqJà^ x=Ý«°H³[XÏ+Þ…á;¾ ©r”9$9@£ÊKrR:‘D¯ƒWü¹Œ?ˆg ·”“Îʬþ1*íùìÖù À¯H@2P ¿rÃù¿»gÜïÛä±}oøµ[Î}ãšÃBîzòÀâí}íG"é¶#Þù¡=äá5TíD¯mè†B»¹ÍÝ{Oï z{í4ú|›Þúe+øôðQôA½Ñd}{?§‰«¿co‚{S–è'&‰mü¼Ç…rÈ•d3„¬R”5µŒ\R|5 yX• øäs€X7:098ÙÑ–ëÏ÷£ÄO—ÃdÐi Î›Ym³ÎK5g9¢" åÞ?°" `D]48ÿ°áÎýíPѽ–I/ð]5>Åu9ßÜîCÅë^¬”¾veÏ'¯åùӵ–ò_"· ½õ³ÎqYø:æ´]§¡z뜻0›p‡ÜBÊØ5§Žœºø%œ¯tzaßԾܶËç.oÏç¡â¸ñò¶Ü¢36ºÐ'¨µpMOB¹dÁ5“hwèIÙ™•Ø}‚À­V‚þ4­±-—Wl \¬r…ÄŒŒuüh¤Ü<€*b°i@(ÂSˆÄÇÚ™ùK³³P§ vL$¢C%¯ÁvƒI§«Ý¹Þ°¿š°©å ½û ™»Ý_m‰ôÜÞâPKl¸è¥ócY“¥<´­³÷ðX<>v¸·£Ö_4›süT1Ôq iW¸š¸H¦-lOLßèâ×b]ù˜ÑÐ’ïŠezã&SÏëÀ¯0>‰ÕÐÊu]h”ô"Ž]Èbà†ÔLÑ„ß9Õ:44¼0|á üð+”†@8O¾pihèÒ '¹ƒqPÀ¯–î#æwŠ#¼ì>Òà¤YDýF‚5y68´0 ï¿:õÂÅ¡¡‹/œ‚÷ËÁ='èņ‹m¸ZÂJÄù(Aáú/¬”I*ªX4l5s:ØLãçX®©fÞ;*8¢¡©;øhÝy÷ÖTvûÛîºëàý‡îB$õÔë©éƒ×ÎdnÝ=;½ÝwÞ—k‚ó|Fƒì{dç£ršy‰³@×ÇÂ'G’ Õ,{\°µ5òËP&.rtR¬”–„EªŸØqˆŽ‘šq:U?²$î„Ê¥_8™Ø|ç.®«¥Éò™³€K:®þרqØ•ž÷ïíÚ?ÒòVš|2Ò“v\¸¼¼¾¨Æ¾\UÍPô<)¢ú@{gFŸ6¸_¡Òe§žb‰û99*®€º¼ˆ+‰Je#' 䇭ÿh×›'æ«ÄýV¯ê‡*—sæLy²ä8p€œ—ÞJÇF÷wí½Ïkôƒã}ý¯¡’TŽ XçJ+ì'ƒ¹Îá\AÝ¢¦D`%qìàÔâÑ\ÁtçþÅÔÿóÑúR¯ßùÖ—¨;…9w/\ßß…×±#}‹µ=qn›I¨Z˜ŒZµ 3h0È”ë<™âìC=$YDÈÞ¶~hç··=à·\ õm­’c‹ÏÌÝ·Ðѱpßú\ÝÚ‹ù\çˆÚ@]O؈(b›õÍ·¢±Ë˜ýIŒ‡’t@*/ù°OÿõgÜÝ·¡°í=ë%çö#˜ª êúƒ©çÞ¿þÆ ñ§öì_w ÑGŒ0'ñ}“‚­Âb”tod¡)KÈh”ƒ‰ùåpV­€™_êM:å°·–S|šìIGõN‰ å×ìÝj,¢åßÁç¨Ï#(µé¶KS‹Ÿ$çœh_½JX".ŽsE,V!ÖºâûZÏWÿ¶ÁÛ-Ô j±´x pÚxbèŽC—€§Â±W71ú ‘@/hþZ v†¬Aø¸5Ä)@"ï!KÓ6«YÏêLIÖ+=uzÙówP¿æŒûÕëñíåɼí•ç/çOßèíØÜ>]f°sÑH_Á sÎŽþñäîæ¾ Û²~ü©=Ûâ몧¶ÎwN sù Ÿ’êËäH_é©Só$éƒ&qUUS0yÙâb¢4)äœêG¬Ër¥~ûÔëVnñC®žP°ÏIÒÙÞJ ÷(Ã@ë!ŽÖUØ*§ÐìÑÞÄ—Äõe«î´êÆRñY°|§ÕÍ|‘U3ŒZþÆcÍZ\²ïÊàWûÆk¾Tå_32š–Ó¿¹ŸzÝÂ-~Ì’s: fr›Î¿}Ü”u82&r»Þ åÕ7ǹ5‹XíDòŠÆ¾Óï‡ãÐbåŠ 4„ÆÄ™°,0áˆÔÊ”4WÏ‚ï-ÎèÁkõêõn·}1y¥Ïn!¿{;¼6õ¿ÈŸÂkg‰>ž7¨ûpš0ÂirNŦP>;…dºædlxß,‘5Ç"á˜YnF·çLs«ÉLÁ™Kݰ[äËrî?Æø:÷\ÜüH{¯\Ç(t:¥'ÑÊ elæôºò Jí÷êç`—µ>ý›Ljd{eê¦ÌÄø:%ÅhUNç+ Emqۈƥù×Ûñ»]õ·äÏ1Þ?|ž  ˜h„e -nʟ׮ʷ6ŠÎ 8l¯¢T­p0à [°¬©dµ¹\©<¥åÕ.X$¿ŸÜ”LoìYRƒ™ÒP!dЙä-åOZÓÆTn|{&1ÞÜ2•êɆtu´øãg¦äÏÚG³Í^¥Ù_ˆ¹‹1›ÓïÔjúнˆ+ßPöéö) ÎD :ŒBX|©#2° >†çêï©ÏQ?&òHs‹Ù\ (ÀÇ@­b 5Ò §RzD“Uµ%û .ïv°¤¸ ‘B—wdb*šjó§çnžÞpa.ío›ÊF×OŒø|¤.ØøÒ–-/m „t¤ï´Â–f_{¶›?2•NOá»Ï^{8LØ=Z›A9ÒÓ3¢4Ø´=Âú»²,7x©r»)7X`$ÿ±¾û1rˆšxës’ï@’œ nXŠK ùШ m”ROÝkuÊ~¬2˵æ1óypíŽ<äì Ž”?ˆâ^ ¯ÿe¼ç:Põ”ÒNp"°Œ=ûòcõøcT¼ÑA@œ!úóðs˜Èñi¸×"úÀÌÓ$ʸÂeŽî¸>¯ÇírZLµJ©aV`&03‡‹Ž ¨ŒŠ+"â*<ƒ%øÆ¸ÿpx¼‡'82àó:·ŸüÌñG?~²z=`¯ÿ¸cžÑä¶ÅQ¡ž- dÔ?6wç¾6ú׿¦Û÷½wl|Q¤.aÝÅ7 §D1¤å7€dBŒpœ€¬ÅùY£sv"Î(ÊP¡.=~•PøÒÕÉd*ž³¤S1¿AÕ¼ÔÄë¿ýëÓ}6½î0×vìËÀò²˜E$¿Ãàå-D‚…øk‡ªM,@5ÐãH;ÝUE0(…Àò0PA¨³“‘ß9ÈY™BýgyÆÊæÛ?÷ƒ ¬VÆjYê$§½ý•‘|±>úÏ\ÂáH è^"D¤>ïâ@‡ž5B†Êó¸Ì@F @z b(9ƒXF°XEIE4-L¡DHj§¹WÅš›Õx'Ž„¸‘µ`ZQÔ((FàS ýâƒõÏYÿðK0Ôo0ÿ¹Wæ0ì{IÍâý/µÈ9óê—½xäÑ;´zô`Ò‘4BÝæ,ÃÛá3p„ ί}"‰–@¼âî>/bê>ÜృÊÏÝR ‚†mÐ|¼Æ› Py½nƒËà4á]ô~¹#j5&¨F ¢aœ}spw·»½çž@[ÞʺÕÕ‹ûzû:zŽÂ.¿èÈ &òë­¾Sc­^ÿ:玘Sùíéb%)ä®S§ÈϳEhùxˆ qó³BXEÈÁl‘zÝ\QaðüˆR:à`rã ׈È,µ¬ñN//oÆ›Ž„P,Ñá4‡b «B1éY›c1Púä ~7|ôÎúïwür?zêc'N\B£ õdô~ðMq¼gÑ<9p_µX©Ô<:0aò·˜-Q—È·w3¬ßÃ÷«†}q¡ºFìÅŸ—À ꬎdDp £Lþi˜‡cêҶ§ößrËþSô·Æ/n-’/¢úŸꎛ±ŒºïÓï£"f…%àAJ"ÿ6Í}¢yê»–H¡…æ)¯YRÀ£„C =õoüþ÷ Tïc GФ&_Ä}Øû^ÕTÉVü´º¡à`gœk©P?Uk:¼º²¾¸ü— ×{Aé÷¿¯ƒ-,òG^<ôå#¸;¨òÙv"Etó+xh„´†ì‘‰GRª[“u4J³ ïȺ¾™ìvoÔè~S¾ynüâöRqû噹sɨÂÃéý‘¬·0ÝîõwLSß×ioÔª&ê?‹†Ê;.M |¢B¼K+W) y°{s¥c+ðÓà8þc¿˜ZÅ %Ñ,–îh• µá„?C½\ ¹×0Ææn8P{^W,®ja±¹ùƒ+õ/]¾ ®P'A®þP} ¤6=ün…÷¤+oÂ.Òq1‚ˆNOrè2`½|­~ë…Z×ÂsŒHçF Hú£WäoÅÈÉD¿ÑjŠs4£ý‚k=msm¥’hµr§©Òz™>Ú¾£?¤Ù ÒÊz&À×ñ=vÁÛ}ÞJG> Ä8€èšÙpyª©¥Tî¡.*Ba„´¦ǯ¼›â«j—éÚ_™lûÿ9cµ*¾¦´Yã·Ý•OÕïôšúOœj•¿þœV Uý›$Ê•ºöÁû Ö=ä¡Fr¤ÛqÝSØê;o£þí4B€xîOÏÃóüˆSÁ¢£(RÈSÀ˜ë4ú—˜DF;˜¢P ÀßRâbhÐ-pNa+Vë$¥DŒ^d­v¹N¦5|Cn2ì?C½¦÷}úo‘ä™ýœIþ÷ôÃú ZqçôÌ"¾ìòÕÏx<à çÑÖûÉY]ý<Êÿ‚Ïô Ø7Ò´4‰ºFcÔbCc(‚â,~K18Ò ¶à¾|Ï W#Ô™A½EYÿufˆ-È´õBÈYä•ð·>W} Cé~dïX´HU½è8B‰Æ`ˆ‹r<_X TpQ¼5CYvDņÎõ*-úÁ3ô}Ú€|S+ƒÏú ­G_¯h”pVï€ã}Ðd êÑ7(¾§´j݆ºÀãû^ê ™cŽ!»ø,¯¥K´+±v¢|¬ð´Ð81o‰ýG‡3ŽQ$.ˆÆ2‚—Î[ÕÚ"|äZ­—Öj¼#Aä‘\4çó8lf#§Wb»±™AÚ"æ[5\óg °{täeƒ‘QCÑ”³gÊ=xrv½;ê¶é4ƺñlr0k#\´â\ˆÙ¯Òôƒb ÏÕšr“Îí½F­l™t‡ }Òî˜"5&‡Îkd×Pv2Ç‘¿—ØË대!Ý.¨AQhí6:$fhm¦{¦§­1Öî¬hÁ¸,ËÔx‹Ã²iÔE«]wV`“#â¨5‰ž£Hk”ºƒQyÇK-}9§§:™OwÙe¬}ìpÁjÊ8KÝZ{˜:iIâÞí­;£>‡ÃZWoq¹~f5NÌ%ÚÜ€»ð,ÜÞÏ\CTá*›œç¬7’®äHÄã€jFý¤ZöSMhš—‘k%ñáÄrkNÀ“Mãt<!ͤw“Χ]JçÓ®º ߺò „ФU“ˆÂ’¤hr~é‚+N…ó.24ˆÑ‘Á‰¡‰¾žö6N>B“˜ XU½DãÕ” (‚Z¨ÀóRJ4Šþ3Ö,ƒ¯-ˆ^عÄhÙ£â@@Þ[ô·8J½"_(”¶z< £3S®š´FÆ’ÜÖÖ:btÍÇ*È’ðÉ=€f @¡iqâKõ=Ë©:µ}‚GU¬¥Ò±ÒDŽ5ÎÈÿ2ß8se{>±Štå)K­á$U¦#Ášò4CÊ;d1ê‡YÒÍ–a³ËRliªÿ$SËåGÐOäÓ­­¥L:î ;M*½!Äß‚¼G¹’çScvVá.´t',æÔ`Ö·ki\Îé†[Z¼%+u üÁñF\…xý»ñá¢[¡*ëÌ&КÌ:­õûÑ3ŽB›úwpFˆý¼>mÆ|ºÐ´×h A#ßN„HV(L¤ƒø$µ¢IíùX(ˆGË&ÑÚ<óÇ•Ra°ùíÝ=UwÀÆ¡ù?PLOw†|ó­³êŸä2•VµqlbO’\ Ø»£³²} bÿ}”< ߎׇaEBžVéGd€NŠ9$1$`{é]ˆóãl``)S¼yÝ 7‡z¶T[ç{BôÑ}¬}G_(Ô·£]ð‹&OÓGá¹M¹â-uØ-¯ê_™›ß*y:Ô3ßZÝÒºù†ëè£ÛÑ5·w|ìc"_"u7y3-š]Ï2Ø-!T´™‘SbJ¨nòÔ ´hîK¹ZS‚ãBHœ[3†^«=kö'¤0úqê›À,x> Æë_]ɬËZ€@ÓÐ;œGÓðlKÜS²¡þ¾ uÛ˜ýDØÆ«¯^J½F Ç«ÏÁ”@ö¥íÃ_EçJî³â°ƒã˼eA"0†Bkð–5hˀĆ}?äžXÄž4Þ4æ`Žîd¶Cg«efjgׇòÌþú×r › ­«-Aw(ÏÇwσ®ú×¶dR  }Êß\#qb˜W‘WŇֽ¸@Œ¸ª›PÓ8^†3âŒÈ¾:´ü×ÚóæP¸¥ùÈŒù¥ÝC’™kÄÏ|ë#ÑŒ-Ùv'm™hd½Ï”Vk~O*µ'oµ*úß_z¡hKØí‰¶@y!í³dœFöùÂ2 §È`]ä6òïÈy¶Οª:ÐCa g?bÎ×bɘÇä[r€|§‚zA`íÂnD6(sm9{N!£) ¬XµˆB3±X^.ü)qy³fΚ"·àüDê¦Ã'Êã9k>Û?âm›Ú^(eb…$—ðú³9˜²z¨…ä7ȃ×ßéÍóþ„Á´­'1Ñ_Œ éÉíõ¿±ZÌ¡œ3”´+v¡÷c%_#ƒÌahq=ïÑJX¼è‘'v¡á „¿ð#òÿ`RÊ Òª÷ÞD0 ×§±„xTK“Y»¨ ²!µ”Ìü•7Ñ4ØÂ¹‘r@iôÚ,mÒRÇ+Ý~òµowÝ|°/mræ7Ÿ Œ-úò©k¯É[È“ƒò¡È¶B°°žªnZ"ÎîAïàODÓO†Õ­L+[-k àO(1œ0 …¡H¼PÒ7/‰q)!ì™P-ªØXqÌð—O7½íéïx&†ÀkkmOŒq™¶å¡—ÜPBQ3üe§2ø=©ÓA>mò8Rýý)׿*òƒ=k­m5fòYӀџ°Z[|F­+é.õ0ßÓ«ä*¹Akô…3½›» ³ÝÁ`ׯÂü­#[¦9]ÏJñ€VᎵ†ÜŨuX°I7’¯Aëù ÒÄ̳.À)æçB Ã!Âæ(|²3¼Âg•£È)zº~«4ƒ 7á]ÛÛ`²³¤5Â?%°+‹˜Á­‡¼þ‚hš1¸3ƒ²B†á»†Z¡0а:ìk5F‘ϼÔ!|H÷êVA.b'DØ(Õ£×nþø„Æã3(Œ¦òˆµŸ”1ëhN»ù£Ó*§ËdrÙâaŸ†úFÀú˜R¶=í6§‘8l)”s«þJ©­?úU«-ïÐË­1¿ðŽª_"Oa"üÄ_#^VŠ6ÀµDŠxsnT“‚dœãôªts‘®À²t\„6Åœ•â2ú‹—0½Ã%þÂÙh1= c!Æ”¨,G2„’e6dñCÞöMÕì\OÄÓº>×Òç8{Ö\q—º:r¾·0Ýá önë@ ‡õ› ë²MÌÍõta]{cýJ‚¨†e#¼-ìñçgˆóϺšƨÍÍSñCyǬ¤Ø·/>¨­© < $ñS%*’ïx‰Zcñeþ„ðþÎ_ý-;Jý3Q%z‰Ûy(ˆ.%á.'ƶãKCË šÐJ‚f•ô< SÞšûPùŠl™‘y”^ÙXÈçD§ˆ(_ËN¨ñÆö¶®Î¶ÞöÞH4ŠÀ5¬WÛd¶¤‰˜YïA¿Ê$81(‰¶±j!òA¸ØÏ?µ{Ϧ«¾+é‡NO¾÷`§Á¾x-™^l€ßÖŸâTz6çß´k_úæ¿n mÞw¼‘7ÒÅî2Æ¿Eà¾Þ1µûøû+ÓmÇ?~°Å¹åÊæ”;äÆeJaÑíÙ±’ UÖ‚ú=7µïŠ ã¨†3ìÓp#D8Ë›Ó)œ¦ØBØh‚ò¹áÎ"&4 X‚Eú>‚ÞGHÜ¥º>CŠ„‘•pưˆ„fm4Ä:]¢q»šl¡InJ (Ô·‘R×°ƒ{3Ô”1‚AšŠr%’Ì졬¿*Æ.?r(Âj«·ÍÃŽyª´kÛ¶<¸ev¶ûp¶´s;üüÿª&o~@æC{ú1bÁgÔƒçàꯂ~M<ÔÖ݈À³¿cAO%´“ýêo©{á˜l"vó;6 ð9Ôc)ö\}=K¯2@Éö2ÈP¾›ò}„œrfÒwXôüŠiB¡XòS˜éþ3¡d0œ€ÿøÉb3™mÅ‘K”Áµ˲|¸FI HXh¤t‘pªI´>¯¨­Zª½Å_Š˜ÜY~pÐg6eÓn  ue‰é&7} ß"7s\ Ågcmƒ;Ç{fÜLé)(b'×Í^Î.±ûø3A§Ãmb+ µ953PPù¢ˆç§·´ÄìÌ£&fy9¡1§6”$æø)ah¨þu‰ú¢B¬ƒÖ÷‡x}wŒ¤˜¬ž”‘2 —I–uR¤ö„2P drR6ßÄó ‡œÄ´ˆèÈ 9ƒ‡ÓšÎ¨ñ®ñ1¨öMÕÆkÃ]~¯Å¬Ó@Õ¾**¬ÓQ˜8œˆ›]®­ Ioø?€Þ•´OïR$Wl¼y}íj쀷´õÂxwk[yÓñÓÇ7•;Žÿ¯ûÜ—ß8Ëé¼)ÏÀúÒ¶÷Ôï óÓÛv¦ò]žî]}½ûCÀ»s×î;ö8r#õGÊ=AVÇ ŸÚÒ…ÓÁ€žV˜r³ýݧ·TâÃÛ Ã{ÝÆÞ2\´-ºØ­µáSÓIÚ 5d%ËÈ£k‹Í‘â_=<¼¿wðätŠz}Ôi•ffÆìîrÇÈÎs3ënÛß‘›9ÚÑuÍŽ1§!K»"›¶ïH1²¯÷­lv¼»h±vŒÖб¢'Ð] «oÒÏaNÏ0QåËJÜ¿,IË]“v®ËÌô;ìáóÚÃŽ0â%4‡ñðª-È1$‘´HŽZæ&õ£GýÒc¤æñè8þ±½{9Þñ9Òµ«/´øûPßNrÜY?‰þžç¥t{þ|½¥àóÔyHŒ¹îâuhlÜ.’¡Í€d¤ÍØ.ÃlD‚›E9Á)„=uسrýç—:1bBÌ^Ý ùå†åžmqLdlЈ59ÑOyhÌÎÊìÙÜd«×™ëo){T¬ýngÆd͇­a»¶›: þ`u8|ÑÁ­Û{ñ@Ú6ZærqáöÄÜÞgS:Òm÷œQgyCZ X&(¨½…Õ-%ï6¢P(ÓÑzˆPêÚ†öé… …=p~é„¥f5^_*¶W‹Ý¥î\¦5 È—{8–yÆAt Çr)šD-ÁT‚ÿ}á¶î.“Á6˜ñeäkÛP˜ïPeZvòÎLÀDjÛ žŒWßÏ\§¶È jãr$ä^[%¦²²æü={îù¨¯«Öšœê uç†]õ³¦Ä`!˜7ÓŽ,o®£ƒ·àܯB«ßíM¢ · œÌEjÄ(e,×0&R&ì)BLq—Ð5¼R3Dþé5Îl:‰O¬jOB‡tÞRk(g¸J)ŸMÆ[¢ÐtYL”_½4¥±·åœ+£u‘è%÷tGz$ç½Ö–| u8bÎ峿B«69ÂÎd*]ªVÓ¹DÜ´ä •œÉ¹‚–HÖ©õºí ]G*TšåFŸ-­ÖrÚ¨ÇíÛàsØ’ØòœNK I昮DÆ’€$€Qöþ&4$‡ ½”CËŽK™R3¨á°)ƒ‘1„T")+Ì eH0ÊHÍxâ,Ïs‘ΔÏC!äì¦?óå/×~ãoOÚ}J™'ò›š ÿ~”|œ…ú¯™˜â•(N¡]û>Ì)Ý(…ÇIÍ ÷}-{&ô¹ef›êà"i3õ_c—=,…&JJp¶ÄÝ%ÛM7¹û3(d‚ɬà .ú®ÓïÊÆqØ®ÅYê˜d‡ nn%vá!C 켜!QV*N££;Z sŠ 0ÆÈxs3¤ÎCm€sˆ ,&½¶‘ìŒaÅ‘ –‚%#Z¦ì•´¼ÿlw÷Ùî¿Ëú|é´Ï—¥?óôÓµŸý¬ÆÐw:jq¨£à0|×Z"/Œ›^ZBøÄ0èÝ6ýT{&À™¬ÎÔˆ|”®œ­¦9gÀÀ÷1êç#“V6ËÃSOQ'‰7à{¢ˆ¤pÕ²€®~e qíó8‰ØM‘7ΜAû…ð¾çᘎÀ¾â\glï 6m0ô¾"ý·HÄ7R¡Ýlî KÃ6ß~}[Ûõíô½ßýîÐ÷¾{%_×Á÷ç"âD™¸™×; ÒˆÏB-ñYØô{±˜R¤ £Dy†˜Cx¶%dèR$NY£é¤DRÁ  †%™ ˆ|6QN–[¢~/¼½+–ZNQ!RŠr¸ÄU$¹ŒÞ8ÕL:º[«V[UÎ,W'ù@Þ¯ã¼Ig8tgƯw™5pd^3yäZ7g³9™­õÉA748TŒÖ1ùbFcÄc0:À ùÒj³['Ì[ÿÇìÑrv½ÁŠÞÇ98wv²8wp×ò9²Ö 9ט!¬añÿ6f:õòx­a¡^X+0]„³DÔ^%‚×EH»Âë bcjE„½\zóðÙ-ú@k̳))½\nÔ:ŒÔ)òÛ‹W®÷¶§]rUA§`ƒ>xõÛàÔzÎKfiî‹ ³RÂ6šœ+r¸kψYÜœÅý|ýõë¨ïî†Sß'_¡„{R¸ž×©’%òpSr ”JP±¦P”ô:]¨6–"™¸4…¡…*¬k ò\<Œ~Á+ˆf&%°j††/Qß]еͩ#÷³òý½8ngžÑBKwÜr¶¥Å[¶Â7–sÄm8ƒD¯b4RGp¨ßi­?Ñô²ð³½çªne~ŒáÀIù1(D“ô»Í! ¶O¡ir’B¸¤—òc uY+?fíµðÌ/5åÇ ë8™BGk¬ Gn e^óõMŒBWV)ÜÅáhYÊùu’Ü‚õƒ®g1ôøhZÔAb —.àg^³š¡ög]ªg0ââ\"‚o ðù¹N«üê`—Ö øº™ý¬ºþO×ÿ­«*àÕºU‹h碈 ^~Ú2F(ݼÄ>A7õ‰îΚh,uKrs!Ùã?ÊûLjÜÜ Æ;v³ @£Ïîux­“ÍlS)#0¢Ðþ*`‚Šß‚lµB€S(ýEÊYAé0'UW Õum¼¬.ëdŽ¡ä)1%³Ô7Ü}7Ø4ÐÛ;€ýÀ#äë`‹„ðÁW»[x¿2ä,ò’¹äÚ>6ÁN£|iü@^= ê8óRëe-0lr$ ¢êXxy»ãlœ U”‰f8Ü«µ×´Ç8-`Š‘(×ÀÏø…±Dbl?%}øÔ¦‰‰Mà«›ÆÇ7±Å—|ªööí}á ŸBàåzÛä,£äìÄÄ,YÌ¢y³…üÀwàû‚–§nyÒN’²˜ .%?—Ã@R¡˜9©ºPÛ'ÞÁö(«O1©1!'ÐóÏ7NZÑ´Æ«RAhk‡SA¿b) ÐZGNˆ†³‡]M9Yiº‘ÔøŒj†Ž€% •»Ë!Gq,gŠÌN­S!72Ê\"V êÝŒN­6ÙCÎx8TæË![v(SÜèbH­R¥t[Í6ƒÑWˆ…º’v†qøC25×breÚ½|V®ÔèÔA«Ån4úË)okÜŽó¯þ Ô›î`Û‰â¤rÀ=KŽPÍzþ |l[™¹áEýaÄQg\µ• Þ«&F¥7‰þœ#-H:£6è7%ú³ùtã+uË9eʹL ¨£o¦ŠNÎdcðVãäçÁ§˜«D–xGE[(¹‰™½VÁå/'‰y@<2²M´D ÅH½Bz¿@r 58!pFê²S¤v|7a×h"¼wÜ ùžÒÉ0¦)‡O¨1å%°ønP†ÚˆÄ¯})ËhiŒ@UaPUûòJízcåo½Õj2}i@Å¡‘±Z¨kHj´5š¼öóCµúYíº+H!R¨òp´¼AÁnÙ eÕe(3Dj1JPPЪVHw¤™¨é"` Ó•k±÷N1øÊqŨýŒ”ð•'4ÚÔx›Ë‰\åΠ+ïçàÌ¡¹¹¡›7²ü±ºdÄ•–H»5|ålg'Ê ó׊Åp—m_‰1û_yå•?°D¼a´;7¾Rßç´Ž“Y¡æó(‰lÛ±…W§aÉ0'’‰b —Á5Ñ4®Q@2Y*¹ròq"M~¾ù@7"•ˆE\½Jß(É9Û'!µ¯¦=/´çQ1 µ·¼¥/Ò_ z,†/ƒ½)”u9³!“Þ4¾ÀYú}¡ê@¤w uÔ×>•ëÝgc;9óÊʸ|¹ ÁÌù\™•ºbæ:YëþÞÜT»ïóRGÈQ¼òbN-’¸×(ÒXDVTaa¤}7…@×dó×yZ×òë[ݧ ÔwZ ZÿAz¢ÕëmH£Ï…VÔÁ~ì…25O ‹Ôê‚$mbT_áZŒ;!m¢É‰ò¼?Ö[MŽ,^‰Ý‚ÙÑ-+ÈÑÁ!kÔ‘çCšøÐŽòì9\¾Ìvhh²òLÀ– ˜:³ZË1 =É씳ÊPhÝé ‰Ã‡ÃsG›Ï¨Í§3ܽ­öÒúÖPÜÊR Ê­‡¦Å:1~†ŠÃ'€È<«Â%®Â«Ñ,à6ô`JRl‚ØY{&éç(wBÐ7ï&?GÞ¯€sSX¢Ùó.1Š8AÎI€ô‚‡sJòp:‘ˉeèZ£ÝÒQ¨ešDÚàmˆ´(üg’Q3©7:´F¹Œ£”¶˜mõ7äò_ÿœÕýú·Ô¸Ïõ­¼J‰vwQnW⎢ÛPÿ‡Û2CYI„e&2ÄÎÏÛÙ-ª”«˜´‘†€æœLšˆôÕxKM—5ÀD|‰–H&šáÌŽHȈٶa—ÕOÍäBAé©°Ú#¸d¦{ãZ7’tðíæŸð#ÝlÊ’,õ[¹üÿ7ÁeªQÄs¦€ž(ÓÒõç<ùˆY0è )f™Ä'®¿Ü’qÙµfÑ7A¾J²ð™#Ľ¼R •+Ô…%Î\¬ëÌ„®LÑûP¢5M.ËjÈ/´C o×®lƒòÑyS,îA¶I7™{œ¾e¦àýx¾ŽPÁ¼/œäÕ¨ºI­b)‡°a´} Ávo%¤›7ÀÅÃ>…Ñ€9IÉ/¸@Þ,€Ò©Õ0³¬Awø€ÖžÍ[²ã•OœI&ãÑ ýy¹â.¥ê1›Æ´.ë«Ä¬àõ7ïz|q¡Žø³à!Ö@(†ñA޲p1³„*ŽhqZ&#Øô©?ÇX¸±s¬!¨TVr7Ü?wPŸ¥8x ´ÂÏD€ºøg”£xœ†o%À{’Fzú!4;Õ*9$•2 $•¬åÂ>D= Î3OpûyN b.°z€¤ÉrŒU¯IrÈi]¹kÅÝÂ|Bî3ïÝäQ¸ò•Ü?€žéVºŒ2ÃkuJuèg\‡!Ôžu$FþFžz@LŠ Áª= V*ÀÅ,¹iÍößK©ØPÙë-ÅR%úÍxyƒ#Óõe³%¡æ¥ /¿¯Xž$/Õ>Qp·i*…ê÷…/´$‰ÄR¾ü‹å÷M–fÙ¾X´7ãØPpXN‘)¶ Êë"Ÿ³Áu÷- gPEP/ª²‚ Ši\™¹ Æ,"‚ š'ËqÐp1™²¦û“7å¦nOB¨H4çKeÔu£ÌDËhwN÷ï *ó¥Û|øUÑy:6aßò)röËK”ù‚öÂÄ,#´#Û:HàS±D‡@x¨ Š£ ’r%i“%ýÿ±öðqT×ús§lo³½JÛ«v¥ÕJZu­º,[–U¬ê"w¹apo¸àBlcظ¦:Í‚@B $!„„›á%¡d8ŠhÉs(ÒÖdCüoPÇ^þ¸Øx8]‡ \k:ð ÷‚|î§×Óî“âïcÂ%[‹³Ç¹@ÃRHŸö‹É»¸x*‡5à­WM夨MærpÄk>™Ìä³9Ýð‹Xãh.(›ÕÝNÞ½ˆtå–ˆS«R‹Uoi}®¶ÍÓ ˆ×Ë ~ED[Jtèå˜k…_Yjä»rèÍm&„qn´c7?> ›ÉùQ›E¥  À¤Mÿj²Px˜ˆ G(ˆHšx7f@œÉ2…vi¹»Ð©¡å…íÅ•J3L‹I±t˜Q(W¾l ŒÖ¸ânµ^¯QéRòe£¯Ð®²ØÝ¦°J){SÂÊXÝÉ”ÜY4»á8q¿áÃܰÃ`7Yt¼Fà˜5Ì™^ì|Äy ߢÀg¨¿›¹ñ‹ô߉¬óĘ']y(`Ü+ÏOœÕˆØ‡Fàét¨úb#5ú=ŸÒ“;¿ØHîœr>ÄBnªÁ¥Tòê,¾Ì ­iô äz PuÛ”qðÏï§@gzèù‹ÑÐÆþzý÷/6âÞ_ĵ$j ΄Æ0¼!I¬pÐÙ€lâO¦PÃf8Žÿ‹ê?·)ë\1á‚F-Žçgue‚ù‚øóYº·û ÕŽ¾‰û1uð– Y¨vc ÂEµqé1b?ªt!i½ÀH€C‹Óy$€V3C ß4ö(ùÊú+ÐL‰FãáÜ6{Ü$ðûÜaOØbÒ±*…L\¤K a×ÄÓ,U%ºíè{ˆÅŸ%xŒð05Ä S ^:â0N“|ÚŠÊ2êlè« N?6@Eìíb ’KH8„e¥³ñÀµ‡¹ËÜ„ˆ?jãƒÄ—ÔŸræç›á"Í/Î/.* Ý.SÌÓ²J¹XŒ¤Qa`}ÅBá¿Éä9hNi°“†iSE/Ì+wÐíÓàdl‰‹›Š2M­×â9÷bÅâm•åK«6¯è°Xæ­Ü\µ´¬rÛPXÿPüB÷&oéþ7Ï{s©wS÷ Ü«¿áõåQxÿŒ›0"~Pô(’ÆùžÔ fúõ`ú=¾U‘0²^¿+Óª‡Ob€…Z±ÔÆñ×½º£@¯t¥=ßÍ}4ʸOre¦h}Ôè³y ’ñ×à QOhÒý >»‘£¡< Ó§¦S„d%jÎÀ;\´qÅæ(k>ðÌ5¯½®yþí¿eÿWžÞøâ íí7¼¸±çjIp¤§—ÿŠåÓÇh‚ÃUbAb¥1Àbbf—Î…– ¢ÑD¶… ¡ª$•3U[¬VåûäÛÜw¶Z- ²…|Ûãÿ׸ßã@×=E+ƒ×õ¡ü+{¿¡ÀÁ*¨]¦$˜ÕPãé4µZ Ï,†<‰“ìj€|Dª‰Õ2Ф1šo¡(nD•c*îëýøí&“4Qrƒ Z-`D§øâê¦b;Wª5™´à[±i,ȸ:[*Áb ˆB4?T(Q©0‹†~q7•L[@½ ”€_‡‰Ð|8yDYÍ“H1Ü'AI’§SÁÕð!àvÖQ8ßѲêb§Ý§.¯Ø²ëÐìc¿<ѶvÓÖ()rêËýäÌÊçž™Mj ñ?S䮘Î,+ÒSo¨:l|ðo§æ» ±æ¨Ã;N}HeSåTæüf7n«änÃÏÔË=G×0[áN gÄhD γȴÓÔ €È û½D(¢ÑÊÏ/O>àË3éIXM&Ó~_òÃáÇ®mj¿þ‰å}Ã%š°ç€Ö#õ4Wïúïû—=ö·uÝ…FªEÍJI÷¼ôMµ»ŸÞ¼õÑÝc²¦ÑS¿µ¬(g®_çu9Õ9W]øÍM7ýéÁ…­]/WkÅ‘ú¨ë߉O©?0Ûþ@ÆÐÏàÕ¤* Q®­ÀªžfËF]8W¢Õ›nÙ8I“‹FËcå¦`0¨çb~ŠxÝ0õ#žàýd² ÄVœ3mC?á¡=ÚÿP‚!öî‡i2aÚI˜ög%>#¢”ìɬ£SÑËV„Ò4.SŽG+Á€ÇÍj§q¬ˆáÆÅçþfJâ&©ñL”œÎ5ªþB÷ gÔàÏ+¿óx¤"dS8l´4§¾¾Övõ“»ë«v¿p=QX”Cíph†¹óØŽ³÷‰$"£#%W˘†ýÏm_ÿìáÙ´D!áuh;”]<-;$JÕC-Že'ü@eÉŽOÆìçó0¿•nc“²›šó™}xÖ»´]1I´Ÿ˜éx$;,¹@ЋyX¨ì¾F“%a%I_2ËrŠ öë^Ø]Uwí“WÛjëësdŒÅ¡°…*òn¸SÎiÀŸUg޹ ¥ÉIS/Béг?»~ûsû™Zžr¡ ï;»ãØP¦ZœËæ‰OèuÌf4—Íh.¿ ‡Z–*AŽñÇH¥B¹ƒ' ä㎘é'©¯ÕiƒÆ'ðÈbÌCÆyDQ»•XÔå å˜×-”©>íÌ€óyœ9?ì55÷Ò‹¡C]5Õ¤@„?Aš˜ÞÝ0Û® ¥¼É‚â²å³ÒÚ[‰qüÀÕÂÇ%E¥+àÇu ö¤‰ÜExð³GÖV®R) åÃ>¼;Ød¿süp]…aƒRe¨þÚG÷„šÍä2>nQ5ñ‰¨‡úá$êP‡. `$ N$¸:cXDa^2)¦ËÆíA°Ë>Š *mW«®ª¢0ôÃÓsMÞ€G®ÃÕN‘,‡{𮬇&(«!tMF“Å¢»ã‚3T²äpûÊÃ~ÿW–µ^RvþÌã¢lÀª ¹Z«ƒJƒ]56®²•¡ªVW¨*dÔºL®ÎG7={]Ó¢Ù³5]÷ì¦ÁG:[î¡¿—5øœ7¬‹.èôxºæ­»¡Ãék*ÿ;ÿüÛ¹mÔ«Ôûð]¦ˆ_§d@,É”˜r„D’n)ã7ª¬g2ål=Ê☬Ý ^¢µÿ椌CÙÇŽÒ "EäËOD®·Ú_tj8Y¾îŽ¥ýç¶4”ŸZÔûÕ"sé`CxQAE{>„bã/Æv¤öñÕ-H&úëüܶâ€,²ü¾íàùewo¬ªÛ3ºuíÅÃs«4íkjmRä(3ùÈÓ5Ï,<Ü ̾ª ñAs©ŸÁ÷¡‡¨%–ŠLÖ¶£¢ý©zFYÏœû…ZrÝ´§b3µäƒìË}ç‘¢‘‘)& ²ÇY]ÊU“}SF‡öÆî"ýOfÒ' HŸüi­`ÈpcðÉ\ ž  ,C}º:Þ{¥Ðé¸-‹Ú¢y‘0êš ¬Àв‚´r~ÝeÔ÷‚±O»§¨'´2 ÀtfÜ;áîŒ46O”‚æ^º9¥ÐŠz’žlÚ±È~Ê>E $‚©BõpOLbslަ+B*h§É6TÛ³¶Ò8QßWwÄ8P»J‚ŸÄ×?º»fo ÚøÔEKŽö$õ¡TžÉA»tüÛõ­ýõj¯ÍÙð9Q*„Ý©¥ ±Õ¥u7œöf ÜNSØVÈ #0Š&Ãd“h^—Õ9MzÒúV ëé¿aYjd…L¥;[yš×Rï5øÆ¦UíëTqßõÊŽ}oúÆæ±ÛPß©t¿+Ô‡Šß’ÜÓÔzÜ·d*–Ç¢ì°<¹}üàØf³«^!Sܯ¶ÙlJ "äËádãvo漆ó®^׋: XfÀò|/‰ô]Ll<¯›‚ç=SἎ¬‘±b‰VþšÆbyôN±˜û—Úi¶¸ÔÜç"ÉZ,ê“'”F¹Ü¨9zÅXŒ¼Å7ânÒ™Í:°!¸À;æeì =÷~~gõßDqËäGfÆò&a¸!ËëfÆòª™À| ÀhM¬ˆ%®m]þ\—ݯ)B`¾õè[7ÏvU÷—­Ôš¤ŒIzzK¸_´’r…Ë4þgpöO©1+MhIÓ¼p Äó||óÂG¿vo—ÆêÒ„6žxr ëbýíþW׫ÍNîd£q/R´š( *žR)IÞ7âìiÜ_3»Ñ£†È¤•ðõF#>+—RGtWFø%Åiõ çæÑÞ½s£Ú÷ë–;ŠB½·m®¯ÜòõÍsÛÛÔZ)¹{kÏÎviZ´}^Òn,(©ñ6í®,Éí¼N‡ÒÖxõƒ{‡îÙ\ëŽÈ5:ñàËäÿãî ýÌ hgÕí©9Iˆ‚P©)€»ê!!=ÔÑÌãb2I(ö°³ E‰Š²DMQ!dÝÒé‰)Éj_ÜÔƒẄ• ˆ4‚Ms|€9µÖܼ²¤jÓ}«V)Î)nÍS˜uJ»ß(Í-…ß[tJ‡Ï$æCe‰í-·ž;NÕ·ôÔí~üêm/›ÓTó¯ú¾„”²ªB r•˜<ø³>ýsq€{¸­™‹¤/}û‰øõYÃÝM_„Ø-E\àݲ6ÄÐN IA;˜ P0²Ð SçdÊ6óoEüo¿®3Ãc6Ù¹>íL[h!AD0¢á™MWü[0µDueEYQa4‚ènb jëòåÑ”†’Ó“ähG21XïcŠËíø W×4Ù¾ WQ~¢xQ£Ÿ))3åéÈZô±Ü_æ>c¯^ÖìoÍS3&¹"ßU—ÐÝþÎÙœ"ýøXºü®÷oËMêÉe üÇ#k* #•WuÇYm“Q©êƒxÌ'>ÈrøÈ"tˬw% !pºDÂX‘©ü*¾én»«Ôà‰=ÙÃ-õøâ¬U+µÇë*Õë4…Ãw¯ßtÛ¢`È<þ<㾑+u¸’»_=qô·¶ËU¶«ZŠÒȉ1îE±Ê¡‚h N\LZIž¾‘4å©€‚‘ËC>BU-C´]¤’BK@*&®™LÞEÈå¬YðøpF!þOŽïOy++QÙP MàêªD'ò!oÈzxðtnešˆWnö™@®:`½{Ętæ§œã¯À¸Ææe¹ß ÂGFGÉßYüà5ƒcüw®°BQçºù1:¬à}}üäÙFãÝ móYð± y€Ä ±åjÄÛ’­G1‰3wñZ”t ^@˜ J‰sÐ2‡@õI ÕÏ*€Û:¡?ú ¥ægâé™—‹Cû(²ŠÛ§ÀÿH­É«ùH°ÞÊýJW˜“Ÿ²ë}…9Іû_¼¸x?ùm˯šìèÙäŠÒÂñ—KÀovòŸ¡ç ØùgCÜNûà³ù‰âT!T­øKPÍ yÚt)f}Ä/„nð£[_Ðçb5(¥œoY4õ•€ìwTalyë­Ëßè¸0ù:žxb”^pôÏS_ÉøËé—#¯Žw Ž×Iä¥BjdÐ×eZ8ðeo¼dˆÆ‰4¢ÎÅ~é ]zðùÕî¸ ¨TA8¾ç ÅÎX*ÔÜgõ±Üß ùÁ¹o"¹YýàGüðäÊìáÑ7òòDsûŸÍmÑe(¢¥|+ê%ˆ'E‰ =Q7¯áZBËwôb¥Æˆk²A+ÌjêÖñûáLíý:œ¼d÷ø#p£ä,¸Ù Ë 78#˜ëÒûÔ¿$|hw ;Ð+˦쓽x\Ìu\ý(§“›é“—ÖMÞ†¦ãÜ¢ÁëGá!3B‹íd¦s*|ÈÊÀD 5¾”ÜÃ,ª;ÌCe ¤„¾ÒñR0yxÊŒ¼öˆ3À ÏìQ•]¾Gà¿Ù»Df“`rê7ÌË‹unjlòŒŽ?5J}£dKÓà½;[ê·?°|þÉP¤N§‰OÙ$àÃ?”Þ'Nµ[lIa›€ë¡nâS&ÈäÃ]¢‡ø€w°&ˆä[+¢#¸~ׯs%²¨æmê/¦&¦êú3~!eDÒÁ44©GS&]!gÀÞ¬ æ:§hM‰†Ó'‰¹—:\ñP9œÌI¨j¤«Ã =þR\5âãMYÑ÷»8í¦O‡ëJ&i9y",>•Âp—˜ Œ­)Nî¬ÜvãÎWo™7ï̯o°D#1«ŒÊÕ+¬®°cþ+ËJWÞØã,³¥:O™½®Ù½hž«ÊTPe0:éñßjsÖº:¥£Àm‹ºt¬Zë©_úø?o½õ³Ç—R¹¤Ña¦éÐ ;v~û`“D2ç÷½þfÝÆÛûOý¥[)UçØI[ëöÅ­¶šï^›\Ýw5­ãç¸Ú?†s\…ô ²ó3ÁÒ4ÈíâA.&%Pá>|BØ4½ÿ É?æö_tY×½ÌAzfŒæ)}”Ï‹EÁùà=ì¨~I'Ø· ‘$J«!©Ào é謰ÃõªÍs£æ§Åà²t+\»ƒô+8à ßm²Ëßg-2‰…}Oê0ç†ÿô§‹n'oÖr^GÌhÊ·ƒ_©m㎎£Ü`ùQüÜ@}u“›¥ü¨©[áöwpºwSxiòžŸ ;èò°"CD ²·\è¥x€@бäzPú¨±éî猘ûÄÈ>nà>­×J€K,2‰NõùÈøû¼Œ Ò¬–q@i Ï“ûTrŽ`•à]¥ÊÍm’±N…rCè  „$E(ý‹xD‘Mh’¨A!³Ô)ÆT÷ ul ÷ã>9F U/¼¬ñ»†r9 ïiE÷4K1QÊŠ§)bX–Œt€â1t¬¥Bù2BPkMÔã _ê7Á¹ñ¡'Ïí_˜­Üû½££*"‹m¹d¸ÿÆUàQ®k`CTÄÆÇF8g>„ã°‹RJT÷DuÔp](+ËAh*üa^"„àÚT ÓÈ–²§6ó~*k–õX«ã¸ ©ˆ²áüÂ93p—„sK×üÆÑ½Ç®sÔ¸GG5¹×-½°­žqs]6KŸû“H##‡¿x‡,–jDÜçŸÊ,=g·¢z8îÏá¸u¨Ï‹H½ñF6S8HíT(5!Û­õöEγ@Šý«£KŸ¼mw_ÁÅâ/…ù‡v,¢•\'xl`Cž>0~^Î9ñ7榘(ƒyNjV><׎yiÒ¸ ‰PiÜqdØ! ï®Ï$<膊r‚HÕ”7T4À‹”zQ§Û­Ô•ˆ9cd k1aî”p…˘¥4ªžþñÞMß¹qžÙ9vŒªÙx÷Šù»ú+Í*“¤¤`Óþ£Mûu~-)]ÑS³‹ES%ÙÛ{leµœügbá!ŠÝúƒ[ºæžúå±ÿžöÕ†â!…´±dNÜØ÷Ðçõ‰Þ:_cHËXdŠˆHÿ–_?SìOIn¿2}¼ÏÇ `ô\¸¿/i:Ýzn×ãýû¹£òPYKÏòÒåU¹©ŠâˆSBþòFä¡OŽs7pŸß¿ÈãÝhï¹ 0`@Ýq‰¸'è«áØs‰…ü;Ë…°ª_œÉkªOWÿâð8„[4Ò9—™v³¨0C†v ƒËÂ辩0{q&‘9}uáÒ›õô!ƒN¯.•t•9ÆB¾utåð½Ê¼˜S·¶ |:‰¤± Êþq8~5Âh@Df@¼Ð‰IQ²ZGÙ€é¶í÷îÞ26ŠE–ØážÁ½‚÷Œi{ Ägöve†‡'{ï‚<Þ»—í]ž…ûå‘콋ƒ[×+àþ¶.@<ʽ(¶`»2™*r ÕRG‹’"aØ(áY:ø—ÿõþ<—±S8ùÒ.C€ÛNf'FegO‰-'ž¸æŠä¬¦‰Ïi<qq%«íãØ¦ä{#OCÅr¡¬F ‡©Êqî^ˆxžDnÇK_ðP‡á{Ëʹ'°OÓMœÀîý§Í@"E±UøU†c«^ž2  ¶ØpÎ ‡4 û³üŠhË€TŠÙX ÏÑÈOH !•]ù¯,·Ü£bŒ2yŽ1œ‹Ý«7r Žq¡wÊÕù4lƒA¡êjá1÷ÇP Âg/ vò)÷À[$’Œ2?ñYFè‡ðê¡ü»ìËäaýìMg ]v\:ˆµ³ Bx"9‚ZâõËžª | ¢ßʾn.î©rÕø”ŒQ.÷Z ý×ý{xÿÞseÃóòÓ¨¸"ÌGœ83N@¹¸ˆp*MYR”m1 m¹p±lÈ%5F„Qf¬"”-Ÿ‚à*›¸ï\Ô³b€ØŽ>‘hUqÏ–»êŸÒ›ÄÜ'RnBb€£ŸÅ}Fþެ†cbDJÔøKGÆ/ÀkuxÀƒGxŒU ÇyÛŠ6"˜ò!³Ž&p“2~”d7ƒQ«Õ6µÕçb½"ΚÀÞ[H ›1œkkv=µõ£Ü¾]·Wþ õš9ßìsÀÙÁÛ6Tƒ?|ñNùÚÓO’Áðì•UÕ+šýðþ­Ÿ1Äó§–8€wñ§H'áÌ…WÚÚ=%&ÅÜbŠó#”@ƒSèçg qÆ%‰‘`ÊQ…ËðA“ýÿ (t¦)Œ—%ãµ…µþPÄÃz¥†ÉG›¬‹ÇqœIHãfŒ~2OÞÚtø{û~7Ú=· 6¨‰D.Ç"Z&aòæ_;oÍq§}Þ²­µËîÞX Þ­]Õâÿ]÷þþh¸í*pË¢;®R³ØŠfä”Å}2›ÓU[YbèÜÛëïõ×DÍën}ÂUÞYæ†g¯®®ZÞÀ:§Ú O0ùD=ŠlUFLñY–"%€£Dz `tG·ŽÃþ(ÏJÁ@6x=A—?²!O´À÷Cšøä—ÌC ®qLfBL6´¢Î¯Üy1Xáœ}!#Ó÷ß9gï`á­§ŸOW®h f¯k˜{Õì8kUGç¾v`íÃÛSÔ»KO…ÃÏ(“o¢× 唸äV—«¶ªÄpáñàüC κÕ^ÔÎ*väúríóëš ûwãµõø'ðÙãDsªÁ÷c¯À’ÙÍP¥÷`ön&óÎÊ®Œ³2NÄ=$vV ì‘3= vQŽ´£€<ºiçù3‰î WnÅüdj &¬±)£õO]½ðäêÒþîü9%dŽƒž“gÿ¬ågÂúÊÎÕ5ïMÆJ¹WDoÂ5ƒƒ¸•‚¨¼þ˲i˜þûŽéH6­%QÑt‘ãñ£M ù^ èJ;0ëÏ YæM@À-ŽûëøSšâÙ‹®Þß’HÍ-XØÝZU0îK븫àˆË&!­V;Ó><ñ©èŒ“†xUïÎLCÁ$bM?‘äaíŒa?9YÓ°Öòâùšfòfj†vu[2J½?éékeČӱP¦yøž–m=ùã 5ÿ> ¢‰¦ˆN˜v6gݵá OÚ·¶ƒÀTqëħô{ƒ ºOŽ)`3 q–N©àü·€(pó‚>ìS›Ö{Z£{ô ˜É~oö /ïºéãûïûð–Ö=ë EÐd*™½¬aÅùMÕu;]ûð ÜïY#¹cãž—´ Gá©>¾Ô©öwêe*¥Ïen9üÂÖÝß9ÜüÌzõ*~ü®‰ÏèïÁwâÅ]¢³r2>A3Ä@¡¡{ ¯!ò¥ÝÓ¥LMJ¹˜~‚3˜"UþepÕC3Pá=õðœ=ƒ N‡†$šyOj5$âmíTß›ÖAìxïP½*ykª¹‚œR"aÔ·î Ø™>šúììúZŸ/è z<ùr!>y>„ø&Í2A» Â$læºAÒ! ߌî]ŽLøµžLfžñ”,‚0)¢D3ܧ·8mh¦`›~L.!"ÐþÔ!ˆ#3!3f&ˆs¹IJ?b.ž_å®ñ©iˆl<ÖBßO¹»äÒ¦î%%Ë*r*Ëá ZÎÏO3W°TFØÁÐCÌ*8¢fbÿÅ9YZ§ó(¤µ48«L†ÛžÔ§‹Zq×›Ÿw‘9þJGö§Ñ”¹ô RHÌ´¬®«_=+À'gêñuº<£Î©"Ë6UûºÆ>ö$óȓڈQ—«„oD;èokƒ©˜S_07ép$çè±TP›ÎÕxˆzÿßäj¨ÿ£\ >dyÅ\á·ßþÙªã=¾‚×÷q×=ùäýoÍMX…íåÔû;÷ÞïXSVwõ¼(ù?{wŽÊ=bÕøÝUQ[&žJ¾ó¡?éÛïϰ|Nú÷•„’·801ogðýûÈjnãOŒœ!KÁm‡¸örÍàÙ½B|ñnj>¼~”Øž’QpÆ…ˆA>¬8ÎÕ¸§ *öw9ö¼w\)YH¾NOD‹(\6ãáé(‘%ÝY1JD ÷=(J Wˆ·eCä $ÿȵEVÕÌ»v~´téá¶ÆÊC§…–Ct6âpj/°G½¹ÊêCÜ}n_ņ{V^óÔžÍmSédú¶¶äÊ4zw>2¶¾àF˜óð‡j¢:U‘44å@ õÒƒ2 nÛ$†ºnª}ZUQ^VZ‚‚Ö^—ÇìV˜ù¼$êòÊ?5‹BÇgQP‰) IºW6}×”ïŠT˜Nßg7­+Ê/dïµw>Üå¯ÒßÁæï|ÏT‘çÎ7¾{ÔVް _óà ->ê^ƒ•;æ‰)”Å!î¯Ü«ÌËq€x+7 ŒúüÜO9àÖp‘Ró€-6÷ªÓ¾Æ Ô" n„úÎM-LåKp¥#!†ë@Lòl§L&#«[ÈÈbùçE»ÿ—?/ïŒ$ª¢–£0x¢–;²×' ÚB‹GF:ýÅzê¼Þξ(œ|nȇƒ§~Æ ¸x^°mÔýpœ¢4Uì‚æ¤P"=î‹ð Tïý¨=4Ëûqç»z:+@m ^6Ú4¹ÁdDqÂû¸ê —êGwäW¨6)£ömí##íÛÂê>ƒ…;æ‹ÊÑP×ÚsA<×Ã\âøþþ7Š~½*áXß‚cõ婤[O2) $i mhP¨MžVpóW2 <§”,šNP¥˜3ÙH„ºËF.L¨âÇGz¨Ë_¢^ eü!’q¥å£P¾…»†F׬ÙûM—gÉx­ ]òýüxEtoî—ä?¨'ó¼Só®48—¼:ÏW¤}íÈoLùî¼ óû;cEìùPû‚î÷ÔèåN™ÓÁ¿÷9x\Gà~×ÇÅç=¾MŽVg97…ÝšÏ{À¯›×(YN#‰R³›Ö‹kÏSoÀýO6ð}®f8„¸¿!(x)­k…6xý˜ñqŃŒ¦éZ> ˜N^°Ìçöp7ðW>4ö$ÌÂÝÍ<€õí´Ü‡øL¹êËr,_šû žžû`ùÿ•û@Gòf'ssáZÒdázF(YxEMûîùyÉ¥Û›*ÖêäîwÒjÖ£çPmcOB8]Óu1iZ ®ï£Þ#ꈹD]ªÆñfSyx2»á²Ó´„„¶ÙÚ`(è6½YyéÂ¥Ibªtۇɴ¡àN\Ì•„Ó Œ8«qVÑ}ÖFí–¢’MÕ îÙÞX±ñuΔSFÛLr»ªq¸ÙžwM³ÖÃ6´g­¬®Y3'Úò M¹*‡æÊ«K“Ue¥å´Tj³Zméš@©¼Å¡dbñWæ÷[’ èºg6ÍÙÒÎïÞÔ8ü&•ˆ4:À¹…§ Oöõ.[Œr“ ~fà\Q yšÙ;~}ºP g³yÒ ×”ô=ù ÷››á´¿‡{î‰ã¥üœ§Pu¿Ž»H…×Ö£z…òûAßY÷žç¹YJñ¡==S€ôpçnÖ[Eo(µb±Nõ¦ÈÆ«äv‚#Ô £j| þOÎR |sé{Ù‘N/G”p¨’Ñô@Ä5€YH…2xWô…{2]Å3ŒàÛßyå•F1wï³Ïîí˜yEPž?Âû‡A Ç!$'¨ë©é*%”NNS67a焺Ìpæ¼'ܱûõÉëÙs“î(à~ý€^'ù-•<,Õ«îÜa^þ&%ØhÒpO«Œà3ðß*wNnƒ"n] ‚zï!¬÷¦ä&¨ÿ³ÜÏ´ÜùðïÁ #çáàÆYîCêëÇËÑ¢&Âl^. P.¯áža”×(Å•öhÍ©/ÏI0±ä–Ÿ’’²Ã:þûá&ÿÌÖ×ÎôŽÄ—Ü44rHгw8Õ‰}@=÷óÆ§Ò ¾8p G}˜,¨æÄdÐÒ$#ÄuP?×áìpjí@o0°z!°$ø|ì)D& AùHKݶ–mÝù#³fm«jçô²Wž# ëç¡ÛwwI€‰ûXª…C@Zp ;àpÞ»šÄÙéx‘‘<†0™„¼÷j!`$¤Ùþ³sƒ#E+N/Cj~©KS´o÷-lìÈUš¹lc@ý¶ê·$‘":Sí!!vN~iØÜ”KPV ˆÊŠÒTY*Qèv9Òas¡yà•Ó „Î¾é¹ EÌùžŸäïûN­¯ÖšÓ>ò> ]}+×/{pw Uº¤5Ìjk*u[›bÖæ;–¢ŽŸZwÍBê½™š|Öî{n'`£uþYT„ Œ:+dÏÍk*÷e÷üTÁydQE4¡<‚|Anû¿Í#˜K7ÔT¢®¶º©¦©¬4på ©UWˆ˜Ÿª1êÿ [Ó¼Þ<¨@»Ž­(ÿ߯Yè51Ü_àÌoQÎÙóÈú' ”Kp7µÎÃ|âZ 3½a³’„ÉÃî4ÈÏš†—Ÿ‚Aw"ˆåù½9Ž©²3 ²@Þ4P@*Ã-EGqkìç##óü Ý“î¿Ïºõ7Ÿ+Y¼v}“`¡¹Þ‰ôìÛÒœ“ö<ö³úT¢ïwÝȵ“ÀLü ΔÎ)ðgç¨ÿ“œõÿ!§€D ûo(mÒEí<·ó‚£' WÌêèÌ+pT–Er¥dÃ!äg f}ƒûä[[ëL¶«Lu[¿ 4ßòZÃ= w`¾Í§¥˜³Å†¾’ØÏʼn!Bꀀf§fXxÞÔiYÙ‡¦Ó p{QdL£Žˆ(× àã»LL­l˜šm@=W°ðúÞ@½Ÿ1 O±or…ÿ¢â¶á[„¥r+|z¹±ly¸? ï‡ï%ŸOý¥˜¼#ƒÉÕ„šÍä"§D™‘\÷È¡ «ÎrÐ÷`@™ÇáýRp_á})š «àŒ†º¤ÛpðÅÓiÄ·kò°&“HAË´]‰²Èž«¿sS×HÞÀᑸ¬Sö¶¤0ƒçÆbÜpÙ¾‰9޹fõ&DU¬vC!o% &Ì0¿A"óˆjtðQ –Ís™’hn¹®ÈÃ’n…B_Ü)b¸Ç$VVÏZ$Ü}"ñ7j=J¯0H%©<Úãw»@²68ÆWkµj²;§É>¾‹zScâD†³¥@¾0iÆF8”Ð:ñ)5u­[öëVäÓ=ôŸä˜.Ï7ˆ„ùÁüËó TÜרð½Àë«Ðõ¯ŸLÙ¡dàõi#¢qz ÊÎÈîž(ÍZ¨† Ç4°ÔY›Ï1 >äÇ‹r ÈŽÈ'wyÓ S½8‹ Ãa7Hà̵»n‹Ù`3Ú2 'õR«À÷°û:ù(§á="Í«ž)WA(Bt¯Ùš|”ÛyN”Gyø4k ¡ì”{  ºøìÈ…Í bö+ä"¤<¨åÞGðüe±êBçÜûÿ}ÒRÚŸòÔB)˜eò€5˜¯7K~È(ž†pþ©ßi0  ÎE¹ÃS1ýÕ:-ÂôH6Sá¹ç0ÓRé%â]?؇raôž ¦Ø|sS‡ìiüÅçSL î׿Ò³å»d8eRp«÷r YÇó¸ßÛħ"9s#QHÔM©ú ¦Ë;án¡£¤—až´],N+QºÛ %ª*õEõº 7ñbbóɆÓcõSìq6ûò²B㯒3å¥Â((‰Ü¹Ki™˜‰ ˜¿þ´»ä;;‡/lªªÙúȺå_MŽpyåÃíTÉž¹·-Ú¶Ô&[£zGI{"·2”ÛÝž†T™±çð‚ž¾Ú=Ïí\÷ÜWæV•’Î/>ñÔôƒÝ5E/=µø+½!4Û&þÊtBÓBáC0î»^Ô¡Ï\3&Ü‹H‘%¼¡œvr²Ë¢);|‡|3‰Zv®Hâg>Ñí]Á@$êÆt;“í†éÉYaL\M<5PîÏmålºeY¡.Òœøá‚ÎD#©Ë9$gcýæÏÞ¿¸ØQ½¤qÛ¶ÊEuGE_eM•_©deAj}Q*Ò² î¨*+Ô$¾Õ;ËQ2I5¥Úx0Šdj»º¹ `·ÅNG"`4ÙŒR Þðñ>}”eQ›ªž”\vCËi˜¦€=~” å˜.Ô»(£Š'[Kf9*>»®ç@L¬+è~Z¹Ï­Ý¼fÍÊkÌ…­ñ’9Ån|ºÌÃÕTÄ5Ž]ý'‹ÔßÒ»¥3•yøª‰¿1ïÂu‹92ñt˜?{ð¦QL»ý†Pȇ€3ÄiÓÝ'¦áKê’Î_æhaÄ šðR 7W{Ëö¾8÷Cy~ݼÅù]E®†ÚʸOJ½1¾Íó«WLŸåá®k»HÉå@wLü]$Ï&:ùx©Mx×WO þçNÿí¢þÈdÔƉY&sJIc¤&oãU³T®‰õïïž½wAÑø##¤hä²Õ+Ì3ôT×gaΦ‰P?§Þ%b|ì¥ý_û7M‹ýLj˜ÛÿŸÄþqæξ ~^¾ö¶¡kž¹®¹iÿ3ËWõ6Ù"r¯Q(k/nÙØzèÎÝû_gµàƒ5ˆõ õÐèUž9ÔªwÇ,õ¬L)6›4‰Å×Ïö™k¯Ñª†pÌ{âït Ž­„YWvÌûi³”kðE|¸§½ìŸ&ÜL´ßM§=ú`¹?– ²xò,‘i;ß²«¿ÛC=Eµÿ­dŠP]ó{.ܵ»‹Tâãý³&þN-æ±Ö,”ÏùÖo bVª©êÿ"ˆà? ÷Oá[«© Àõ› ÷ó؈NÀg_籑Lˆ÷ËÒñ~ÜäÇãqeëŒá~ H÷,M ÷_vF:loáùβ£ýøØ+…ù-8Ì/7 ~Ô íÁ+ø§Ù‡go³$û XðcÈä·ŸÑXœ f4¡¼æÀg9ÎäAýy?O§²‰(W%ªéBU~YÀ÷æŠC½Üe±l˜€v3ý,2,¦Û±ã´>ÓæóY§ž7)@¶¡^Ȩ™ù4heäG#_ÐÎgÐ_Ñtoû”*¾$ë¢s»A!÷cppüí’š%w\UQ³ñ®¡¡EIF¥T9b EKšá–%¥5y¤Ÿ2qÿÚÃ}r8®nÜýðÊUím©-n“HM6S~ßî¹swÎÏÓ€¸ôHMØèQŒH³àê#e±4”4BP!mm(ÒºN j±¬€ò*DÞ=@,"()-EájZ"¥%+Qom³˜Jyòúú´xªB<3~å3™µ3Ÿ˜*º üǧ JÉÎv›— †ÞÂ(Šhð$3BC„’ÂD¡ çZà¨T5nìŽ*fÔØØLúbôµœÓ©(ÿå¹yÀMí»{c5[î[¡$­úZ5£EB ô­¶rE¸FÌjdF›ËØ]:?¬fÕŠ9•ç׫ÐoÙÒ„Š´éS»{gÏMöW»´°´õè¥Z©Û·çÁ×·]ÿ‡'wXNëÏÑqã—¦–ü>©Re´*/}®(ËW+;;WiÓÙNÊžÒÙ©³w}Ð@½b]¸ÿÂø­:ŠN¢Îcü:¨Ÿø”¶3q¢†ø=ï3¸ ´j t.—$QÄJ)Фár>S?‡«"çxCÔ!bÄÃBßd@b¢=m/ñ©ˆÀÆß)O’ˆIæ$B‚N“ƒ“§Yg¡ xá¬+‡0]2µ2Ý6oƳ¶ñ…á:ò\é¦y|”Éwø%«ÉÄb&RL&ê-ʬˆZ“ ¶š…QZ¥hiþÉÍû^?ÑÞ}æ¿öí}±¥R¤U±¡º¥³z¾ºª,¿kc}¯ÕHʼ ªÊ½äCfÙâ>î1Y²@¥˜»hW»þó‰¥mÝR…7âEµ®ë»®CMÖ[öõ¯°âwÿ¥ú7Á¢¸Šœ¢í|÷W>JÊSßBÛTœ.7a‘RÕeßÓ¦ÖÛÉ{9=¸™ÛðŒÞÖR£'¹á“ÿâþPOï(®k-€ÖIKª1$2³ÕšÔÉ¥ãKˆ~Ѥ¢“ɤ]p9ñ«‹×XõuUùQhÏ›|~«A¦0º;r pÚRÂÅçêWS|«6Þm'A‚È/€pQ n'8l7p'ò¬%v²K¦³iÆÎç”F¬=ö˜Ýpm|éÉeM뻫 ¦éçÌ*"R í²a(eð Äj"5œD VÇpöÊå¦zŽ=h¥ …cNBÒaóô+-i'ørŠÿür©Ùÿ×+áJt9^ŽdnÊni&ˆk6¬^´ ¿¯·g~WÛìæU-«êñX^Ðo2h5P|J«cù¸p:ºïËj5"šÞh ƒÐÉWhóXM¢ àwy@ÓE ÜöèÕ%•×Ü·š©Sêv_«¸2'^]WÏñ67´ì ûTµÝ—äÆª¢¶Á¶"wÝÒTËVÿxtng¥œ]èbl<`3élzµ$¢ŽÄâcŸÎk^×âåþØKºÇßaÜlw÷œX[]«WëÔEA«Çb0åFSÑâ:_q Ê¬7éY»_ï ç8 æÕE;ª½ñðxÏŠEà%ªLó[ëø*®%/¢QQª&µŠòI_ÃⲦ€óMDÃùf&æÄ£)Õ<ÀÈPï"¨ë‡š ¥DÉ w 1%¡ÄTÄÐRˆŸdp5ˆe)Ëåé'‘](]“÷•ÿ‹SçrBHÙb ¯«cŠÒ’ÂW®Ý ‡jÊs±Jí”—+6¦‰^þ7¯Ù—ý"g¡œsU“× Â÷Å*‡.ú_¾ÓKsnàß܉É7׸}q£Öèÿ<Þ0êêþïP*¼'1‘€pþ#øžâªÕFQÅv¢X•Z>êÉÖjR!'ëÌM“+¥±ªÔk52š0ÕJZ® äý8­O”B¡T4tÌkŸ 7ŽJ”Ù—,A,ú퀪êA]ëM Ò›^oò>;öØM~‡|wb^‰RÊ…þúIƒÿ@xÿK‡k Ø¡çþ'ä{\N÷Ž*×Ñ|ñäÉ‹µz›Dµ~ÇÜ“fù»£llü÷–”ê äø×È£ˆàŠdYðW“nüo. Ïâq ø–Ãnà#± ®{ ¢qâ²…ýÀ²v "!’¸=" ¸8Y,.F=‘ƒÂ¬$Uý—¿Ùïþñ4±ßæöQ¹ý¡ö]ôúVO²‘£À]78vp6üi=Ø}æ ÷ÞÞžb¹‹’MÌ*B‘W¨¸Þ€H¦…OŽý¢ÂRìEÉçÅ„D̈%C¶ˆb%F»r¾í‰L&¸û0Yn7NÊL³éò×NL=žíSJ<øoNE>ª{*‘u&2·¦\$UÍŸ½“ÿ‡ ²f‹ÑÐ9¯¥)U]QÏGôs†z#òˆ+´i²1ò@ åøÆãJfÜûh±#OxIÆYOOŸ,)âSÖÄL»yàÚÛÚu›Ö‡ïêlZå\?ÂÍ¿÷ã#¿¥wóñ;êãêÌ vD(…+o·µ×~g௮ÏÉÑnxã|eï¬æÜ×stIcÝüàÏ÷ÜüÉÈ*Ñ~­ýqùüzT&Ï͵ˆ.}€Ï AÛÅW!À—›D݆Àp%1ü€{Ûàú¼ä;·>nÉ­ãºáÄ5?|òùâO€åÂ3Ü'`e÷…÷[•­T§¡òvžßÁ­6éÀzî$k§wœß >ûóù?6W{Z…|x÷ħÔ9&NÌFÌÅZ¸Ôg·6756 k î­Má´sàæ$ðÿ•SúÆ ´ÈÚA»ùQŸ'(ÒFtÌ$ ŽMVŠšÄ˜mZìC} ŒYœ2iïJ2„g'}+À;[q{QyWEH£ÒIój¿¾zÑÍËŠ®S¦Ÿ8[¼ Éĵ{«ó­×®kZÚœ¯SE%ͯîX~ÇÚRƒ¿(÷)ùÛ¼ÜfêáÎîr}"'×—«TõÖ5•.ÚÝв&äë+2yÙξuºhistq(RÊ{ÚÆtAX[lŠõÕë=ºåHÖÃùo‡ó?NTm©V :@Šƒ¨I`(Z˜÷¤˜“XNb$'#ZKLAauóÂ2y¼>_Àc‘˜"Ì ‹€÷òû ï{м°BE+@¶úm5]§~ºGNšõÕš…'Ê[IåŠ7ô‘…íõŽ´ç‰VÂ#ª&ˆpÛêšžî#KKh݆Gß;tò/'÷iíÕ]UÝR…Õã••#š‹Owi­Ôš•쯣þI·|ë˪‰O™@ ¢$êˆ9Ä|PÉëKU'PPu€H‚=â“È$TÚ÷W7)-*h^Ñ:Û¼‚:±ƒP(p8 Qi%iO/Â3¥—_AA© d®tÅóÑ@KþíùiÐbŠœröLC¿â‰2ÙT³‚Ýܶ9­Í©j/êqÃÕïTšqÏ|àâßjdÙô”8G:r9iî |¯ñ¦-ò¯«Y“àÞ-dLì`8J} 7ÊíbwUu²k¨+ékZY7çx,ÑÊU®hU¸¾ì®[RÓ´-<æÒƳ©_±QwÛ¥º_vŠTb‘JDÞϪŽ“ëÈoýD¯ÖªóýN¯Õš×\QÐ]íM„k%r½E_’çõ[Lysj­ež gü¹\qär­ï±«5Âb=ó8yn¦†h®Žh[?Ý<1¼ mÓõ?Å7Åã=”ØD%Çz¨‡Ç¾¿‰×]ŸÃkÞGG î‹GÒ¸Ãì `=B¯S)Ä"Â,ŒÐ*ÝGAè#É×;.ˆoªÛùغuî¬Û_p€z©xá¾9ãwË¿¶»µu÷×Ñ÷sö-,Æ|QçhüNLÈQÏÁ!Òêi†¡Û„¦ô4“Õ13‘Q5c/¦ÿ’¯?84>va-#´Ù>‚ë‹W‡O%•ˆ)’™./€f†H$’‹dzäP7³.+…ÿ#¹k/EèNÎÌYÁÔ½à%’«[xüXÎÝ1„2[º¨säQK>>èÂ÷,]Ld ¶¡èº I4 =K×^þ›þ” ûA–E=w (¯:7žØDþðЧ`’x“›G9áI9Q7Y”7žnMšÕäØPâGýl‹ªiÞ£ÎG~YxÓß°hãÞ†ü%^¥røºŽ-O>H½ºýâÁÞ"ƒBÓ*WŠÈàÊ‘¯|ë$'~Ìí ÷0wÁûu?-Ãiéi†ÛgeeoΉ t(üØDoºÉ®n þ°}² lÿSü}zÍ/G¾Ë› ÷ î«áæx»Ž-K>øÃíLŽ/°räý6ßg¤”–‘×~I?T Î ­¿ô1yíf<¿Á+œƒ,­†çä¤l$À jÓóx>R,Bã(®l£hõ?Ÿ’ÌáÏ}™sàû1˜Çgæ÷8õ%ÂkÐ2.¼ý˜žBc†÷/Ç×@=+¡¹°7TEGf/A6 =+q]’ŒlØÌ×^º›úXzf8~Ít{(àÌ“w¢A5e/² ütüyÑê1ýf~ ¤”,Ç×ÀcH7umºn™¾™¸é¦ üdóøóäµÔÇ—îæuÄî¸hÄkmĶÑxˆ¢ø™Ì¯ñó*ê%ºLÊø2àJX\KYŸÎws Í-Ìx¸êfDKs]ª¢¡Ù;Ÿç€ÍFL|Ï+"4›ªo‡¢ùj ÁóÇK£÷†ðhÁÖWos­Þº%V;/O òmJcÝŽeþ«®v®86ßo«o_œÜrÖgk_´ªèÅoºkúKJÖFm¥ó÷9L¸<>‡8N¢w[ÜÆp*b¤(ÒŸWñP²¿Æ½ïªšr þDÕ}³ç`_4¯ÿ@/ú¾ãºÁ8’cœl¯3h%u¥ä, H‰*„ÆÇ–¬l>s%í±Íð»tkÌþ‹Ö@P'4ÇÄücÂöÆo~8ªÆW­¼>ºê[ç-Jxæl›oòidj½,VáVÖþÉM£d±#WT°üŽ · —ÑT‡š•Qy³'V_—Ô†ÉÕ8Oø8æ¦,$N],€Ø<£Pý´˜bž¡2“îÉÓ ²õBÒ't§…GÀ_Ó ¦ñΚlH‘¾ ÊŒ‡2af<õq³¹‚^WHÇç l˜ü¦/Ì‘L ‘Ÿ Óx2£KάIöFQ›ä«4bRm+‹ö'ê úÉßü½[§PhÞÒ}p°@&5x,Rµ^îOE-c¢«\5ÿê6<~†_+ýЃ¶@=Ÿ_EÒnÌP5YïC§AìdrL¾Ï“@M¢²Êöô*ÑŒ°^$ôuGÁRd¼W/XvvxèÖ•Åe ·íÚ¶°ìöcó·uëUFIQÇïŽo|rWmÙ¢í»·/*Û¹¾¤»,‡Õ³J宬+ßÐ[’?sãü½5E…ÕåÃÒȽtÅ=mæ•'⩞ª¹ lѲ\«ß®Cëê ;Ä O1D´ß RQ—šDtô|ûH¤[õ(‡•'ÒÆUûÎ\Ä ã»½b]$-ÓL×\L÷p©¡Ùèvð;]bw]ù²p÷¼–µ:§¥}~xYEݵq=øýØ0yš{Šz&¸|þf©°³<'§¼³ÐäÙܳ,0Ö| yﳄøZÂlÁ¾š’T"† nꦴ€Ä¤¯8’ªÎXÔ(!<èßêÝéElß“)UtZÅ$³Hðôâaåô¬Œ¤çܯÿÍíÚØ¼]lÊiŸßã+i i@Ì®4V­évwuα_R*K¾ýÈ¡_ÞÖý|ÙšS‹œZSV¾æÖ‹àWðÑúg¯Ÿ3ÿüÿœ\~ñ޽qŠ¢$—ÕgÌkˆ™áá…·®[¾~ÖuO­ë8° îØ:gð«K‹òöósÐõÌÓ8¾ƒ¸X  ×Ñ0*陋ïš$Î}¥PS`òºÒe¾Å ƒÐ#[Ç‚GÁÕÜM£G½9*PäPêãÝ5tá_ÿzrl)u§Äà±ùŒÑú¨ sßíb~J½%ÿ“Ñ⨘šD^ä¥FyÁˆö“WþÊË÷ Þó‰š'Sk¿ôrR© cÓ;Ì ç08cõÊèº.RÁÖ@ˆ{1Ë£ó‡ú¼[tä9ÇÑÀ)‘?Kɘr@Â8u#â½û‚î¡-¬½&6’·á…“9õ޶E -(T±RÒ>çȺ’Ce ÷ÎõØ«›Ú£+öØ ó—ÄïúúȪ%yíêñ›·l¬/'9vhøÖ¡(↛M¦ÑÉBõ”* )èiêq%ƒÆ•ƒ%s Mß~‚Ü0~óÆ-ŽòÞ2rŸƒÅPwQïÂù±‘³J· )¸jilqj“õÐÚ"T)âÝ563Õ§ÉB™ßጿL³À:ª ü˜%xØL»ÅiûÉK•ºkdðŽšÎ뇊NÝk he¬Q©ê<©Þâ’CsG¸2Ǥ®¸wÛK?f¨µNFEf/+ëØØâdYò\÷r¨{Ÿ¡~‹:HïðƲÚ¤T}””HY@b£}Bg}"XÕ!9ÀKHðê1„µß¤ºP Îffºdb>-ÁÑËOÀÚE\~R*>åxñåÇËxò0t‘{›jS-ÞöÚvøT5¾p(ô¹ÆHëšeNK?ÇŒ5 «˜-FãÜ(\ž–¹\»g½·¬0/õ$šc†œ²ÓËÚ6·s·ÎO´.TÛ¦³‡Œ‡DUÄñ|IŽ?Ìó|³hß4-Ê7Ý÷¾™È1Nì¼9Ë99ú”Ô·º¢<u´4¦ÌbIÅÑæh ¼bµO†>5-EOÓÿRÔÖ¾®¨¨È“ )•ÃmEEëÚÛ4Ó 7¢/‘þ-óó!w?ðÍNêÑ3•ÓR@LÇ@p*§QÈ¥ŒqµÂ§‰Ìgî­$0WÀg€Ö²£¸ˆX„qsŠb¨Eš™ZX´°œâdQÍM‰Îdg4 8lʔУ¸l6‹"Þâñjm>0YÿˆgÕÌÜôŠFº{Õý{[ÛŽ<µeìÆ-amtë3Ç"«ãžr|Ç©P‰iMtiSŨ¯sçÒr±¥}ÕŽâö©Ÿ¯gª½gG÷NæéŽ+žš˜xþxoÅò7Ü?ô8*~oˆS™4R•FâMùõâšË¾xª¬§ÚzîߪW_ÕÛ{d¤¢|éö†/½)ðèìYFÊm¦Š©±–¶À¨ÉåK<„HŽ´U¾ÈܧÀíó˜Vø²©˜²Ì2tML3‡¡ du=‡4Ùß?×ï•(PZ+¡uåKë™71nÎÉ¿f¯‘½¥r Á8 öÆ«3-âZîMª„JKI‰/&@¼œÔiÔ‰DH¶D^lÂ8¯MROh‹íoÐöÕºíw¿òí½{¿óÊ=Ûëê¶ßóÊwöîýö+wo¯ëHlºþ‰··lyû‰ë7% ?ÓÇ ðݸðÝùÂ|#øùÆdrã¼z0ÇC3—påT¾iqá{údu#Ó-2ÄrbŽ{QŠ&¤$,!ˆî²ý„TÂmÍMn犸äæù© oa‚ÚP¤D|£ÕTçã9ØÚØÉ —=½#Özô¥ý{_íéyeïþ—¯líxêpå`{mq@¢Q*JC á›î~ò3C'ª_ŠþÌwÞ©Y ”ýRUãæçÿpíÃÙgÇ–õ÷/{6ûðµx~s‘-hÉIÅÆì¹³3ÙZêÑ;ÙšººÚLj…2¿÷n˜ù“È:J‚úYZVŒÄÉ ¦;¾¯®¢ÄN"æ&ˆ¢å@´&ardˆ¦p~‰„wH'¾ZBnX”7 _¨&©3ëy]X;]û *’¨Á{ˆ§6DU–c÷?Ën5 ø‹R!ãtˆ %¦96àB7™yž§$kºpMÀ½ Vtu¾Ëeïܲ\œyé%ÄÈÇîxçà•otÖ‹4ê¢`ˆ®¾ÓRÚ>ýSqzüdoe_KU…´ØÞŸyø?¯%€òoœøáýËsî¦u;ž¸nû“U¶¨Ü# ptIÅ×Äë("'[F>ÅàPEà/tnKnÚñif{·“áMלø6³MüÍŸºîb»ù6:ôlöÊ\-7Êd™YK‹§ß§+§?ÖCßnCíÅíþ@Î{##@?,À“ªÒå6?+C Ž…C µŠ·CHÅ ¯“ëìb‹ »V8É“)ÕÅÐü¾9’•þZöLâ+~–fqÿב§½.¶›nG¯¡æF½Ì ‡Ž½»Eïøø'…]¥_Díñ¡@;îgæë’‡ ÇÍÆI´9tNFÉhjR*1Ø×MP2™Y-áhÒåúºò2ÆïÕ ü9¹0€pʘ$ªCÎ:„lL3‹ #1? ø èœô©XU{¥»è(ç0ßö°¹npª+Ûr Ç|ÀÝœméÚ6Xo~ø6³ƒ»ªÈ]ÙV• ¹§Öà*щDº§áÓKèUEþæªìM1z³y¢3ÈÒÓ¯²Î‰–lSq:UÙâ+b¦Ÿ GWŒÆ@ \ŒHxu˜¢…Ø]šð¸kÔ¹„b@…±x…2”BAòË$—ËÔÔXUôã¬c~%ö…Uò9Ò„Õ:߈ýn#bæÇ~¶»"|é%úæìànÉŽ¶Ž/I™î¹Ýääö«ÑƲ(Í>ƒè”'u«â)¢ÃzW1 __ìÒú ôÅx¿eú1Xá×èöéW¹@×xó¼3HãOWdkŠ™±â°O_˜ WåÙ™S@ÇW£]iUIêøØ;¤lâíï‰Â0<šcGHI)'®í³\O‚n¤Ãû(ˆRiQbº¥'žŽój/^Ú”ç2«çëÛþ¡·cª™¾”ÚTAe†™×Nºñ› ó·j¸j¶ Öw «G‡—öww¶µ`°â€ÓSåR™ùÄì„qñj[,Ὲ1š5“ÌIß®‚zôtu«Áí/w}åÅk.KŒ–§VÖ;¼mžDÅ3i·³i¬¶m]k¤È¢.ë|íÀºû¶×®Z½'¾õ¡mÖ2¿«hLêˆ×MFêúÊt´¸H›£*À°ÌC;öiÜM8h½}1@ä w…‚ì^rýÖ&VÄö›\f…ÄMûäzµtf†êÈž•ÜÅcÄvà0ó’,>\×sŸ ‰® Q×&Æ+`:åÂòZyô–ˆË%MLÅhªyæ)NÁ¤ìp~ýiO]œf¨°—æm‚XdÉ5 bÚÊÊÊü¼ôDóâ½IðâQ¿Ê˜$ŸŒhy ï!Äã1ÍÇÎîÔ¤ƒÁf펯êè8úâm ü¦ÙyöXÇ«‘Áýݺ:»^ßµw0ÜÛ¥«÷xêuÝû#ô­§~ùìµ+rí|æ§NýüéEîPЭÞñÌ/No{öê!¥Õé,U]ýÌÔÖg®R•:œ¥ª¡«Ÿ%w'DƽCuP+©ÔUé+ÆP‘ËZÚ"ìHØBiaSj¹Éù(C*…\5J)tjZY¤PŽ+,¢4E£ç•Å:;·m߸vuçÊΕ+–aWÖÞî¶–¦—VÏôŸX>‹Í—ί^Å Â-2V¦EùÝOšW"nó‘ë#7LŠpÙŠý›Ó[·X²ìøö5åµé§¬6$UF½a_¤Rço^²ˆ7¹råd¡x‡®™Ü¬ ® TZ¿.rº™€Õ©;Ra³UMhiWöM6û¼Œ:‚±ù‡™äòr†)†åX†›œMyJÇ éµÉçÙ<1³9õò¥©Ù²¹üÜ$SŒŠD¬Fno Êó”ðÏ "*G’¥sFÑf*¨Ž„">‘&ì$I]°ã.ÁSÊ}Î{äD‹É1%FžªYÝê;Xb^Õïiˆ(5™">os¥U¡RèµÇ5OÛ†ÆÍj­"9îtm¬‘+Õµ§îúì tµSŸýÊ ?^ؼÿŠF´~ûׯ+L…ƒãGniÈ4‡Ušâþ{—n}í‘[Ò Eó›6=Û¨ó˜’3²¿e~ ²ý#X¶—°9Ù>z©²½ú“Içê¼tnZ(_T8÷Î çLé…söV•¬µæá=kïÛÕ&yéÉ'_‘uï¿olÓ½5)®H¥²Wt%«;Ë%èÎìqEÏš¸»¦"dPüTÞÝÝß¼ÿ‘ ÙæƒìšuînìH-•È,V‹¯{KëÑ®KRÕ Š,¥(6D°ÒÎ#—«/I.gœÂ6`C§%â»§Ú|—Èf¸½ñз‘xT®—Iõ2cÉGWdŸTY cžo˜=žFá'aŸ~Žàƹ°\n_L.Wÿäò¼³ËÊì÷ž|­yÿ³èñÓÆRñ k>3zƒÈf<Žþu¹L/•ê¥Ðµã¸cèGçÊ šÙ΢¿|ÃØàö4y»Væ,Cð>k0ŠP¥QÃPÔäÿ”P´K"¤Ø1¼TŽC´üþ¤ß!]T"_T:Å‘L<üu‘³ÂS¢Y ×îו¥@B}ýˬʾ^Ñ[Õ¸ÎP*V–x*œ:$ú £ôWÈ9­Ù®?™­DÏˬUþì‡j3jŽ ÔºÅèóÙå"Oji,û%³ÉýUVÊ /ü:Ú4¸Ý^o» Öã?`ŒÕ@@¯Õœ_WÏÊáÍéêªp0àÇc\D¯;¿$žŸ^‡aÓŪ„;û0¬Ùÿξv Kÿ»&ûReo]™þài£U´\Yê©t÷™­Q¯M9ÂY§Ðk:‡Yˉ´f‡îÚlµø’ºl?¬j}Fî®]0r”›!ú^“×íÖëùY@”+»‹þ;û-ê º#­9æöMë9VN©‘TÆñÇ p4äJ ”\¡”+&(`([UHI)dJÅ(N0$–IGqtªÀ×y×G,Ki¸œD>¿‘K¬?{Ùøgô[2ë.©ŠZÀ7 [䳓R)USó›à‘ˆ[GïÜJ]¸7ùäxXB·ïß»y|ÍXfdp IG[ks*™¨®( 8­%Åf‡º(Ì›‚‰1MC|á„‹d"žƒLÏÀ—óïÆÁÁ2}ÒO ^ô¤_ÖY\ÊñÚL½]l©¬ì®4WŽ\é- DúÛ Õɤ9½¢Ê *­B*{Ì[¾>’Þ1XVšNØÛ]U}ñ’=‡¶nÜo‰-‰ÕôÅÝÀ$P~bÕ[z‹|HËœœë××:ô»ß¢9¨¬J/ duUC—·iÛ@TêöU•3 S\Ñ0¹KZl—ÊxQ{öY¦Îr”úNZ…­eµ†¶W0öxQÛcø‹Vò1‡½*`aø¡¬SPÊ—ÏáóÅõŸ¬ýKn“|iÀåvÜ.ì²P9ªC¼5„AµJí¨h¯jou{ÛÖÕÛ%E¢ÖRÓvk銻v6ÕîüìÔ¦»j—\n¶š R÷µk*%2IvÐbELÊ»µžÇ¹A¯Ð"ºæü' Î8-ÊBÙ£Gá»­ì.úQ¥#þí¼#b)O†&wžXÅÐcÂY’ûž!/`R(Áé̼¤Ñv!˜‡sÄÍûä±ëÔÚ‚F£ß¦ÕÚüFcЦE{´¶€Ñ°i4¶€É°i§ß¦#Óßf¥à;`A\:hš÷;ÜËã M²;è×EwRnÙñgÊ- RE¢¿Rnôóì£Ü»°œ?§LܻԲ3Rħ¬Áƒ5cp jŠ¢Yz CðÚ•_äÙ Á—ðl?c–áÄ'L^ø@À 𧨉±(·Àû_=JÞ¿¿Ÿzr樌¼ÿáýFxÿ áÅ"”¾ŸÂò(3Œ_¯Çº¶‚Gð%<ÀÏèe,Æl†ki„cOJ,µ˜C2Z&&@“hæ›ÙÇÐh%F|_‰ñŽBö<ÉS*às)[æ§O± p“³ ¦œÅ€ŽØ‰1æN.’8òƒß}ÿ™’†õ¾žŠ3ËeŽtåÉç q‡H¿}Ü@]û¿qçÂ>~ý$²Îïãµ/Îï#uoö1ºšºŽÒSé3U Â…†øíã“5 i ø]BœªóßgÎxnœÿÏ”‘ØVgïýTeÚUÊMœ:Üíë\ßPrÝ[}#j¥±S«©ÞQ_Ї èC×¹°cq´,ö2BHK"³b„/3%¸ S³9n°½AOé¡3œ6ì]ЕÐ4ç÷¥b²°/£xx=ÙϲvægT$„7x{º® É%µ)Z&w!1Ha4‹ñpð·Òyß ¾;U8÷ œœI%’QbJ&Æ©]0Æ6…Vƒ— +¤´Db*\Í’tâ•H*%fb;7ÍÍícI&(ª±>Ñ–lƒ^W|­ÛëÑè±µ\SŠHp!37ì$éŒ;« ó o’‰vÉ)€‰|½YӺ嶵»ŸlõˆMú¢PÓXëæÆì³h#j¼aãÊ6T§¶Ý»~ÝÕ1¹ØŸ}uðputGë‰[œ«˜΋•¢õ:} ÕíJ}‰³dIÓNNÂ=ßP[³ã‘mk½ ˜n‹gÚÔÚˆ®M–?qÏÐÞn'¦ohsöÓÒrc€¾ œÎÍÍM!'fS\„y„zœ áç°üÉQ²uçСųðtÔ$+‰DüWø‚mF™?SO`›zG-ÂPø"ЩDÜd>%(ðxAAÓF·Llêl»‚ù*ô·5^8qÉò”kV‚X md4'*l¿65šé[ÒÝ‘¯Ç/’ÚÂ~Oó¼g8½t'’H›‘XÒÏ»…w€HI%,(†%ðNŒIÄÎyçæñõëÖ®Y¹¢§+µ·v/~¯übï­fž€¹«K×,A¬„€ø3xŸç5Â@²ø…"Z2gpý}z§ÆM&Pæ¾Hû•ÌÇÐþ²ôàD#-寑Xº”Ä¢Qb©DŠÃµà…2¼F2P„e,žK©˜“æ^[8¾Ikݾ€;@æ¾ðž(cÞ‚÷v¤[SHL¶°Aœ]^'"[B sÌŠÇà=-™3+–¯[¾aņP€¼IvÑ=gþïžf Úq,‹Á*±sW)•Jõ¤zÜQ²±aê"m—3_}±"=<ØL‹…ŽTó¤@†#lØÿ…/›Ú²*S›Ú³kË¡©CÖe&WMÂÖXY»¿\i»Ø¸bdO6¥ëSüÞ±4+âG'tA,áÄx©X‘dÎ{=žHÀOöý÷üŒ°ß£Çg˜n…9eì¯ æà¾´|×Z.Â;w½TMÉE¬H>K·agËáDS¬"G¾1¶§dÞ¦ŠÏ¯–/õ) ²Ì"t?“6­X¾}ÛÄø¦s€Òy‘sgÿX0ŽžtçÊAX*ØFR¨NA/8©ˆËh‘d¥N¥–/ëï[Ò;ËäŽðEæ¯öïìÙ<“V\s”VH £ ó˜[Ç‹#‚¬¨VTc*˜R8eóúS;k^URo _·$g9yF…ӱͯŸI{xæsüê˾lqTtñ3cQ0Æ¡ôÀî)Z.Æ H \,Ë :(–sâ;"wŠ˜vkÇöͰ¾yö¤p_lŽ#\7y] ¼ðú´ìZ$mƒS˜Û¢yA– Š ‘¿P*Æ4L,[„G•-Zž’±RÙâüɸaýñcû÷íÞ±þÆ 7æØ”ÒÆz×íðO+w¨™æ´«z*!ˆon*j}Ž»éã=Pô£±™?2ïp›)+UIõžapx´×,dïÙD,•fŠžÏ>Êœ xC/(±8< Ð²H®±¨€f‚6sFmU†'ë—\·¥¡yÿg7nüô®†Þ¥¨EYé¸ú[®–u$[¼¯}}]ÃÆv/³Áà×Y‰-÷n\ûèáöÖ#Ïn}ïú®Ö|®­e{$Ú¿µ©m[o |`’÷ù#ó:ñ‰Q Ä,¡àÏX\ç¡O—É‹ÁçÈ0Pmñj§QM ÕñÎpÐsÆ4jÊŽìRu˜SsJòÏñ·Ñºy¿ÓâõíM=<±9µùÖ•+oÝœšœ8r4½÷¡õõk[\ÁöÕÕñÕíAWËÚ†úåÕæÒäpþ”š«—«“›o[uÍSžÀû–œÜXS³ñä’]ÏßÊžE_eþN½g<œ0…1üÉ+d+Õk1nœ‡·`À…d Ƕ2XQx:αb)wP$1ßÔ9£Å–¨S?-Åð3{––†‡—­¬ªZ¹l8<ýWöùâˆS«uFŠ‹#.Îám››˜Mô(Bl‹ð1èS@6qä !FA2wbLwl³Î/©šn£‘ÓnÐk`Þé0WLrÜldÆè¥B[ðÚº—´5•V%ËIÃHF[ÞÛ#AᢸќÙ[‘O˜ Æd¬t¶@ŽÌ>&¶ÊŠ2 ûj·jÔ¨–®%¶JèG 3Žþ"ò“~àÏ ºúÑDµ½$gsÑëbs#F- BERÀb›Ák˜‚_áÛ>ü-3„isGæŒFS¬amØ[­ý…Û†ï á£=‡pFy7ºhîñÛfþÊœ ¦¯‡‘ ;¢å´@´½ ’RrZ"_%Ʊ˜õbL8XØ_ O4 øö’œ8S‹”.,—IkxGAOb<Ü AŽÃøºœŒïc„ïcšôñSôËBÿ }¼*]”NÕ0"T LÓˆ-ôÓ#A|bÒUÄÓ‡¡¹Q1ßM˜—eùnzàKšZX¸°X&mVÈ;ÚÛZ[š“‰pÐn+1Ë㊸Û-/å×k#ôµÛûº‘ïëIa>¿@ú »ˆ9Ný$­A eÉ $“ìJY¡·U”‚’ËrrNˤ£Ó¥T¢a•„^0Åüý~~D)H9z6æ,M’B¥)'U°8J…äRkâP&·kp ·§»+ÝXUáuæ–M MÌ>Á´mÀÔ;•Nèñ=¢ðí/âh‘ˆ§'r±ËäæˆZÆcr|îH)¾%áøüˆ8r¹ ÅLF;gBóÓnë^±©¶ïøÚD¤e ½’ÕR0Á–¶2Gª7R»¬Ö£0+³×Ì'.ï•Æ|ÆÐÒ=]==ÕeåŽÛ?SÖTUV; µ”[¬.«‘ù•%Š© ç©Æ3mžù½DYÀŸ6R©cÔë<Ê3^Ì3%Ùvr„¹a…|ªü‚U(…B)Ô˱7P|q*bå,¯šW“Z¼"ö>vôòÃûöLM®[Èñ-Õ?η¼—Î×nªÚZÛ´­?íßÖÔ´•ðµ”Ó™ ™Í!üøÚxÅàÖº¦­˜õm=O‘Èœ"Û H¨?îEjC–žÿg²¾¦Ke ÿd³g)šÈǼüC0g€òÐ.1VŸ»é#ìnFÕ1Fúyî$ÙK 9‚W…°ÉMÚ?Hå°Ä lŠYlʘçgcnÉ}«BÚt¶77 A§¯©ÜHœ) ÏHrž±‚h—˹4§[#a¯?eû›w/«¬\¶»¹y7n`wsbM'trM"±®#ìX7zÚWWáK)9™ä‰d‡”Ky‹‹í„ŽöÒËé ýÂ÷à3 º‡Z½(Vކ.øÄ-ô³þ©Ÿ¡NïEëC;cÐpà1o’f·Špœ5 ¤&Ä×bäÖ¢;id1é´E*¹”cQ)]J.iáM_åöAK¡L«š•i ±Q¯a¤"-È´1.¦[ ÎþÝ|Ðüþ|^CÐXö‘éï³-`)ðî# ?ÖÁ»TœªO§J•@Æ­Óò‹ ™÷T<·äxýQUŸÙ¨SNäå´FŒ3aHEó>Ïï%ú)f—öŽ}+Ši›w V[ÚX¦Y‚™é ó ìhMoZ3Þò¥ßV ®Lb&ir¸ä[J«|FN²pX°NwÍ”Ñ ‘›¬Ó]Ľd)Õ'A+AdoOÌ”¡ï‰3£E"-©ÿRJøÓЏ¸! Š°LØ“Œb@ÀL©œn-gÎ쎙ú6´ïØ[†ýÄOÛn ùº=ðþ"¾.Îω–AÝXºÂK`Å0Þ"¹–€”#Ð1ÞÛÆ{Æxo©due,¬Ñ‰È}µÚªåÛÚ‡ÛZmÅÓU6’u#DZ =*¸ŒÎ‡K756TUDC±H E3p¾™û¹Í¸=23ÏÁ×~^’”S³V™üëÅ·Ün|ˆ ?ïÍÜ,ZÏÝMæö=ÒÂ:êu}Zm5xK3Ò$‚Ã#àžäšA2:¯s¢*I2Ex¯JIz_š’2´tôBUp²"ÛàReF–®\ÛÙŽ¡™ý^œ8["Bô€"—&€ëçRIŒETi5Àà3 ç# q’8Á)•(éB!±>ìÔÌ ó‰YœD6>°˜O9Âøú¯‰·xhݺ‡´Ä×\ÝŸµX=èŽÕwl©YûÄo®»î×O®­ÙrÇjt»Û†žP7n<½jóý[‰-÷o^uzc£:;â.Ùð9ãØñÏm8òþÃÃw¾dýçNŒ?·¡ÄÍï§)8çßá×l CDìWQTïöŸbóqv²®›àÿóïØ„ßq@Ka‹”EzI©DÜå¤Qº1ޕ芄œÕ®jmrМª•&:ã8ô?“|„ïp@¥ Ù"%ôg€—"¼ÔrçÙ•IhœŒÎéx*OÍ©°hÑLÚ Ër¼(VQ ü(8ŠÎ©1c°~—/©Ë!äº|@ïtn’q‰Çìvê@¶31'>«‘× åƒÅ.q[ö‡­â¨µðúR³Hò/ ?Qrnß]í¢°u`ùû­NQû»ø»ãŸ/±ˆ$Ó‡¯!ûCtOv2ì@}ÙÏ{ìèG Õt[KÐJ«3;…îòX³Ÿï±g¿ ®B5d½7Ï”0_•áµØŒOØÚci{ œÞ°ÛÁ2¨§9¢‚iQ² K%Aåb9Ø åAMÿ5eß|ã t%óëû²ƒ÷eÏg‰,düúö0¬#æ¿Æwó¬žzÏO]$2‹ò“ezjkm¬¯(ËezÒ`CáÃøu sÙ©íüøåHJáK,Œ»+Ð(™ˆ ¬‹”uË‹SxÁËGó5+—IëJŠmÖb{‰§Åó`4uE!šºŠáQÔ½NŒM£#8êÁ;Ю?M+®:&]rì…ݯ¿xꢕòGzýõ»×LÈü;:ýµ/Ý6‘Ìj˜ÃãW™þô5—Í\3bÿÀí§:¨ÑôJ)ÌH I¤8N–ÁÇ$%KÄR Î0©DœˆâFHzºT$ÎÓtbk&qÎY[G;fþ©de¹Ëmpc/­"îý¼3‘˜O›…}¨MüG’£€%B œùªãÕI·ùÃäƒß:vì{Ñ"£F]Ö³¥çÙÿòÑ[KÌÓ¯2@œ¥¦¬ÕñïOÖŒ4Wz­*½±ÎµfÏÉî«Þ»g­öíWó3~øík{F—Wi4®€ëK¯$5¥>‘U?¿üe]‰³D§«®ì‹—.¹éÝ+7©lj¿È¹ u1¿g«ÿ8ÄFú1S‰×`€™+âVÍáyöáãÙÇÅ ^óhBnqóHòÌ#Éç„цÁ31Ï< ýõ¶þb—$ñƒ¸$j]Úu´Ô"–½ZûM9ð‰›“âuiçšû¬Nqâu9çñ®}°³Žo7õ  ? ;¾÷S·¹œþšß×ø0GNõ§ðËï0Ëøœ‡}3¥Ìœ zÞHuãL-í>+Œ®Í¯„ÍSËÂÛR 4®y!Õ)b9Î@B·Åªü›Ó…¹2”óß3érÀâ±9¨â| …¹Q5IQÈä0ÅÑkãO¤|2³V鯬)®Ý³*]uzT=®^v]E@¢Q)œåÍsݾթø†ÓËÔ¨ßZÙê3Çûc–l¯%Úà6ꢭ|icZ]êq‹Ñu°m¼/¹uE ÷ÞûòtE³Bm(-eŸÒ›7v·M‰¾AË]•žyö ¬½¼6ÙËÝ%r”⬑Þ?Ûf"Ì]ì^ ÔÓic™¥ŽTÔEó°ëÀEaRšyoy_ŽeaX¦¨z%Z^½ž÷}_¬t®X]Q-"òí%µŒ=ß5F·Á¯ÁÉ ¸Å§b|c‰‡ðWý,Z2—Á29ß*7ß½©š»ú¸$5qÛúÓOì™ë$ë¨_>}ú²ƒòìô9”ð÷ïë[ß½w ”½—¾o}¬=¤ËV­\÷2œ¾ }Ñÿßò’¿^ÿmR-ì¾ë!_Iýë÷$%!ë@ûo;0/I~] ¼ädíD yñ·©ßò`çá% ¿/9¼äð¬>ö3 s”¿ ÆØPÁi¯Ã®ãÀQJ.ÆQªcÀQü¹7ì¼ h0Áv?÷C Óâòb P27Ö2 -Vý’ó&0Ž>Øç_‡3 ãèÃãx\LaЍ‚Sõ}rŠè–ò~=T ´ûìKh·ë?;EQ5鸜¨.SHpf˜œ6¢!~¡”Ú{œoOûùÄSŸ„âirýZí|ï ÜΓj S³P3âBÍjS‰êòhÀ—£f%éoŽ~À{âø=Ÿ‡é¾4*â°»]vÓ£"šü=°eæOÐf·iÁsºå#ŠÊ¤Wȃª•® –l¡”]([ƒh]_“À²µ‘ÈÖÞ‡ê/§Ö2ÿ‹ù%ÇÖ`hËÅ8«Msƒ4‡ÆÆ&9Å«Z8ƒ½&¯#jè¥h{öö[²w¢­Ì/²GOf¢c'Ñ1<–c¨‹¹ úŠõ*øL¹Ù/Â'á$ÂOê>ƒE.“Äü›ˆœmJ܉ÌÌa¦š`®Iê9¨¤ш®pÒ¬¸ÜEs"–ØCXJ$¦v+a+%lN,¿æÌ•ÖRDá8Ÿ§4iMšj¥LB• PIlȳ`~^¨Ôç{úí’XÈ.1;¶¤ÊWJño¥Ž©†Š5£+xFG¡3Hw†C6·M©3ɶVDáSö«JqÞ·^^£cÔ$s+ó}ÐWñ”ÆÃâDkq†BÚVIrn´ìðÍhv’‡çü%2i¥N›—dØ"Œ‰c'†ð»q.¦¤óz{óïgÿ´ mÈ®™:ºõ¨|yör†þîwO>wò§?=ùÅ÷ïV¦…y ΰèa]8‚ikâ Çè7³]Èd éÐçhª í|ð{25Ç©dß_ãcp–Z˜_R2JE•¦-2)ž€µK {[<5%øZ·+¡Uº‰[^é …èñé&úMøÛÙFx§H*W}ùÁ-'qÛïCÛ·¶-“\pij6¿i;÷&’>¿^÷öûèsŽpÈ™Ívßü݃_VÉ¥Eï<}¢{ú•“üÙùŒûV·Æ-¥`ÜÐF#ùú ºÜ‹[¸õÁïËTœLÿ½³·vñc^Nä:uÕÔjÞÇÆ1ïøIx̪Y"\’¶ÍÁ®÷ØÎ”)(@ °1hKþÀJçX,†ÚìÌä¹{X”}îDö%Ô}-=wBÛ1ôSæ6züù;p~—cŒæÜïéq¡Î êGÌP‡£xIÅHa«õÚùY4p´ÜbÙ5òéoHwO0š;§OÐãÙƒˆåÛßFdcÎðóÍÁ|Ki±8”„M~_ö=Ô´&ûföÍ5(}w5Š£3’}7ûÖjÔ˜}c5J£øšì»(Æç–\Nmº÷#<>г¼i×l>L'FòÎ^L‰ïÌe´™ï”¡™ó»o9žPÔ u„-–¾VŠX,a‡¶ð÷X+Lºfök‡NçˆLß_À‡0)Ïã ºáŸFúœì§úEöóŠ d¿äBÙOK.*û¡§¼µW²¾”³ék¼ ½†­6MHYKI•µêk<ë½I½‰µ®‡¯ŠËL-:(ýñzóú/Ùf¼õn‹áËFkàX\¯û”ÞøöÛ&ý= ½ö˜©40JøK 5J[iÂç[FA>4ó:õC*’’\ßìA!.q²>|m¦@ |.´b;//Œ -AW>Ÿ¡¥‡gÞ„–Î{CuVRïÑRÄß©ÃgÊ uÞ¢Nºo¡ÊKämˆªÙOý+u3ÐucZ'E¨)çG ôΊýH“9?R±Ÿ€û'ËP­ßáóÕ$ŒÒ"ƒžsx–X}•®Ší!½X&Ö[ìz„%¦š™€îÝ<×7U)ø¦& |SkÂG$âp„Ûaøä¡h9:'ÈP õÀÌÙs×a” 1übÆÑÇ-äVq€DŸâl¶8{°·ÁëÂw8^”ZOé2ØÇqª%ݤV)0C96³Ñ ÂXJàçi»‰&c_5£ª"(-°EXOú“BFÒD¼„…Üz8„Sì‡é™õÿ ¨ —+å=MÝúŠbK™¾§©G¡‚ß{ åÅ–¨¾·±[¡S™á:ͪj¯ÖaV¡.ò G©RÉ{Ó=ú2\‘$ *е‹Jeqh½Õ*³ë‘3wÐ^ÆEÕPMTÕœnl³— k­*€í6Ƭ ß^à\‰6¿ÛæcÂÆ,‚kgJž/Iž8gÎ"êX5 ½a»JRÅü&±Öá“i¥«¯Xçl®²ÛëGRҪˤÅv±B&ÖKuZWsµÃÑG:UÙû5¡¢Dý¯µ#X,£%eΔZ¥×‰¾/+ ·Tø»RnT'ׯ—ŠËäb©BÅ}]V¬”6z™6¹%|~Õ;¢Úê ©^fŠÌvXû¥3wÒ&àaª ë:lÃ*w»J€ºâý³ØM (7ÄO Åvü~·…ÓŸ'Y /oVBüDä,K¦pwÜæh­ã±Éüéb;'“©yï#æWDÞûˆ½k;8DAK68Â9 ¨`w–E%A¤PàƒÕKÿ€ú óÐÀº~…ÒÀº‚d(Ã’á¬Ó ÕóìdVçëÀg¨ó¥†:Xê›-ïÉ—?͌џb>N“˜»É<—âœuoøŽÔQ{øŠõ°çnöÜzwr©N x§´»d—[œ-ÏK·óNó"™Ç)x“ÐÎyÿý¤iuPDƒB‡΀h–]”¦KDå½xH&,âIÏç£Éí8úF‚YXx¶\&-ÕûÜž€Ë-&1§ÔQèË¡/GI_ }Yš÷S ,ÎNÑkr> Š|Vbc)æ]•{˜9ës‰³¼k-Ì_§ð®µdþ$ï™*ñQ‚‰‡y#™*¡ƒ-!½;Ä–âú‡¡¯ãº&}ý4©¿ã¬™wwâÏ v˜àX$„èHHêÞ!û>©±RhËÉ;HÍ)IWV84J%+š*›‚~G¹³\[„ì´]*ÄëÜcÙ.ôå62–Ͼ¬8Ã’­ÀÏ›ÌÛZLÄYD`B(â3ˆ'Š¥:1~LÝÖó=Ïœõ¸#¼«~ç*æ+´»¼>Ã>ÿUÄŒSëÓ¶"µR $ˆ¡\N»Åd$:…J:…à :Oá´ Á»KˆÚ¢úì”Xe8h- ²9¢Aú#ºYtм>Ãû¿N•Âûw¥ írsÝRÂ|6  ürXóB?Ñ+ØÑœÔ§’"àbó•†C³…In)Äuf^"Êô%}qp_#}ɾ¼KIá̯NUbiªÂã.EDÆæÓ~Zç‰Ú Èá²dÏyŽçŸÅSeb°Ç_ðx"óPkß+¬}=á=ïQ:x·pÃr\L ‹‰÷“zV ÎÛíý$¶?#~Òö¿¾–N7Þ)PÀ€¶ÎSE"®÷‰A,äDí‹Ü-D¡mŸÐv”´ýodÎ6ò å"æäÌù’eùô,Ð.,T ŒŠD¦|y?%j/ðJÒð‹ÌÛûdÞnJËñ¼EÝDòäÁ*çO߬ةÀð„ƒ8BI]Q Þ‘»¢–_’Ð:[#“Ö%ˆà åäVÒ‹ÇÛD sUCúú Ê}ÝËÛ(½Ø8¶5'KæåG ï„eKþx…ç”›'s²¬x€Ù¡X1 'Π×ÙJ‹áüêÌz3/yj<2«é•ÂÜ û&áqŸNËñ½Hy‘y×úÄüy˜•sç΀LÆO‡ z›:ŸôyžfëBß›kAUFÃ9”Ü®@ߥ‡‰N“¾ãÏ íZ)Hsd¾Ëèß#Z(ƒ?ƒÄ· LåŒ^‚~eÂJŠ—øf|ç‘øæ‹e9qŒOóqt ‰qsÊÿßø°¬“½úÉÌ<žÞ3¼¤ãšL$y‚¸Cä{®^ç^Úz‹Ä€xfël†¹+E.ÁÿÛ²Þùûs8bvÖ–…­X„Ìš³(b+,´f>4sŠö¡e¸­UÏ…àý-é¦ s¡óqšY6ƒyLöUº™îÂmR<‡áÛ^”Ã,pšÃ40&òž‘}v ëqÛŠçÐv*˜Ï1·ÊP9˜Ë<7À4 æµý·YOñœÚlM§/ˆ€ V‘¸s.; üÚâ÷¼Ÿâ¹´¿ãŒ\ðÅx®ž0k°Àø­ìŒOÌ„ûRG&­^Œw@?||?¢Ï9 ‰tìRùA!3XdÞÞ'óvË0 X0$ÒÎ òèë?öŠçÐ×½¼ã¶÷B^œ³kþ³8éOpÆN¯äç.HñœúóðOùóÓûùE/U_bX:ÊþŽŒe ðè¡™÷˜ãŸìöÓfæ=Ú̆H;ð™èÓßdN\PŸdöÓ–¿¯ÏDÿ€‰’º0?$ºlNãnxad‚ªMôlà!LJгí<÷¡Ÿ¼0÷Á¼‡þl¾|æ¹Ô;/÷Á㣟¤KÙÇøñÑOòÜg¦ú“sòþ‡è•ÜnáýaÝ}æK0ç üiÔâ i†ÉGFÓDÇÉiõøá€ ÿ0 _”{~=£Ð·ÿæ¶ñò1î'ºgæCX‡æ ’‚˜`,PÌ üt) |ûIØû^¡ïI¼÷ѧf¾}_––ÍâVðãÀ)fá·å¤¯XÚ"Ò¤eÔëÈ8b ï)„­<ªyàð¾Ì•[x_‚ÌÕ½ä}Ûb[”%?,$è‰ëÅ|äÉB`‹ãbèÐ=Œ»I8óz2îûf^€~l=cte¬žš‚ò G´b“vûôÌëÐöîY» Þ_Ö¼±E ¢>F“™ÛäY`”ãGô@>d†#{Ó ë«_èÏ2ÖÏ̼Š×yŽmÆ"ØfXrß6Aº—“àY“æIòxM* Öd®? …¥xÑ –èÕb‚ew1áN…óHñX†‡~Ž óTCú R<ôs'?O~"Jå”L–‹ÎÂQÌX26±ÄÎ~>9¾°Ü…m9ôÃy>ó<ôågó$xWfbÍ]&’¸X"})EÖåù륋ò÷*œlÞZSãp,ê骩«©«M•—E#·Ã]¬Ñ<*ýÀ3¹½&±Ÿƒ¿È ŽëÄþ$1tçOš¼ðÅ“&iëà/3wçÜÀ©³›'^<ÕßêʼnÍgO  7ÆÅ{Ñ¡‘Þqt v]-’v´ê¶Úí+G6dO²§Gz7e¯¯º¶6ûá#c±[kw¬ÙØØ8uóààMS S7 Þ<ÕH? MA“ù¦™ÚGF«n…fVn„f$û²§àÙëc¤ü7ƒ,ãâ}è2òêªkë²u¸)¾é­ [o"çãâkýTºº9ÝPo…m.aèÖ–’b`MvËQ ?Ød¬*ò‰Ä(^WØë‘À*k”ÈhNë„ð: ^êçY[ߢeYS ^ÓEÖS¡ÏwSÕÅ72!¿×Äù9ƒ7ŽÄ(©3IÆ­‹!?‚o½&o´€$‚oubzÔHØl Œ‘Ãÿ\?®kBkF²3I”e‡´>[Šî¯ÊNØÏ®üĆq}cöáD'³¨ =mŸþ-úyvc Ýë8Oé϶?é?fKHeÇÙ•ÿ™ÀMg"•ézÚ1ýßPyS•PšnBkG²Ù$š©ÊÛy?/ò2Ð@)ö¶Ó4M2ü1Ô V£ |Œ’nÓj´8ù†Î‰41 ÖêT ‡ÑªçžË^½š»)ûþxóêúÚûŠåÚ›ž¦™÷Ðûè«þö~ˆ\ùö¢ô·>Þƒê˜W Ú“PŽ´•F‹6§™ÓníȘkíÜsïÇ{Ø?ÿXÆle\"ÞŸ>CcI˜ÓE¼LT›'g(˜™9gdÓ¿ü4(ö8bèÈß¿ eΪ”8³bŸ¦à3a†ŠEMÊ‹&(-£ð"-­eÝ圑û4ý+Ñ—ù¶¹Oóm£Rì™-4¾ ™¹mPhæ[Ù‡Ð~’jɤ4–E’ä1ça!ø½JSíe5Àr™aULƒö»ºw¸“Vr—u3üý—u½©l®£ŸµÛ¼è#î¶AÂG—¤"…!zÞ¬ûˆªNW8ªªVÁ‹_ðµ–è4y+˜”(©ÇRÑrn3%Ç3ˆ± 4¾*ÆjA&'‰[êÜïsJ™—x8ì¡ÃiY $-eÉ=oÜèizjú®ÝÓûèð»`üèc˜#9¿ šS ¯Y´±¶“Ož:õäIÚƒžÉg›Î19•=Ns°'ÍÔIòwF…°Ö[‚rù>%ü@`"yz8$€“0ŸCÔ‹i£T9H&MX¡U¹É$œ0«XiI6!hñœg§81ËåH¦|\«¡UŠ7N ¾‚oZPUJÝ 6AͶI›ú—t¶·µÔ×&ãåÑHÈå°–õ.yÑìD'’9@¬ ÌëAØ´XúЉÊþ·4n\iðkÊ×ܰfûý‰€L£T¸5ÕýÕÅöÚáê;.ÛoO ņR~\#ªœ¿Åzm`ɶ¶eìÚªÚF[lmw8]ÝZ$5–˜Œþ˜Õ÷÷¯èò5•Û<¶ù+s>6ó![ 4œ ÏV/¥zÌ·[8„1—ù¼Æd'Šrzeá ÝªÓ¨TªÏBè%±S/Î/é4Ì"éµ,î¬2Z‰£=n@wŽ?s¬'е©ÞÚ`}Ì^Œ&*—7y«–íjjÚ½¬ÊÛ´¼MÛƒ‡õ›º=Çž•·zlC×U[Œ,7b¶½%©ÝßÒ·o‰Ï·d__ËþÑzÉ[6óǶ\Õµá±C­øìÞ¢Ç 06#åĹ´„£P¬á˜ç¼.¡EQØ~¥ !§F¬%øNEÞˆ35ûîSÈš‹ÚŸDÞ 7® ï›rš²÷„Œ?”—– ŒÄ‹7OA'²—3»77Žßn×9³·¸P‹F}¤¸¬Ù×?È˽3b&à,„ñÜC{\ “K‰MôDî"~ÖWÑÀ¶¼8?$θ0?dá6Ëe‡Œàì®XO¢k{·/н¥¹qi­ÅÇ»ù©±ö·4ìjçäƒ óÒCò{f «09$ ºþ‡ŒŽyÇóåÝ+ y„ðY²…Y®nöÚ(ÿ-¹1ò€py1\FW¿ñX{ûÑ uu޶·ÛXÿ‚µ2íq7U”–V4¹=éJ+úÁågÕÖ:{ùågÖÖ)•"™¼ÏÓà̇ô—˜/€L¤–ÁÌ4ç^i¡aiŒ$G‡5­³w¹³Ï2i¥µÔë. Zƒ¡€K¤›#–§J<˜›‰Ì¢‘G¦E}% îÈÊÎHtx_GמpuÅMÌ~ß_JÜI¿a ½eh˜Žh îΩ®–ý+c•+/ïi¿¾•–”dŸ¹­¸¬Ñݾr¤·%Øy>¤ÏÀÚÛAŸmÂ^£ávïǬ<õ\¬¼Å¼Fç®&Ux5q‰³¸…F].n‹Ðat¬}Ï`¤s ¾!س¹¡asO°¡~ 3:¸»}I—%/--K:΢¶ÆÆÖ"G…Üß=™ØgµnéN¬ióûÛÖ$º§¬Ö½éÉnÿõÃÍÞ¤K£q%½ÍÃ裧²$ÕÕ•*©ôùs Ç›þ*ÌQ„JË1ráV2JâXsבdÇ \‰¥.}ÁÅdþëLZ…(—C¯+RRÁ61aÁÕ$P“U'¿î ?^ؚ娑A÷5Mõ…Ý©×ËŽæÒ÷KÓת43º³¦¼KÉc²Ôò­Ìkþ®M ±Ì`¯ÿ=™ÂUÙîæ1]`í?û¹œÚ•VG9šCÎMÂmŸ8rÌ¿í®ùÔù\Š~=M™DØ $Â[å9N¨™Å™. ?y!Ôn*+iÿv€ÌëŒÚùø\‚†´ZŽDøº› Ò—Út)nÚŸÄQº&±_ì7Ákºníº5÷çͲ2±îÖî[áÏ-øßÃÔäNô1s}ø"˜wÒÿ5]J¾Š¿o½¹äþ…Øðgš9ñ·¿`ìe%/õ(s;ýMá9|¦\Ì Ù_Ü@C3¡$gq»bkZüýÍMÂ÷(­¬RD³©*Ô5Y=0Ð^f±(•„IúoâõpŽè¹µ4· ÖX"Rski¥¨´ê’b·y*´†KÞ„á=GV®ÁºÕU½ÔE= fwñ%«„É7K‰oBÝSÂÍÜoÄ-¤˜ÌiûÓÏÈJMn`Y;̺o‹¬/µë?ÍQJ4iÊïó8ì=FàI1-f”¿£3¦Ó|8#:EäºÂ•tQƒZêmkòŽr—7ªUáÞ¶F‘Dk×t–B%sÓ¥%¡ÃéÏ «¸OÎ NÁ3 ‹«êÑA%¯À,ocæ¡hÒAQׯtŽ©œ…E$•«Ý:ÌÜòõ/\Êö;ˆ&9û³KQäUÙ³À²»§ß“Ôk-5ˬaöù|A_°Â[Q©Û¥= c}¿>ÖGµ±úq¬ƒ´øÿc­ÊŒõ>ë7¤›ÙXá™DÄ¡äO?´Š|Ú9tªŒõÕwHQ$IÙ‘Fž1œL¸ù´rWbl‘")ûÏ^*®ú9ÚÔXS.÷{=(€ ™Ø³Â÷AØ£8.xTiëý9Õ^M·*°Í›CÔ‹ +™éÒ°$ ¹ÌlÚŽÉ/¦7²öN2.î¢"Ž•••»fõs~¹X‡ÑPŒ†²`4À`ta¤¤`TIÐ÷ F A$*˜¥Å9ŠÅÕ’` æ´¯®*‰c¹2éþgÀøîÑaõˆ+¯†ožÿmXaž¯XÝ!Lò»áXà`¥’w³lHo$þA|•òô òñUôcE“?’È Ï»‡¥,ÒL ޔŰÊ~W”¢*Ëﵚ©Lå°œòI[Þ÷ ùñÿjßé¾÷ —rŸÕqžŽ|Ö³¾?¦÷í…¾/Õú.O¯»1½9fvËnvåY ˆæ×²ËHsh¾#M€¤‰QÛ;Ú´qÂÔy‹cÃgö6³WøkÎÞJ>ˆgøQáîS¢GÏu£[›õ\7È’åê–¾'z2~àBînqÎ\ÇÅg¡°®»°ÅböŒ†1[CäH™Ïa†nLÊdæ~VŽ:“,Q/çŤm”|9q }2ù9à·hqÀœšMÆN²ìÁ•¶ÖȾ$ðg~¥zä%-YH<þ\$‚ºðÀÞ^ª…‡iÿòÍ¡úâÎÊɱÑÛ›ªŠÊÊÆÖ2¹JÊùˆ8EŠÈ­Cè#( ï€nðÕE3»Þ)Õ,9Ùk* :…éOÜl¤è5 Ð3÷wŠöZÄ‹?~mÍù‰8Pw»Íó‡@þÈ“ABíiqóÓíÞh™ËYõsr›7Zµ>ášÍ–âêâ’h±™=kŠÍ,žµ‡9m>if–ëÆµBs`¤ Œ¸ 3»åET‘lãDVÖ̼’ÓnE–ù% ù%«igñ©½žH¼\EíäÄšø¾¡ž×Íln;zè¨'Ò$üÚª yö{CU!ï-‡k:ÃvÀ¥Wwû˜Nëð™™GtòöNù8Ž¦È— ï•¯ÃþxDß+_×öJÿw¯''|•å “ž5QZœœåJö·ÚUÙ)Â_îÍo½ï{ttsâ ¿¢ã›û­÷~/ñÜ&:ñ«Ä³›…:½;ñé7ÿ.qûÿ>ñÜ.ºéôñ?Ó«ß>NˆBüÉùëâA`¨Bã sí'7»ÈwÕoc|;¿$úË‚¾¨X).Rmo0Úû¢ÏãâNâtE¶éö‡¨@@´‘æ«Kªx)-.òÏGÌa^±R{Œó•î Þî…mhwΗcž²®<“`çÆ£—˜ß:9ÑÙQYá~ðŽÛOÞzó‰£wÝx×u×^}èàn¸ä†= íÞ¹cëþùýç6¬_;=ŸŒ©«úz;Æ;Ç[šëk£í•íp¶û}Έ+b1I$ꨱÕU"*G41ÙIWÞwš÷=ô?üžÿ]÷æžà‘¦¾¾¦#ÁžÍÝ9ß–Æs¾JÁœ¯o[s¾ å¾ýêÙÞ‡úæb=½½=±¹¾ø/Ùß–\Ùß²ŸkVzÃï)ï›këéëëi›ë+Ï~&Jò­ÄýòƒÒ)%Ýd+9Lî#Ï“ï’ßóF²õýtý4/‰ÿN]Ò¦œìúírN¹žš¸÷vp¼é$5òÿø-ŽØž{–³“¯~ý}Gî»îš«¯Úµ#¾eõd3ˆ*eÝåÝÁ@…Ï^'f«ö0]Gç*žé#hŽ’3Öê+å}ð~vK캥º‘ ­^”ƒW«àMvÅN ngååÖŽX)¥™;9© ªW3õB#]EYmèÁ†:( ïd©ªrj#¬”)S«ÐæÆVåáÇj­_†ã‘"Ð+ËÙKªýÚ«¯^­ ˜ÑP,¥±7QTÛçŽ4G·lZ.2YÍfc¨¾½t°Ùìw]}+ØPj+ & „QÞèq*öêšjú¦-ÕdÝ@4Ý$/ =ôµ½îpc FoS´HÐhG)]•xÑf£v‡«¾¾ÖUÒXjRÍšÜN%´ñÌÞÒ¹Ë.z]Áµó{Ú(5 VÅRë,2ØmÔ!L–ΆÜ6Áç*Y7¿ÐÆ ’â´Ö±·À˜JžÊ;vݼiºµiݶC·|=íiÖ¦cµÂPý¢ú[Euµ#«I¿U±Öµ±6©s4Õ&L%·MÚæ»™FkJAh`Ú“¸ÔV ÍÂD|8‘T³–zl6Ä¿¤al-Ö"VÚ ®n§àZý oSð±0°+xìÕÑj8‡·&gù!ͤ #7ׄà·ÐL]ÍW!¢<§*íìÐB—9›µrM%Ž*À¢j@W`ŒÙ¬ŒeÆ¡?K\×î(·=èšEIÍAçC¶£ÞNß¿[ªð‹2þìzÈVîh_’¸Ÿ- ôd›]²=è,·E·4r¿iܵ…œÛ${½-q-¼³>\QÝÚ¸äiŠGíåP^%hwR_á~ {ˆ›Sa:’áÛ}Q/U»Fcqôõù•-»­ksËVInØ|ã¾y¼Ãîó<•5îX`hÿt-æ ?š ð_–I©×í\«¦–fÎhrQÙ˜Jƒ%Fƒ™“%#ËÎ)râfb2¥T2(ÇeåÛ­†_‰ÉHMñe•rJÆU_yy/ÚÉÃÈËëËë‘2f ðÖE©#ÄfâÑÌdÁÁr—¡jÞåH_aØClò˜#ÒŽÆŽŸK|™ö—¸ñn?婇J‹÷ï x¯ ˜½~êýÂîÜ™» þˆ{îs¿&v\pl]lûŒêîM$6ß³ØCŸw•ðó‰PåÐ|Gû¦¾rzÅåW(‰&ó K6úo·Òïizvú/²Ðk\µ+À ü;$K§«Ç­È2ƒ|OiÈ=ŒgŒô4. Þ5’t´[Ê¢ÝVw¢¥õÆÄËtÿb× /tI“ /vCk‰ŠÄIâeý®Sí PGGQì—êýÚP€ÁÄQ«á`w7x×ÀüµÖW =³+8X¼ÁQÇ;BÐ5trø/ïízñÅ®Eº/qRþÒÞî_èZü‹e…{Ò;¥2&+Ü‹zò%=mÓ>ÏôiŸ‰}è…@¢ÀEµªM©èÛ©èÜzÎ@4ŠÈ˜cêùy+Y,ËXâAÅ*-°xß%LG¨{‚i×nEt¤¾¾¾ ­\ÛP¨*GÌÑé.0`|· cãÇOý(qûmÉ>||üøó?LÞv{âG§ŽÏ ^û‰W}ôè¯_ýÄ5ƒƒ×¤ž¯äŽgÆçcããÇžÿaâví}¦žþÌòŒ¤Ç[¤zÓ†¯0LJl ,γ+Áô01Õ³P»|Tl´ÒBê§TßÚ8Ž#èžÄƒ†&è m8†Ñ‹XÂÈȼv'§EžNYž‰#ÑšÎöšáè0ZnÈ®:šg©QgÉÁçRz᜖Ÿ]aܱ”eGÊÒC·ì¸X·ôà­<ÑÌ<Ñ£‘ô«= ‘L:TK:ˆ9$Ž_DÁ\àˆO]Z2¨Gkª«ÊË4ØKËaÏûJ¹Ž*yÐDºø†Nqõ ³øp£â/៸Ë`ŠV^œ3éšg0,6Õ.柉ío÷‘ÜyLÀ14ª…¨ÈO¯™ ¹! Ãs¾Ž3(CTù!#œ%:„b˯É€ëš~²íÙÛ6¿eóÄXOWkKsS]´dSp“9óŠ.Tñdñ³Àd.íWQD¯0fe„é]XßaÜæt›áКjîÙ¶óÒΊöHMGÄ%™êK×:š'.Ÿ¸b:º~vt]åè…k×vGªÎÒ=£ž3ØM±žU}£“7ÒáÑ’¶hQ°:jö™-¡® †+ÖîíÛ¸à÷í¿ 5>TíF`€¤,øÖ“ÒNºH?yŸjéïën$® ¸,<ãQ‘Ø`9‰3r’q‘yÎÈ/("Ç·›ðL‘‡^~tNt@m,\þ_(T!®ÂÒt2ŦÆúºšªÊŠH(X²âÒÐ2õ±CvôÓ'¼äˆµGB1Yzߊk@×?šØçàKìtß=9·Þs qÏ=‰nk¥­¢ÍÜs`¿¹ôÔV^ë[wÒ`âgô–îDˉ5ôÛS•&ôÎÀs5Y Øcªw-5p³֕м\»»ÿ@µPÛ‰S8ÃN"ò/ ‹F„Œhž¦/h‚ Æ‘B“Öcâ{VOY|gãj1p»k&ÇG†ÿ›ê2 6qˆ÷åšæD„×,s:ÑMŒtžvÐgS‚½Áho¥Ã_ÛªˆU—9Jì¾îáµõc×ln©_ñµÿ¹³1ñå½7qU8Ýg[†D¸ÜlöE»ÃÕ«j½¿Ç¿ÆW[îj‰žX\•Ä/G«ÖÝ‘xP2ÇHîºt’Q²žlRgWwr,á°À‰œ u«bÌ ìvH ¤$sxŒuwÁ‰¾~lý@×h÷h{ fWaíÜ‘ÚzܹM¾T|u" |Ðq‹R*iœèvÅ:P™.EØö_È*œ)˜ÙØX$ ±µ±µ¤¡¤¡ª"¤ŸÐ†'4Z …$™¢Ï(ÅÉòYwüµ>½½Ì—øÂÑŠ:í–­O ŠxÓÙðëZ‡?ܺ4)â¾iF¦eÑArèe/ìðy²‹ì!{Õ…‹··UŠ™ñ&@&xIe0Qp8¬pL˜·Qýº×7l¡fuz z R²gáÂÝ»v^°mãìšùéùÑá¾Õý«êJzƒ½ìΧœ£G·‘ôiØÖ©á–nMÉg™bÀ™†U:c ÞJ+oÚ[G¯˜iÈ7®¬Ÿ98:¾¦(ŠÆ•µEkBÍåöáU«Fì¡æÚ-ýe“ ®†‰²ÞøÊ =ÝÉl0¯ ÷el0÷ƒ‡Îeƒ¹‹šÌg1•ð9û¹‹Œç4«®Ÿê,£C(¡£ô½ˆqóØ~æ%Â,ø´Ø’Ì€OG¼ñqJƧǧG‡ú;ÛêR›ÙXÚ<¢›+7‘v¬µÓ¥yiàõ1¥ÀÊãμ¯´Õñ@Q´Ä]s`xìŠõuucñÆÑöQWE[¨gjº§o:Úש¢+ÃðíÇ÷¨R733Û0rùL}ËÜêèÂp8D š‚NONά[XyYÇÂ0³`Õ§û pRR-¨ÂmÚØÙ,,îçf DA…pèP¡ÈŠ ûÌËTæébæ41dÉ/p-«BD™—E´rÇŠ …+ÆU?%k׌ õ÷õö´ÅðªvE¨çË$4›;Ï75§gåÖùh¾¸rú¶ Ï?•/»œ™9SÈ“ah<‡ËOTåÉ4ôjñsä—NÒGÉ´:5¨öõ¶ÔFJ”cúé ¯@ö½ä„}HÈèÕ Ë’Á_ijlÈ–i PLW6ÔÄì/!YåYáö™ Îtež¹gøä9Þ@ À–˜0ú‘Ñ@Œq3UˆGeœ¾Ø+Ô¢Î$Æ-B y-Hé€ÀÏÍQ2·}nûôê±ÙñYÜí}½Ùòw¡ev­ WæìôBÂø9¨úWÉÏôgÓîåòùÒç`—V’ÒoË%‚B;·—­®˜#·÷Ö÷ÕÑG2BAho„Õåõ“¼Uà/ÐáG˜wà¯d#‡K¤ ôÍ&Ó™ò(`‰ ¸sE~!«VvØžƒƒ(ÚŽŽâ…^kK®€_HñºLÀ—)å+Ï*òß–/òŸþZ®®ž¿úêÎ,ã\‹5­@îYÙK&úîê*r˜ü\u_Müáë†ûÁxå¶ ÁdN~]–„7‚ôd4âhç8¸8$¡f+5)fÓ~üúkz…/;+‚ÎÙÍã—OGíEa§>Ï‹ùÇÅ÷'‰¨åXPúÀ¬"œÊ€GôÞÎhížÜÌq¼º³Žöÿø?ýxüØõW÷õ\s„u&¾÷®mÅa×ZWšïÑ)áfÞ#=Àìà™„É³{…(~‡?¦HlLðL"ä òw=÷Áœ(ŠäÏ_E¶zÆÿA"ž‡ïœ0+¡xÜj¡4Z —–`/–zk=5SS…‘ù]Áòá~ <µ ­£ · Ä?+å !³ÉˆÑ“"¢½Žji»B³Ôï?ø©µØ½÷ÝÖ²òÄ:ôY’Ž¿e3ÿÊëI¼÷ Þþ'õö«Õ t¦™R¦}ÊÚ—r! má-à AM”G+ÉHy¹åî‹ÜÅÖŸQ®~é»Òq÷WfÛ[Ð>Àéfñ _#ÿ„Á žœ.&oœ^ü!Ál‚Ù3â K$Z˜R€ãÇ(µÛ>1å‡V!žá’¿ÅÚ«`í=–ü6yMoï Í´!=¼FšVË›v‹Â¨¹ü&4Rtâ&9[Ý8sѲ0d‘9£¨ù>%ÜÌ%%-ÆñS)üdžq¿–~Ç<ã¶I¿#—ë™Ç*QY¿†ÎFSc M«2…ŒyXjÌñI.æèà²e‘P.–2ß8ô¤‚õý,ŒÍÅb›–«AæƒÎñúŒ…4‹#7_Œ‡wn‹ùç8.ø¸³4êõV—:¥Õ^o´ÔI:Kk¼¾šR‡£´Æç­)u.}™¡¥à·j|¥á]î÷ÄÄ‘°¦_•ogðƒg€®%ÂïnÅÈàW¡I™Z¢'ÛÌø5:9–²ÒáHÇbçÄW¸X[ð m=Áÿ€õò{öafaÀ6ã-"Âí'ðÄ ä¹ñt¬Fá0¥°È}A:¢å¯¡½§íùy’¼IöB=H*ʾ‡áFÙg«X˜N‚Žåšo™a?ç&_AÔ"ˆˆÜ^xųèú¨xŒ®³« aTxŽl¿Ë}Cþ:±“ rð”’ÕsQV+ä–^—ìïA!û‡”8Mf½ƒÏzB“/]ÜD ’Õ§UEªk¡=˜#³å  ²–ÖLY4üS7dýW-€ AtÁ¯ìÆsͽ‘dEäÜÏFÚÎýçêy›ÃÆ—¼äívøüE)|·òQŽ=&«ñc&øsé¥ìßËLF£éQƒ± ôuZÃ_Ã÷ÀªJÏÂ@jê`ççEk¯L|ŸïItMѯ1¬½?ñ<œ»Å0¨E¶"§,íE?)óWö3§J 9Ë0¦$…+ –Š,I ¡‹™"T/Àü‹|@.Í@¦ƒ4(Ùê|,èŽm©3ÍPaäÓÍ® ·¹ÝRÚRaéˆD­ãu­[ƒñUY‘û›|uå®â’æÁʪÁæ@‰wÐ¬í­»áÌ3ËÿÉö<Ã^À³÷é«Ê%lŸîP.Á³ïü¨°¥Ã½Ý-MÕ•Á€Ïë´3Ç×r®Ü”ÊmÑ!\Èm€é°\Â…Ðßãױȫ_?¢åQe[8+ì9¡4Pä°Á9l`;˜&þ˜¼ÿ‹øq8ß*µ}hJeÿ`˜A7±'»âÏFÂÀz±³“á÷¬§;8-¬u••_Øô‹? x1æÊýÉ>~A¸Ú³#’±“‚îbUY-Ç…û±5T´êÌ –JÏ*a¯§èôkîb¨›à®æb,gH ˜ÐiÎB3Ñfôšw`4 O¨‹-½Ê“Ûogôñu ·êôñu8ó^Lþ†5 )wjRî$#«Ÿ®CJ£9wc¼vÝoÙI™w-#\4¯~®CúÉ–_ü…4ÕúãƒJiVO­úkU›¤ —“Ýìkv‚Rmuuš*ë4}ÉÿVúxÆ—œ ÿ!oýél:’Šªôþ¤ä°‹ëœ0Vôî`ÿòzÌW ∢/'>wƒä<˜øôª‹}Ã3Œ_A– }Î8Tpz¬ˆÜ0¨>BWì~ë OCŸ~Õ“:ÿ¸ ©`ô¬»˜:ˆÅ.ƒz½ézú¡›[¯3䀚—ѵGméªÄz¬§&>!äç3:4 k§–5q;ƆÒç%?àtšþ-¾¦Ð®º™wn>£Ž<òHâ7YíŠ$¦µë ©fÙ¨˜/•=g‚ðSÊ õ`“{±Eį‰D”»0y 1‘ö€ «&Š*Ÿó‹œæCÍZ#më  {/²ÂŸ¿P9ñVâ²Ûã–¯ß}bîW¡»D iûUh€ýú¥ÆÓcä%¤øzV@876æ(²üŒÞOëÎb©$<7šEñ9òÏÀKÿ‡¤°|µÜçfÑô½.'?;)˜6p™°H«)ý¶'Ü”¡øMa $œ¡»Ä;Sd¿¨?‡Í`Ž|@ˆswHu¬ßZµã‚`€M&‡ƒN–ô6ˆ‹ºP ‘‚ÐjÑ÷k½ë½ü8a¥¿cp §U ÇÐë¸fAØ÷ˆ‡<ª“QwNOs˜Ú7ˆ8Aúç–î¾óÉÄ/ ÎküÛÒeRðEª£ä¡Ó…šæ"=N„K¥#â=•ž?”¨ºW|`iÏûĺ™Ä\Åo»â‹lï¾–<$]ÆÅµv0xÏI,¥ÆPº˜¶7æÛchéµ{éëïã>pú".ž8:óïÉ+Nþ–ù¨°ŽûŒ´žŠH9y\‹»%³<üÓq·Bè?Å!_!K†ÝŸÏGÃTžh"HXn%½…ƒRŠ/+¯FÓEyVxžˆXV\^Õ&ÅÅÅåÅåDÌaW<Œ$Œp'jAàXtKó¢ÝQ§îK©)?ð]“-YÁ.Ÿþ¿ .U熋àîÆ‚pY£G½+-^ .ÏHÂShÇQ-nÀCÈ ‘0BKÀy ˜ñ‹…á^6Ñ´b0CVJ\^ þ’Q( —„Ë‚>Í*Ä€ÃnÐb(¹ Gôi8ÀæúéeA¯Ë ÁÎáïÌǃÿ¯æ*;ïùf­íòù>›YÜ‚óÕ×7™¤Õ‚ƒ¿WºÏ|™à³ä‰ä“$"®"¤.Œ.GŸ§.ò‚¶#Š)š×ðSNïˆrÀ^QX'S³S´Ðä éøUætBùj")T‘(ÞAëå᫨Äó‹ÃÖI•YÙy"cQyYQØ% mˆ5Ä0µj¸ÅL8,“{ẖ_ŸcQÞïds¤_ø¿cŽ­Í+ÌÑs¼Nú›£)gŽä(u²u„õÄH\UÚ¥œ9¢Ð¤çZ2)€é’¸l’j˜HZ).+$/+WË›šü>BÛ›ÚaÀ5á¯Ñߘ²Y+U›ü½`[0GN-&ìþ-!#ê` &õa·RÈÏžA×"dáRAòÝi÷e8ÂÑÚšòÒ"ŸÛiTh#×(³hþÄÃüGÄ›I=櫳bŽV*Kå ÇÓ¹Y†-}ä°‚™½1ë HÀ8¡õ¤ÞÕQ验x Þº˜£º‘öÓ˜n„ÀlŤ ¥s­âXLvGÈñÔ¢§ø¾»û*%þ¡n´lhqªy"¼kÕ‰]w‡=uK©¢¯y]G6žÐhv-Œùus=iW[k«*AäÃÄÂ|EY©IÐ,ôÜ[Nãäâ%Û˜Ví”Y—ÌŒÖm ½)áÕÂâxf"‡µZŠäEV •ö3ˆ=9Øì¢».é,€íKÓôï«Jå_+W[nyºïÒÙV‡sÜk±mf\3¹2ñn ãÚSèV‹{´"wT<ÕY…"!{T¸õQeÞÇŸ¯ E²F•ŸÎ陉»[ÔòF³É/Zë&«Æv÷nyuÍf«Å;æt´Ì±\KW&>Îm;¸i·+Ëá–Yº@A¸Eig¨@âº/'žž c꺪É:›è3Y Ö÷Û°¾ûþ÷Ç‹´XßïìIžsya€ç61Þ­Ÿ|\õZ©‘¶SÎØI.ÅʼnúIÑf€ §À^2³¹“,ÖΔÏÚÔNb–ÐÈ-fªŸWÍ8ê/ûûz»›²¸Có;â5+˜FZ-Ù¨TG;cçd¹?_mòš®Ñdûgñá|ô¼HaQ1Ý E“|Ô•„38ù#B„'ŻӼ@ü«HˆžŽ˜Oq”—R;©˜þžg1²¾ xD£ ôÖ§¸Ä(«¤¥o‡*ʬ‰*Já ÐKg«‡±9Wyä Ã!JêBáFZNË®AG¨Ò‡ Ã9âKÜÆ?$'e¤œ:Eõy,R~»ÃØ3c.2àêØÄóÆuöJj­‰êÚÏQO›D¨¤ŠÚò†P ¶L›D.ú0æ¶Ó“±ôˆAЉ5EÝlï‰]ßé)¾«iÇ»wNß9¼û©æÝ‘+®ÿÍ\Àk-E.¥Ôß߬cGlæžKTËé;„Óœt×M±›Î|q›ÇdÅ&[½žÌŸ¸MøºxñÁø0ϸ”¿çþ>½!°UWqÉ0µ»†ÉÙ¿%HQŒ¾ô™f0˜Yžbô²·[MòB=ÏfMÓÌÂ˼"/¦ÆfI E>«‘‘3›)=Å–T-|O <5Ì/«Ÿ['®VMMi3šº`ê‚­[6ήt i±}ŸY÷°U¼¯ò¯ßò< ] ˜žQàRöÏ¡”F—11Z†ÑaAæt]8³ ©‘)aa±Pá”Øæ`iœ(éï…òy,fYÄÌ¬Š­NlÕÔAU©ìàN¦¡¥NM.ãSÙ+ó³·[Ä@ñLúÍÍÇ¿ÿÜ–‡|òäOÜ2uò‹WÛËŠlUåN¿ÍH=ûïß±ãzz|`ÇŽû÷÷|Ó<è-Y£Æ¶Uˆ—¼”¸ÿCTøÜÞ½ŸKœùÐ]xáݬˆú+jeEæ}åÎÕ«ï|åè¯Ü95uç+ˆ ÉŸ'ÿÈÄÕi9ÿ_tÏ’2ýîam&ßS ÒJæW„—j©¼µé\®ÅÞ…êwÛ¹/㪡¦¦&Zqhö«„¾W éU»L°RÔ-qq-0%,²àfÜÙÒ€¾EaI£=n—#õÇì¯s9bz=(ø2#´öX{¤ýöçß|óO¢o/­»í¶;øzú§„ñö«nOüéôøÔô®°äüýÐWG0ç¼ÛÆb_ôߢLCÊ,¤ ®‰4V–Í®PØ@˜öÞávƒÐâD=<-¥ÌçL×â°ñX)ówëtÄè‡Ãýű®ÛËû6wF†Êpp Í'Ëz6u<Ï}ÕîS»zvŒT:-‰‹>À].®jëÞ>RE¹jMǵ:ñ,ÿŸüOŽßyN¸Úú†*Š…‘<šAòB¥s°Üߨ@IWGƒÚ¨fcº¡¦g%uHã9Ÿå[‰¸Î½hÁ­­w µ]ôðåýƒ×=±ÿã×¶í¾c‹³²ÔÕTå ºÌïß7‰À?ûñs¿ŽåmDÄÕ'N]våK'ÆÇO¼tåžÇnÚ`¯ÓpœN¶l?¹yóÉí­­Úg ®õÑ$Ç}*yÕYü^}©„[w\‰ù¾Or¶äËZ¾¯äË€ÓŸ&âéÕz4nÍ‚ Œ3 æSþ—&ž¦J~÷Ò¥_AÿA<=­ç”Ô”Í2§Šóº­ú¯ót/î9èëWLÂ]Jüé­ÿ(8R§ñw$xá“xÏyÚØ®5`MN É·þ•ìPMM,èfm'h"YJeiN§egÊXv/îñªŽ`˜qs‚H“ ±æªPM¨R*Óç:µô_üÇÄwã\§púN¿õ5rêñP‘ –`˜>ØPüPg 'húÂj¢ê¸æ60òOç3œJpèÀKẼ’ ;ì 4• =»t\-..â¨Ú__*+ª.®öy]ƒŒ©©Œ©õ9•ø÷›ä#šŸgòX¡g uÝ Ù²{—çåÔÌ4Ï•·“£¹¾qr±v6•&/ôŒÚÈ ù´êm99 yKêî Z“QЕ¨MèÎ)qT‚Ó0ÂÛ`@‘ÎhÔ÷'ðÂ,&¤ô Z:v>5DÑ¢U#¬p•íxfµv ¶¶`žéÊH¨ ÿMh ÞFÛ¬YÖ൴ړó¨ªZ–ꨣ*c"îõµÇ^|whÔ¯­Š>œ²bß/Úxrg¬çÀwî<Öjª””¹d’žÞž¹>Öxéлî¾I¸C3#§¯ŒÏùÌå×¼|ÇšÉÁÍ•ÂE¼fVNé‰M¼ºf,Q3{ù‹Ï|a|`ü€q;!O¨EUN=TàÛ('¸©‰ëéx£IÌ€Y${9à' óšîÊdÒqÌÂøpjN10±sÖ03¬Äz™$ÞáÎJVõuŒtŽÄšëë¢Õ•æb¿×iW$ÒNÛÌ,!´~m”æ2ÐÆ²ÖÂQ _4U/ÒQþ2—Ÿs®úÈâì»v¶uøÐîøÑ–<è³–"1={}{ӥ÷ÜZµÕáºjõ¶Á£Ÿ;|às'×övþ!Äú:¼ýÿÜ8=J ~nÇÉÍÑdóáòÔèÕîL=ä%Í[‘›_ú1 ÑýÑ¢žÝ½oÐîuÑùÇ^†¸oùö“â¤7 í¸•YÝ?;CÈ©çí §%Š+eE‘f1Ÿ§{s6гfvWlÂlšš^’¥Û´¤u›UP˜(²¤À®ÑUN”ç •VkÏRù¾é úÉiuD‡vOnºkOwßâßÌ´®î ÚmVóĪÇÕ5òçw98n÷gþ|ï½~f÷Öé9ƒ¹¢®bì–/\wø‹7Yƒ eVóÚ™‹Íþ©ú N…ç,ÐßQä[4~’ÚÏœAûî ±ôS‚ßet€·‘Ðë…í^w¯nÝíK›i SS]S©‰D¦#¯>‚Z5hOšŠ°ñn­Ð÷:*RfC.s<ôñ{t A°½e_¶-9ôQ‘écäÍ>}ç!ëó¸séy2£®uS¤V‘Ù© ²}€0 ôˆˆ™NtŽÌ=Ì̯5Q‚£«úÚZkª0|Ê´k0¤äÙ›x˜×ðoµ>mZŸ9¶ñl^ØgÁ‰åÉô·Çr åÓ}ÞEŸ wêvÊð¬åh[ ¢]qäËè#X•í®{r»µDᩜm˼³ì•Opïá‡yæÏè§Mžù»wæ§þëôb:Á}Có_‡gž)c)Ì|_Ñ&5åþ—Åáw¸4ßW¨+Ô™ÒëßÊêß’®¯E ŸÏ®•©_kïf8×OîRmˆaµ„i;U$ 扢JSÊcä:,d!Ù° ¢cóŠ–Ä'¯†ž€¦PM’©WM•¡ Y þºšk0œ$á·êå–±Ù>õ…‹6ß«q‡ÇZÔò‹É'Úê&«Æwõh<ßNäù.Ðx¾uÀ^sé‹ïZ]¼mz³Íâw:Zg/íC¸Õƒ”v‡x„t’-êÆ@1¬´‰R©ö~Þ1 C |ÅÛÊE”ljQ&éïšš~Fdwy&Ç-F”R;Ig{Ìq o€ÛmFï †šZv%7OHÓgè^ñÕŽïDjä9}å…žâ÷4Ì]»º¬*k‡›‹©œHš|¦”¬ðÓ§„úHÃTg9G]̺ê‚—€-7%>*)š”mH<Èòæ$—“>øÒM&Éfz”¦|f½×*ÆÍÃeŠ “]”7)­MU<1E˜ˆxØzÏUNbVJðã ,+”’ØIŒÝÖ Õ‘b”yb’(M„dTo Öd«Aæ™Ý´Þ}ð[’‰H‹Yõ€¸™¤•«…Ùe¬`Q,‚²˜ªN,PÛrÎÚÀž£"3ÞÇÚ7çËTÆóµˆÖ®™]ÕàZÝÞªê ·-†ú«xD öo#LÆ€_sDLå]2»žU‹íÔn:+Y²s™©Î¼¾GK‚†‹7l\tû8[IÿØú†‹o xFçv¶>ô”»H¹bfãÓÖëž§7<½ñ3 V¿‰ÿíâ®úµ½‘Ï.X‹ÌKOíÚ®^Þh Øžœ¡¯øzJ·ß\A:\…:£ÞK/èXÓêûâ3zbí·T$î§Ã”V'n¢¬¥t)Á]vðª`ï¦î ñë'¯:8Ü;‹PoV¼\/ÿ3ÒLȃª»½ÍÁ›Œ!*šÂTâšðšZ×Á4a*¤7˜†›RƒDç5 £ õ›L:Å1SƒÁvޱå5ô FcŠNåׂUi…£¿»³e uÕ\ᬨªåtXýu®v]+ÖKõuÉQJz¼Y;ÕC³”‘Üj4}˜¸×åç«Zœ;¹³­ç’P[æ´¦ö¬ÛIù 7´5^6rë=¡ùÄóÕb|Âá:ˆš²—øÜmkû:~¬ï[i½ð®ˆDíX £ùƒ,ÀÎÏ€‡ù¾æèñ‚´²y‰SQjF34dƒáwaùïq à™ü$Àð("™GHk–©˜GQ, n¡æ6œ_–¡~ÚÛ—.¯6ãÅ‹°E@ZÈ­‘b>Ò`ŸØaR-¤¥ÂYª¨¬r}Ù”3½^-­6nÙ“K4R^%Íùj{+..ñq憛/(ë"Td•5ŒvÖ:-E–üƒ%ôˆXªŸê(çé¼ÅwïºÜb;^d ´¯iyA1h07%¾¥Ÿ­€ÀOó?% äaM­ë*§’è£h"M¨o’™°QNåü_uÜA‚GÉ¢suçxÔ¦IÝð8 =f(Y$¨a' ™Â)V>U–…V©©ªŒ”–83 ­Ò@rB«T3Ê¡/çÄ©b¶»¥”>–Gàéò*8otUëÏ6­®u‰>³¥ºrý¬¥ˆ~4 Z]˜í]ëN\s8×x-Ö ÷üT608Õ'Ç}\´‘^r©º¿$ìnDÂì¨ŸŠ¼8$Sª­9àÅcóí6ÁÌL˜3 œ°,mâœÆ"R²,·ÖEkª#á’bËne“–œIë—€iN¢ªŸ¦qˆ}ëtaX+‡väÒ§2§hg}qe±K1˜Ä¶†ÃCWNÀ—Û<ŒrûVjIüÁä£ßÕOÒ½}}V§U1ÌÕ6¿îËéØív¹iƒÍ™ø ;w.@h¸ßóß!­d:Ù(0…3H¸Æ¸;Ðâ9dL †WÙz¶PÅ0âXgÝþ¶¶¸*ÜÕø§ÒèÇ p2Ïf›¾Æø•x7êj¤zômÝ;Cí!î?-Æoz¼fŸÛr½·~¨>Ç-1F'зåà䓚|š?9êOéõoeõoI×?§œL&ÑÓt½®“'ðÌÓÕK??»Þú¯©C*„ éÇÅk´¸½ðÌ‘O?@ÐbƒíeN ‡šm±‘нªëÇá}Œ¡å¦tÌUDD3 «š§jLEéãvÅŸsÔ„øÔ>Û’ø÷DòÜg[вàÉäoO÷‘bÕÇnq†°•}TÛTŽ(Vƒ:×$>ÅÝ‘|ë\óÔùlòíÓcIóZ`!´­G3!Ä”.f_âÜCɇ±þ>ŒîõlòÔéI´fhøŸ¶f ä@’ãî;§ Í͆~¼ÞÈsqR‚1˜ ¦ã À†¶!xfh.*˜Œ;#ðVxàì@ጣ䋋[ÆêjÇzƒåU×ZÇê£ð\ZZM¯ææð8, —Ô×6ÁÑzOÜ’x{<ùâ€5ME³EИӋȒ»¦uå,þ–жC‹E+G¶6{¤ëAlV”#¢×tõÙ­H'ƒh–NHJ’­Çí‰ç¹$_bøt{ò%ŒL¬§Gp¯Zd,n^¿´*´žK__ãºå©Ô Ó3Ì"þ%ÈÚqñ廀]ýµAœÂ0w¿8ÈÚ€g˜‡Ú:‰ñ2¨ÝCôäèÄOtë4s) `‡1û…êÕË‹øgÔ©XÉo-`ÿè1úÊYŒ¾ôP5ßmM—,4tD6dºŸl¶¿ýQü›¸ñú©›ŒébÂÄ%“O3†Eü¤KÉ«Þzí®Èo=@Öi³K‹7¾iÔB5¹Ó!Øí#P¡¢Y³ÃXx Âu$?ƒûôÐgš !o=HÚ5“t+Ñ0#žš §`„<Œ ÇhïÈÒï¹Æä£ØÞ‘G¡=/´÷Q"=kÐcN&«èyßO¢UV'PìÿÄH£³Ì¿#Æ’Xâ“Âåb˜T“÷i÷,¦bŒ§’€_ d¾Ñ,ûCÈœ cXÞ<¬H»r1Âéñ…ŒÓ÷*…J,ReY òH¨È±Ù\N‡]¿ ©¦Õ¦¬ÄUÕ ï‡)`0ˆGÎmÇVÉæ zj¢3Su渫_xaéöQÏÖuek«'JÒdE./ª oÃK/p“§ÄÍ-=>ái+k ƒhÓá`ö\Ç>ò½¨Ú£)1¡ç–Oâ¢üƧ¾éÒ|1Fz¥< ØI™—g*òƒÖD©^‚Íò©<Ͼt)µó`'º˜]0•b<]NØKÅE0>?Hì!Å{A9Ý!Ãö8QX*ÿ ’pþ;¹2øÒû¨kqgW>(=‰¸¥à=ÏxùÇÈç“ÏNµÍÍ(½ œ´ˆyV¥}°¼(5oeá 8ÍD¿È_Zâ…Žˆ+¤”Õ­Üîä? Ý5–jæ¿8t±`»~¿?â×iÍê¾}Z»“Ó.GžºþYlµŒŠ2¶Ì Y”÷It¥–Ñ­Uv×£µ¹#¯Íç ÍquDiÚºÈĬ‹xQž/ ‰¶ÖÆͨÈßYÔÉ:1§l,µ~öe÷ƒ¶ÙÉuÒ¯Y|ߟI¿FùIëQÁxš‘OŽjjjêYã†H6\|yð~‘|(ù,Ʊ×Ú$Z>ûxAøY{Ì ;ÝÞÍyí½D¾ë7 öeÆó‘ 0PY\"‘pyiIÀ_YTÉ:0fãÇßçµÿyJ`¼8¥5¯‘î µ"‹Ê|ú†°@7°÷¬h.–.›*£[”Z‡óJ;ßAËîwÐòù5G½µ©¢*kS0‘ɃÉß‘ÓpÖ€L«¯¡Á( ¨!Ì^8‡ ª««k°KíïMwhޤñ^ ký1á‹`œt½ŸB8‚uŲLÝÕycýäÑä§ÈX?‰Oùá—å´”V˹3 f"‚•jBPê/¦ Áwì‰Ogê¦ö;}£DF]ÈJ- hÙÀ‹Äй{å=¹° ß¥ ÉgiTCb›D•Ö Nbô‰ÒñÕÉ~¦Š\A%ß0{ w¡ …ÊÆñ†GÛ# ÂÊÒN«)›mÔ¥­ =ù@$YY¹ ¦Rkž_<{ûï¨i¶"UÞÚ KÁòÙ”·6ß/~šL¨£ e\†À£©–¸ˆ4^¡«ì.PkÀdT¶Ô4§(¾1ÌÖ,ù6Ûkæ´ŸŒÖÏ?û´~ÊÿŠ~j£ÑÖÚVGmN?úžÞ7Ÿ×ƒOÑ×4w=W”•aj2¶—s9æÌ+9ï•såZî•j¬gøcdVª&£l4±KX£lX  1ÛqÎFœ³BŒ&Ÿ†˜™7™lÄš‹ù5,É+Wu2k°}‹ çÙ9θ-§óìªgëý¿Ó1£ßŽÁ¾ž¶X$µäælΣã$Bž"· ¾…–7ÞŸŸâ괭ꬥ&Ã5›:¨dÎ!6ö¦ƒ“Lé3ƹbw¡ …ÊfˆD?1›$“YZ¶Ïh’Œ Ä(›ŒÛ‰lÉcÿgI>šêƒì„Ê&¬½¸Bm3]©º“(4yáüÁRÔç¢@õ•Gþ…Ú·Rb2ÐËß²2KgÔ'Pä–®½­±¾²Xì ?Z¤qHÖ²•éàÒÏKž"{Ô‹Ò¼I6Iò¢,Ô$±¾my}›ÍÆY«Æ^f £®¶È?6¢ôvw´57ÕNÖMæÃ‘Ò \Yè!“%tZ4­NåJ¦•¦žæ)UB‘¿£­¾6O¾0gËwæñ§Ÿ!·jrm¨”“Eø]•΃SXT¬M‹Š˜ÖæÍä,òËd¹UjoFîê:£R˜:¨«:Új*u9Ì”-×É[òú{™îØ1ù޼IºÔv½™hA„Sî!…æãÎ+Á‰#äÀiXUÓ«bãÿËÞ—FrT‡VõÝ=÷h4£[F÷}®î–´ÚKÚS»ÞÕj×ÖúX c0‡Á€ÍxmüÁæN >Œµm>×úƒCÂ'ä!`B8Âa¼3¿^UwOwOÏH²M~’ØÞ™ªW¯ÞU¯ª^½·Ûެ˜<\`Á®57Ö·¥Úl6Ígç…k/)ÞÃí!cLë“9ÎS“l&QA³¼,Ý@ÙÝTÞdm'scœpñû ^!cäö¿Eèb_çŸý›ìÛ…ÿ#Þ⨎Ð{PïKU—+œ(!X/9á²aA<'ñ‡‘‘ŸÈ?#²·Hå§¾! OˆûJèq³ŠÙå1-ö4”Â}¸©Ó ªdþ÷'.ÁTÊ\…÷Ü\V­\ž97óE(&u°¯Táê-¦’¸ô°XŸùg|AeüìOžSYeU—½ÿ_{s™žnÔËÎþ[ÖÚÍ;õ^œxìiô>–õ5Fvp3¥¾½, ¢¼ì  <Ó†,P1›ák¤»`ÚXXY£5Ù˜ %H$TÈéZ¼¡ÝR/ðËoÎáŽ,y'²(û4™ƒþþ§Ñóì˜Í6‘Jóƒ*rf>ëö| úîLS9HqÅ>äf×AI¨î›L RÁ=Q@Vfþ í„!Z|ÑÛÙ첚K´ƒ4¦á1ûö(ÒÙÙÙßÙß`ÒȰ Ù×xÓ(ó,¥ÑÔOÿ?¤Q£›FOP{°ÕE£é¬€~ºð—O¡Cîa¡HÀgêÂ&ð ¾À²uÚ¤§}~·1"˺,‹nâ ³þÂÊÆèCÅúú!…‘¯U“…xv’lº:Zš›ÒõuÄ>ö–÷²]€Ý>Š{\6aòé§ÑݦdH–Mh„˜2QrÚH'@Ï–ì³®ciË(x4‡s£¥L~– ú7z¤¼¬­…¸ö]å]l­‚s›ìërs°ñt2ó3fÞò4zÜ9—:–ÎÒ!D€¤’ŽÍøTNËØ‡Ü”سS@H¬ ­„Bgƒz $îÏŸ íV½¼»»¼¬{°{°¯Ç1;?ãQöµæ÷S¦×ÿò_f~ðÔÅc~Åur’èäÏMô[:9L\eÁçwëdÀ­R!â±Ó\î5k„ÈS¬õ@ ¡HçéáTËdxÈã};úwx¨fØá§‹S.ÿHÏž›ý»+‘Ü»ôüST÷i!¸DŽÞ²Q¨Ö ÌÅ{†ÌMy缸ï&¾ò»÷ `œd·ÑãÌ|T¶ª=†…$å•5Z멼†"ÜMtûA©T™Ý2ðžwúý oæËá}ºÖ‚¢+šÂSDš"(Ú²×L‘º6æ9§(}°gïíšZñîtñ¤ÝaõÞpÿžò¶þ*R4UYZ‹Ä}kvô"9dtÞÔÛÝdRÞû¡ìÍ9º#Ý~(¾Ùð±rL¨4?øêcùé¡I!^@=Œ+„a~(DÚ‰Â$]'”ö Ó€ÔÙMC7AÕ–Ö%òHó ~;™¿û5Õ¿¸Ÿò¨„'ŸL?}°s°,1666=6Mœ;ë‚ôþ,ûšâ¼ËùÇâÝÿCÞMMzñî à·o`·{ùþ¼®mÆA©pд{ÛQ8(ÃËd±Ø]…]«CyÞw0Z@¡EncÝgÙ¨Áîp ¬¼8ˆÀÀ9‘ð%Ù“›uq! ‡‚žRb¨oy!°|dËÈ7«©ò²ý ;ç¶ÎNŒ õ÷yx>QûÚ¼§ÀGö&øˆ©•ªµÆ !•X{Õs£àåuáEn“£»K+×è_GÝQÚ¿À*·€žþ6é¥jk¯sýk÷,°ÐÅš‡‡zºÇ›ÆMµóç-¯ËñÀÃfÚöVøµù¼˜ðbB€ÒÂb3A?§ªæ‡B$ô$å†À4°„ L€…7 ú šº>9š#kÀƒ!»Us2gSáÞ6¦ä1T51ÑÔ8±yb31Ž.F…è¹söµkðÉÚ#þ‰Oÿ|š™*À§õ­cd¯‹¿l®c!kÛBÄ ‡ ¬ca÷ÅÁ`x…Ã…²9Ä<³¾QàÐÌ3—ŠÂìÉM¼8Ì0 ¯½–m}AÀ ,fMÓSd9Û7?7uhúPÁ­$%]éŠêCw’½±}/+J«ô’£pa[K:U[m 8bæþ(ñƒÙAHüK¯IåVdñšÜ)²ß©ïˆCô4QUU[%®Ÿº QÛ Dm+HiѬ ägi(¡X@Ü\QQ‘®h¨­Ž¤Ì¯FŒ„1^Â5ÞãÜE„V[ô6ICª¶¸¾Áª«*+¬|öy±ý?ê$ã }È€¿.¸L3Öò$ágäëÀ¼g*2¤Îä—èÓ2¢€!›xoŽ“±ÛZRÉx]¼¶¦>„o}Õ=—ôV{û#—ßõ²ÌæoãÛ¾:sátòê·}ë®»0?{Ádõ›®Ã_á÷u-¾iO÷áù‰’Êw¿~ÿ  â-7`)9²¯÷ÊKÎ>õ¶KMì»èÕ(Çéƒ&íˆDIFÆ–jœ@Ûõ- ­ä,ª'-•-ÇFz»]ôÔ >eNÙùdŒÅág~$£9}›É­"#XeVÊ¥bcÍ=™ÕèX1T‹ZQ'D§ù]ðaBò–"ÔÙÑÞÖÚ\_WZ[ZS'ˆA ·õå± GL&¥-æÁÛ‹“y,{ÎäRæ¬É¹)3ëÝ,ãÎÉŒ06]Ã÷ܳWR~Ñü:ÌÞØòëÜÃÄÞœ£/ØòëH2'K¼ìæE‰´NOŒ§w‚ÎNwwnêëq&ÙÑŒº³lÜ›\ã>€>MÆ]ÔÏ ±•ll~AXŒ%Ž]^@ÈèG/Ýêƒuwz ]xì‡Ì±‡ú9Er ÎI ”)3&N¯œ‘ÄÌ,4>6²ÆØ'\cŸÁ$cÓØb[>#:QÅœ(ÑnƒéŒÜù“ Ó ¢õ}óÇ‘ŠÔ Þ®qÙOeß.|V¼õ qзv, ˜X†RáJ i@ú§ÌœˆçøKPšOÆLOäX2ó¦tsº!Ý*Ç!ã7¼à—iÞ\¢-š(—–ßêÄæCÿ!¸ÖDZx³—àæ$nL ûÏžé¿Ñ–]këWn½ü‰­·Àmþsãëà­cÂmÿóã ®¢ny ¤änçEn¿âÇÏÿbzÆH®uîÊŽÙ§ªËðß~ äàjoz{eüùßï¹~˜ûN¬~ËuŸÉñõAg€¹×¡£Š¡£•Df~À•U†°(úÉØ[hÍ¡àTeþOØø)—Ò!r+fÎ ?Ö/i.ù¦®»‘5—:iÊÝÙ)d„‹C1³lÒ,JuÛ·sxûþí »æ7O ôu¶7Ô¯+.‹x·-@ip#1Â÷uæææÐC¯}õéÝ IN"ÆXÄ‚$¬B2®UτۑÖȶ —Ìî|ßDÊ6Ã;ñ× *¸¬æfš¸Û¸ã¹1s«Gîp²ùÿ•fðÊž«ihƒELË™ŸSX~TŽªQ“ÞÀrˆðžRŒtµÞx&éj"Cú©$]F ç’pïÁ÷ÿãÛ2#ø oûÇ÷üäÈËï87ó :÷Ž—œ‡õ‚»õö_Þ®XÞÇ~õΫ>uíìóÂì[?}vû•t^'½r¢££=× µUS´Š9YüÊó„ÔÚdŽ‘í• =à¶ U¿õ‰¾ØRd€Á+}´ ·••-•-¥‘2§÷W÷!$ÿÜ€wƒWðn‰¼¢ Ùö&iCO’ä½DC.Øz›Wd5aÇD3ŽT‹xÐû"âǶ™ú Á#Œ’4·dýàÉB¹h¦þ§¥Ý¨B¸ÍÉH$iNBŒRIÎÇ-Mó[B†ØÀÖ÷àO¼åkwîßç×Þ’Ùþþ†Žßv8û½ïeßv|ˆûw°˜ÈßÌŒ¯fzY>;'£Æ»ôVFáõáao³n%‚y›ÄÞ«ßJéë3^¨ÝIteJŸP»AQ^…2Ps‚4÷*¬ 1Ï {Yr`bâÁ8öc?øóQc_UhŒûуæÄÃZEô±ZÑ1dŒ@Y€ØQï1nrÁ¡F³útŒøØò7Üã¨îq0&{²PÀ1 *6ÎCdØW•ƒ>¼SÈ4ÊzÈ®ªÜ=5ΪsºM»ÑGÉnT×Ç í`ËK3;ÚFÒòG‚ÔÕ•%k,Ÿc¬DÞœ¿ì4ìåÀ%y±8k0†—¦lÕûÚ<Þ?‰ÞCxOö €?á½âþÆ$@ÆÊ¥%‘p "Xa æ·ËÀ‰¼±Îà2ÌĨn½ÖLT¢±1 ¶a‡n¥Ô~têmXR>, ŽLclÞEáäêÇ0 š9‘½]iVÍG²¹1€6è•ÉØ×ä΢^Ju§$Pzê΋ŸÛ -ùÒ‹Ì);]L½ÀÏã:¦›‚}ZXž¤ÛÜ´Â_ÅÃÙÓxŽWúX‘šÊ9.PÙë² ý «J9–UÞ|iàhènc½bëd­–Afèy]qÊ#X ¡¡F2qp%¤¨ë飩÷]ëóG‰¥§Su5e‰HÈÉʜܜ—gãÙyr¿úˆo Yˆ¡Ê}©âÐï%¡-M©zðêK¢ö`»5fÐ6¦p‰ìË“Ÿ¿ÿɃhŸ¾»>ÆÁ¢ä#ŒRW -My3­®&òÔXv-Pf Øÿ†yÚÞݱ¿aŒXº¡¬]µ5×4Õ6¹V*‘§tý‘¼ŒS…†ðØK–P5ö©CØïk‰qšÂ[hØ×Šíë¨wë˜W뼆¹§33ÈïS|~®ï} <÷|Ú¹09¤î3_}اðX” øÆŠ†û”õ)möÂG:¾a„bôrÉŽÈú0ziaob‚œîÍMÉZ¿ Ÿ°ÝGnƒ±6̲ˆD’r2’6ë°‡hH~I«S_“B850XPµëÙOà¯gZ¸ø,â¾p¿.r™ ñþË"Ißëø™êí[ëƒþÙ¾ÞT]¸¼4,«š(ÖßžÙ{Kfóíbý15vBP„ȉ1÷w$*f';g#­ÇäÆòX½âSŦÿò]®u£üCúfp'ú-ºR¿Üô}’O‘ŒG;çÞê@4ŸõnhrÓ¬†¨êðP_og;XŠê±š1—ÒFŒ5í¸Î¼ÒðW‰=x¥™CË£úV¿âãÁ!̼¨ (K€Á«®LÔ•Õ¹æ€m=ËóóÄ{¸z²öÏëÛa=¦+ϼ-ZÛÕ«(§®^U°ÊiçHÏ[3÷Rš5¡¯3ÏJ+DMÊ4jRà÷²²+"õÈ·Ò“Ë·R¬×ºó®Ø* jÂTÅ™eþs¹Ä+™ÛxdiFæ«…Ò¯zàʽÂg/˾]x%}_Sª×U!‡ª<ÇM#‡6HSéY…&E÷"ÊŽ£S…¤ž••Õ•Õ‘H*Ú@Ä5ì+kÃ}¥I¾”`>@Ïìp$ÉÎêúp2R:Ľò㙟|–êò%p(‡SÜïúèqÜ×ïÀã™¶.î»·d~ÿèí{n«.Ã7 ÙÊøÙ­ø³g¦¦ìzúF·G|È#è-°'YyjúÕ±gÉ<Ç‚ú©ª¨¨‡M2^¾p0z|†J·Ξœ ½œIcYÂ%û~Kö? ŸÈÃù~ô-t£±º‰…)õaá÷ÀY–‹âÌvÈtW¹NœÁá‡ü6® ³³§Í󟻞›†[Rà ps·RlK-!µ?^›f:Ö·=Ç:.xîzËëÔg£sż¯™ÍðN\4V¨;¯sà$å ·ß¶¨W/Ó©Ú(ÖD¬Ks°ÙîºQ|ïuÚ2é}¶=ë— {Sé'¦Ñ´7æ§œ½&K‡ß·Šü!¬ñP£¶¨ù :ÌÎú-#´. kÔØÚÂáþ¾ž®Žö–ÁÖÁ†ú ‡i’EÜÌ5‡á 6{îÿæ{Rq"óWÙ§±DOØ c$âéJò§Dþ\d^N‚ì°Ê²ÉHŒg!ÝN‰ëÚNÕÕT”Å¢‚Y¹ón c]aÄ÷Ü@Ç ñ=8ú‰]4«×ç†öB.E¿‘Óc¨‹‹‰%ÀŠ‘w|gö—Ÿ³ýçN€dñ 8N<)âe/³‹\ æ…ŒxÁº£«^‘lTl×´1ëò±R Ö—¤šÛ›S,ÏçZ×8Oây2Î^}—} ~Ú¼s Ö+4Q9ŒsÄsœ¾Þñ±‘Mƒ½“}“l@_A36æ טg0ä`Ôõq瘰6`ÙÌ{RÍlv7·3sRøs{`ƒÿy|+Eøï‚_î <47Ctûz'ÆrsQsqóé!ìc·4£›8Er2ʼãÂÊjQFZsŒ²õq×X÷㋘L8ç¥;ŸœaͨŽnZ]‚¨±Žß¾íqæùhòQ‰Ü*ÐJÈes n+pÉŸß¶ÓÉÍ­v9Î=›c`½sœÊžû_e޹}œkŽÅõs’êç¿:ôsˆ†ÜúÌÓ.²ÛòP0è^ŸF„<[HPT¸wô «h}K3QÒá¾ÞæÉ–I/5þß•TO§\zÊö·½z×Z®_ž[0G=aNeîØÌLâKÝäáKAv7óýNvßÞÈnq–==+[5W¶^4Yw>k4‡ iΖH")󼇔ãùõ}(Þ߇¾-$X›>Þÿh V­Ü&Cȧ ªoÙs.ÞlÚß,;fÅMÛ»»'W¼#K/ ýaUÜ8€žþ6R}¹£…ÉÜ¿VOo²ÓÌbMédmÊöt |·ÌMž~Ênî\#˄͇Ãoppa¢0ùýìýl€óùÌ…ˆ9Y„ˆëÓAyj‚rì&¡› ù–Ö'é“Èvªø´À⚬ɇ QÞ¬!PΜi”Kå===C=4ù±ƒ[!öV/óìšü²ù—â׿û ðë×ĶXS©û³q°¶â/<:ŒC·v HHE–É´/±QºÂ…óØPÈù*ÙX.˃¹ÇÓvˆÁ• ²ƒ>ž¦ _˜=¹‰‡F¡H8ä-+6ˆúÖ ù‰ãô0¡ ÁÀömÓúØH_Og{K³—ÏPbø Ñ5mO5úøø¨C;7!XvÍ{Mƒ2ÄÎ$y‹Ú°£¿[=×PG9  Ðª¶„žÜl|¤—æ[Ǻ6°fׂ [iUeg{cº²·ª×R¿@ÊôÛ©½ÜSÐ^:ö(øuŽLz²"Hé µc3¡§iæ‡BtÕ½é¹!8 ”Áœ % 1u>è'ø´ušÌ±mƒ^l¡XØmœ“Eû;X“Å`Su__Ueßpß0±•nv…©/²^^ѽ֟xõÁ«Mƒxµ¾uîñß8Öµ9&9\`]‹ä-%8Š, H¤ðÂ6ï™gã7 Üœ fÁ…cÃ@{rs/4B…#ëXÛ¶½h·æ¡A²¼mÒwí,¼ÀÅØÙÕÉÎòa[~{y’}uöa”Öë¡9·Šx̯²÷ptIô©bP F"b•q®~BÜaç"‡î÷ÃPýH€•«“e‡"ŠbX ‘½¯Èê&“3âºY 5.d;³§Q£žòcN`YÎWyì‚iI8FÜÇß8S¬nš Î#΀Þ+CÉ_—ÌI˜ÌPXrÌ-Y[YEB>Mlzª1ÇìÓî0ËsbÂ…[‰ Çn%²ï’ž…ò ÐÚ×6°Íô*B‚\¥Ùß8Q[ކßãÙÝ¿*½²`Èþa›+òHúþéÛÈîkm}ŸÌ¾žð«Co…± ¥‰ pÎy…CŸªˆQ)‰ÈU¬&¼5ů•N@½U:¶QµÛŽyšV»L¦"}CFÙÔ£d*>úØcU%ÉöòD[] ñ˜åíɱþgæÇ2ò‘ü òqç¨q¦q•  ^T9fÉUÁ¢mç\süiæ/²÷¡¦Õqd>ƒ5ßR'I1ÄAIg'ùu6Ÿè«gÂöÒñrCóz©Æœ£:KëzÍYº†¿Z~ýÂxÿ$æCp!÷þ‰~…E!e¬­÷Oö†î6Öû§JDøeĉ˜–*·áe€¯D~…ˆ®ÀããPÔ\Ìk CV1`xÅÚz-BïÆxŠ]z¼3s ¿ÒàÁ;sC(ûeâoÔüÍÎ3Fõi®œ£wŽL§¾Dø2ÆîðmòöP^?6‘Í‚8< UL‰Í!È; O«axœ]NÐ ‡CÙqVëA$žÌtÛ>pHKKBeá2Ó¨*UÌŽ®§Ýº.Þƒy¯[ï r””°QBÞ¡îPø—¨{LŠQu'ð~”ù6Wmæ²æy:ÛûÜ!t‹&®³ŒcŽf$+,K)D ©Úe?9á CÕb’X]™ˆÇ¢¡¾NªcÔÿ«åÏ9åÝŸ}}>O.œ{°Šà“"š,ª­@©O ÉŽ(Dé£3¡©SŒFäVs X•ˆ"dÿNÖ&JUG…ÚØÙ˨þλh<‘ùÁö*¨æŒÍ3ò*²òã²ÌÀr¿@„)–n0KÕ¬ ZñnD$Ñü˜(Á©á‹‹Ñ{6Z 0+ZõÚ·"Ö½ãUÆ9^)ÁðS†”XÖ¨àÄfd‰ó²ß§UÑá¡H6´!*éÒÔ NÈr’ÑK/-:hßhµâi;È–²ÛRZ=XYY™¬L²+VV/îW ÌÓ~¿ú_rž¶yfáÿI›Ÿ1üìÃÆiMö)ôùGSX±ä­ i R´eKÝ|TÝTKÏ­ÌÕŽw³nheÝýôÎb]ÌœÕvAÕ+$±¯·½µ±¡ª"^jhz­T‰øM_éjªW{\z5™ù7ÂÍ·:ôªªÄs<Ó+‘Ú3Nî&´†52+¿,+FüÊ .ÕÒƒ’X‡Ä*©Š¬ÔÁ] ‘½=y²ç¼ïü¶u´¢%c; ‰d‹âв…W…ê ºÓï€`¤“t‘v¯£%íz‡hQ;,ÉñMއ;1 T¤²EâÞþêÆ ^ˆ6U¿/sy]ª3ßÿ¡dm0óáp:=óØc“4ñ×¢1ü¯‰’³_j‹†{Ξ,p ùt¯XÿÜËÙKÐe´ì…9—2Zf/d³¦âÒÝG ÞDz•ʲ•#ê!1þ7dNò£†}boJ iü‘Æ6½Ù b,™!M‘K"rL‰¸jz–ƒQà& „v½…ø»ÃÝÈNj\8“ $‰KÀÍͦYêžÌ)û|M˜ø Ù¸ˆ}-š,‰ò’`yÛ´4ÁeGÙPù…n½Óº¬ûï2èi‡ÿyü!fkíÀf ö¹F•—5ÔÓê][ à›ƒG:x¨L/uÍ:÷Æ?™‡Ã)ôK¼p€³+B, I«î¹Õkóq0äÎï þáA½^›4;°f6C&ÈžOb‰ÉìæÆF§ý5`¼¬Û£”ô…W#,;'–ìƒbÏèc%~ìªJq©j+é¥ijÈòCWë\Jëb˜•( qÅñ:RZ<;Èqú–ÆÙ{¾¥¬+Æ¿÷¹ï£üW­%Ï/çì¶÷\ˆÝƘÙí:"Œk›x¢²¸â˜di#ó«mJ%…yaö—²Ê|œ )įP«-üœüüjfµã Ü8 vÄ-?”ÎØÜDƒã‚$yÅlESJZȲéú7$JëR‘[RÛ ˆ™¹Þ?“yŸô%Ã~†ÍµÚoq?³ßâ˘ýNÂyè´1Wóì'8£aÈã™Çײ2Hù]ÖVÖF¬7Œç3ìÑ6²~ ÌïÙ£U²{B\E$­Gï$ûb‘ø="WÀö³­CkKWGKok/\cÈ5m½aÞ™Õ$,DãÀÝ fKKKK_Im<ƒw³ Þèo‰!³ÛÈÌÊ6PyP­÷‘ ¢cÞç¹`>BaÒÌ|FQh d‡@Fð~×Ú2;391²i ¯«³e[ë6Âç¤Ãª} úÚé#Æk§aéYêÑ™'ÛÞ@²ÿ¥Ù*3áÂûqôK‚7dò%:JX¶èM‚ÀawM ε.8Oâš,Ý•˜K!ˆIB¡¹çÒi`Õ9ï›\°?Kk¡ت¬ðEgÜ××ÒÒ7Ú724ÐÒÛÒCÁzLa³7 ÛÀ:Má&¨åè®Τ>@ûåî+»Ç[²÷¡o°ë°z DÀ̆ùwDãV¬.744,íbë†Ø¸.ˆÄÿ%b.ò…˜KcDwºZºÒ.ÝÙ8½Ð¶Ì©\?ƒ^dõ¸B¶gy¶¿©udyvËÙmNYÀ_ÅŸ"û‚*ãV³…ˆØÌ'J¹[Múٹ˞»Õ´7t·±n5Óˆ—$.Ã!¦$R\c vÐr ò1ŠðÞÿ8dw–‹ô‰Ñ}:ñ÷а1à‹´*gc¼Ñ¥ë²ÏŃ¿¿èA¨î2ÑÍÙ3·÷Åßðððôð”aésàñ ŸæFÜ’ƒÿóüž À™‰´Úà“É2ü÷¡Ô„6¡|ÆÈ†8ŒUykêl7§H¹lˆìkÉöuÔ»uÌ«u^Ã\6Dœ”TM":¤Î<ŽAU΅騆‚ªé• °ÜÏÒT©ÐuÅÝUêT¤o”¾ ñ]8¾Þác4å¨}x{ߢ㿨¡YÅÒ½»çwÄSŒ± íÌ)B3°×¹Òäs›àøõ':…ƒüîõ'þéC‡}èŸN\ÿÝ|rôÒ÷./¿÷²ÑÑËàÏKG¯Z_öCîÖÛóð<ü›ÛßùÛÓ\pú·ï¼ê3×mÝzÝg®"nÙrÝgøÕµ“"Zúv—s½&~Æ·²g¾ú-ZÖšþ†*¨’°€'šŒ š¦,À…C”ž‘š÷¤îÈð…Ý»vlïaøÐÈ!C5ƒÔwim¯{~ýc–Öèí*TKÀ•¯ ç8ñ·'¨ÿÑŽt´½ÏÈýåÇ<_LsáGF"»Vž7Ó5A9AD°K–9úÊÞ¬—À+ô\„ ³%”`<¿AÏl¿¨Ç1Ú<3=5>Ú×ÓÒ”(•EÔŽÛUÈ1hTÓèêäM‘¥%h‚|i,‹] uøÞ€XY½Ø{àØxï¾tënS>wŸüÒÕáÚòPwc¼*æÇÕnq=»›«:ûƒ¿ôOÅ«æõ¾Å”¶úàÏn2EòæŸ=°ªùª’M¥ ­²"ó‹nͬôy¾Ï2v-æÉª>ž·Úö÷µ¶ õõås­Æîóº|>ñîn{L¦~(¬kÄéC…×µÖ–ÁêömjÝd¸}–î´ºt‡ø¥è y>ï±§ÄO•‰+Ã^úÓ< ^cíÝ áQÔmÕØ™7§Í5õÛ™Œž†úaHC>Qó-Z~¼-?~~nÇö|_>àôiO¸té þ´w|Þ®“ò!þgðg³øÿš¾/4s̃K ü›ýzÔû~üôµ_ŸÊ>€ngуDÁdÒxÅ•'KÊä,`¶*õ>¿±{“…U¾ÞK‰^=8ÐÒ²w÷ÜŽ-›§&GGöî·öxÁœŸ*Ÿôâ+“%ÌäG £ú>‹¯A‹¯ðœÉ·À>_ä[[.½ä‚c‹wïÌçsÈÁgù.<>Šôš+%Fhx%»roƒ@‚(­©R½ ®Y1¶Òì$ˆv(@­(¨­ûg(0ì¬,ÜNäñ÷|t£µ¼‚ñ·ðT؆[¦ÜSéBP·HK˦!ÂM“{Æ9pö(ÝgÌ;÷WâDæ+Ù§qÕ£íF¤‚F¿KÏIËp×cD¸ˆïUckDìíem*¸Tµ¬=¿R,«=`kV®Þ`¶%î>'îön¶Hý*5ÞÐl,ªÙlæß íî·ÅJUÒÊ.+µbÄ„‰äÏo8¨Ø•‡L§ *œ$™ò‰Ùh´%ë³­1û”OÔ0HlCƒÔ¼°Ah°˜¾´)«Ÿèn Ê$?@^”– E"…'{ˆJT±"‰Êb!fw™MÉ:æd6ä 0r·¸ù¬ONNÎNÎ6 1Ûù ‘ÿåÏ2pÄö¦æO2ðßI2}9µ¡{\6t2óÝìÓè^‹óQ–»›˜qY8Ž32п{NËX¬ ™Ö:º€?ÃZ“m'í.ÐfÒÚ26Òß×¢·êÔ_0ó=2S¶øµJt·¿¶ßˆ¿û ™Ñw­ô¨6¼T6c»¦ppræ%4ðMµ!GÛJ p`ïn göõähUjÕBso Òî<°žüÔ˦¦Z[¦¶NmÝ,Í;eãO{Ž?•ùàmü&ûø”û€íÙ÷‰2»kÙ0«Œ»– ²/y›^Rn ¿¥¹ž R9²KŸ6²ÿ›{<-w‚/!#g5 sv4¦10´‡WãE=466¶ulKSKkSsÚª[÷N„øνåÀª!à<ƒ'Þ‚¸ë\q6í‡qEY4ŒC8˜ŒJ4îï%ƒaÜ•³7%5FÔÔ_Šë#ŠqW7^P¥Ø le%Æ••iH j‚g±¯üyðÏà$?¨÷ÁÝ-†¼¡…€CUådm0¼á²‡ Dt=Š¢$­!“éËx )ŠQ…W‚ ¢½¢Ê Bh†U¶C(Y[Nsœ“nÑH¤9©•ØÊ'ÓªÔ@âð¬¼â×^õЫ†Ç_óÀ•O=öÏøŽ÷¿ÿ©-/ßž~ÇmOñŸÃÇFVß}ôȯÏÄù¡nøTó¶ ÇÞxcŽ¿ÒWÜó'qoEײê–ÓS¹‚Bskk± |:åÁ'?ó;rÌ®Á%GõžÂðÚŒ|òßÇ÷œ6F™×·£˜l P Râ˜/ò¡0QW”›·è«fujß™ÕèœÊ‰ÿÖ–#èš…UÝI"„^*YUAº–u$#jÔf9h!th…xÖT³ÀÉ `$öcåª^9زóò­‘š >ö]™/†R©×}tÛ¥;Ò™ï?ÜGÃZëÏ~G¬ï;ïÄþÙ«–g£¾Ê’³¿Kã¯ÆK®Ko>oø_ÌÚçCÙ_ò1ahUoXÈ»[ŸÇX¬'›Pûœ8"<…‚7U+dÙ'ó—Øóˆ±è—r›'&&¶Ol›™nn„I¦ê5w“c<=0ñ0Ô\›%fÀ‘wT …‹aDBÞãžCۮO8,8ÞCl¼­›‰r (JJnŠPäñ’{ŠûÙx»vÎïð/s’¿ˆÝÏØèùyôÑ¢µ6z®9%GøùäÄŽms“sžó3Ç#3#ãndns¨d¾Ï`t;ášÇ\Ër\†o• õŠ[çÆR¬±2§òhFôõf*ªoˆb6Qh,2¯Hÿf‹UÊÞM¨wfµ-é‡ìR¯ˆŠMêáiœ¤*KÚÒøÐž]n ð9h+_圯ø®+?ŽþYnÁv‘d‡9C ™ª²œ/˜F¶:ú66_D+ÙßM…{þú »+äÙ’4u©¸nÀ#°óŽXpëˆÏБSnÙ¢÷D¶ ¾Ô÷¿ ³[íó—ã”ͤÁüËÌ&”Áø Ì&´Ñ˜àhÈqóIv5`±ù½2{òCãv1./K”úTLÖ^Èßo¬ŸeD÷¿Ìâ-¸ ûÏ”ûônG²àÌ@E3OS1.-QãZÜÔÄÕ„I°l`Ä,Á“Ñ‘¾õzaýÈÖ­,s/í—Bo6îokDŽÇaÈ µ›ÌO‚U»©†$Ú y.áo>¹‡#çVòšœ#íØC JE⩆fö'8оõÙJѵr€ÿræÍíöroá\Q¸tæ±þŽ?¼Ú^îÍ*'ÜpÍ…‘½(ûvaE¼ƒlOË âò¬$ü‘4DÃÜ%„=ÁY"{lDëC˜t‹Çãeñ²T$Ú…¸4Ë76ê¼ •Þ"%ü—Û3÷"sTxãú2×ð+´ÆÛÙ¾0–é=»Ÿ9û|u—å¸;f+ã™_f~‹U»œžpÉÓì'6ò«Ù÷ÝrEå3 |e>Ñ9·l]û}{‘'™¶ 7,²×(Ëj–Œ|ôÅ ‹ýÍG!DþÊH†'QÆOB÷Î}"sÍÓ,Æ—°‰pÄŒéõÂ^b{nî¹#FŒ ?\qk'¯ xCìqžŽÞ”ý…Ïò¬6Q­<ۃ˹¨Vž¾;©5íZU%‡“µ• U ¥±P€øÝ\…RÑVîý8mµ"[mp£’Àe‡Þpo΃ûúd–V¯wG¸2è² k·¶47VWzÃ>/ö#ö}ƺIõY7© ºfÃh24ÐÓÝÞÚ”NÖUŽTäÆñÛÆYuŽCO·n1N·RÒ³,KyºÅ†QlÃpxrœesÈW+ÚrþÿÄS矙Q§¤ø–—̃¡üQŒXÅ«¥ñˆ!8ãÙ6ºn¨±uC]@zµç ÄkOÞ䢇>ûgCÄ¡&«|aÙH§AÒéö–¦Ê†Ê”]||Û;|üÌQbÙÝ’a·€vbì’‘ÛÜ<Ä_Å÷Ø"X+‰x 49"XéWd Ýäˆ`µ7t·±"XëݬE¢¥@„iAn§òbW½˜½~°Ë!Ycº¶ÚÉòÂ<‡(´§‰Ž‘Õ¸!éŽB³7KƒÛZ[š\ÆÍƹÕmwÀ.‘Gvû•¨ò"ò͇g¦à< rZŸMŽåÏåy?ÞÎ2Þ8Ƽše¼i5#n¬øhLFþð•cÞŠ²ÉµÏo¹¨×€VÌmŸšèhs//~;¾Ì×ðÀ_²ƒ5xBE” +pÜ¥ÈGDáÂðð9¶g\úôv»å·ËÍ>J0(B3B0B6ÍräÑŒ­ê²¸ÍÀloßj®5Î¥Þ_ÌVA$Þ³ÌZIª‰—¯iƒû¹Gð­±þˆ÷p·’1ˆ¬Zyª‘ç½57Ñ%¨µªÕ±1›øm›µf?÷Ü!ô¶yl²œ7¹f€ÞÒœRD$ˆqq¹ ‚t° …ûz:Ú[›¡²lå`Õ K$ ÛkðÏ/p\I~Ù‰/ÐmL/ÐÓõÉÉÜ£zz™w_öôn З´¢Ú6Eõ;„®;à—ë˜ßeQoln™Û44jÚҼŭ¹!§}ö²oFdÜ-òLoä'ØË%}•‚±yD°od×/\ËÞFI0b3]¥yåià‰ x§­Q²*‰ˆ—0}©ÊnžJcÝ­-i²HW”ÇúJû"íIã|]"<¿–ùý9Øø™ˆ o~ì ×[óö${óë'[Ì(;ÏôŒûÿ‹kN'ž§O~­W—{&{Ö¿¹½­§«­¿½¿,Ò>šTXÍyo˜÷sͬ— “,ŸdÞhÅ—ël«MÀ¶µµ ¶ 40¨æûp÷fÜð·ØÛµ^²ò3Ød+*‹òª„á•®ßHK’5â<ÌU;Lt3³ \’XP²ã3q&â,">›,\"S¸æÛC÷„ ×3ÜGØ+b‹ÆëS4Ë|_çÏœ’£ŒÏ9XDJ—­f²àï÷›ïPMh–ÈcƒåIl%ó­Ö+B˜Ë‰‹ù¶¾¹^0c9_T?FàÞjìÉûYÞ@£?lÍÉú´ä„“HFngÕ‡jl%ãz€ð‰ì­ xlc”±™Á4ÞSÃ…Ämy]Û¨n~ôøÃ° ÀSS ÎËÔCvŠÎ]&âUñÚDm{}”å–(î®e¹ sYþ Ì; Æãñd¼®1æ ÞÍ.üh; x5YÜDnZ€\ "W x¢ˆöZ9_`m¾«vx4§à/ ^GhHdÓÄÞ«bÈ0¹dGhH@š²Iašù v³J/7aØ{’^<Í«ÄÓ>s.:ýô쯳÷¡ÝŸ ]3]‚A°åärO•B¤œ±.ÚYÔý„Uñªzƒ¬ygNåæmò?ó"Øógï´¾ŽùÞávž‹ÇTŸ¿È„ãZ!·—œ²ØÛÝÞÖ”®¯«ªŒ$xÍ)“·9iMv\*Ùq…Ø)'²Ó]Ï †<ñæ¾+F Nœ™°ßs÷åÑÉ»½µ«‚€QXF<ïìïŒXÅ› ærc8¥9Ã\$wªÊ÷³Žœ{5 Ej7ù !z¦kç}‰Šo ʬlÎ!ÑÙ™ˆwöwö÷tÙæ¢s¹–Ì¥Ä1—c.¿e±†™ßügœ <ÛÊ›ËGÉÚùA+‡ ™‹79}ö7,‹`æÉìSè뺿«l.š)Q=HS‘ª-[ÈO-ÏR¤@®R†#›`/ëˆV6Ðòrì„|–Üß%е½=‰øæé±Ñžm½Û:Úš™§ïNt ;˜ùÕ¥)—.ég–ý‚µ6 ¦ÅÎ[mœK Àûº'¼©Lÿàå¯õ/…ݹÖãÉՇᕘ}u£;{š˜ÉµŽ$ëjªíR °ˆŠc ÙØ6‘>ï`¾Ð&À¥åºÅÅ<Ûçéhë„4ÙËnBÂaŒÃ•áŠÒ¡ÒH©… ƒ{“ îCiúê®<¤àe7xŒëjkªhH›Ü‚}Âì¶Ìrmت7{  %Ðã1¢œ££Û&À’À3r€YWŠöþF_¸W ç¼r!™çAtº‡ìPÏÑN?Èi>Bɹi?öi¾U¤amU8° ¨ÏÌ9Ê,jæÎ;ìc§î»÷Î{î¼çî|èƒxÿ[ß|ͯ~]wWg[ks0Ô†«°“Jå Ä®Áà­±lû'„Ù÷©d$9‚½ñD †âµ| ®Åð7ú‘|/j°<†éÃåNúý—#ÿòæ×©úFØ;‰i¶Bö|€qiL†§£ðÔ'Ò)¾¯7^˱G±p/&Þ<Ï…DŸ”¨ªeQ‰¨¢"’‚5áÒ¶._Y`b:3‚¿ ûå`y ¢'!ù%ø…¥áš`4Xáo­“ýb4ÔˆˆpüI€É“7Wâe!5QóùëvvDüåA5ªø™_àÐÍJꃢ*KUÒUä › ¼¦äìÜ¥ÏBS¸ãgOá_ÝõÃküuþWù‰D^éOúÞÌݪ9(W+ƒçíöÅ}í锨JXdžüã‹*Zi€àã'îÁ…ØyžãEb#•¨/àKøÛz|1_]*ÝP('0ª‚Áš ?áODê¾_ A¨D*õµJ@üu]A"¬0³ç…y_8—CJ¢¯Z‘ÖñÊaª?üâö­¡ ¿½ýB²ûß!ļ5"Yºð.={ Á9sÂq‡fj±‡šÕÖTW:ÕŒêñ-½0aãg>Ksvyi—ó~Õ©gˆC›²…DQóöJx«)BÑH8DšK#1zÇê6zÕú+¯cW®j=·t%™Çþì/øƒâk`ûa”F§^‹Ðýº/IÓ-r’ÌYK‘ªˆŠ*®@z^àVæ²ÄÁsZE BT:õ´v*ZÀg¬Š#ëí¨ÑH<èNcçJáAwýØh{‡§&G7mîíiiI§jªÊ±ŸŠ[¹Ö;§òš >†Th6#UñB R'›Í:;zÎf”ÎftóèŒ>Qh64ÎÍçbÔ‚†Ñô=¼yšS´:2.Lƒ7|µA+DÍ=ÐAšfÇÆÏ¦áãL'§žžsjжJ@ˆ‡ëê_¨«>´®^>:}¿}ú‹z][+F;¶oÛ26ÒÓÝ:Ü6 –eñ’¨¦ Üµ%!gD7óJ å’DtaHQÕFÒ½ð<ۯᰙd‚X~© õñjE•oܾçÈÐðsm“ïøÁÝÇŽN˜èHD+”º½Ÿ¼bß5çtd6·ï˜­þ³_ÜYÞ:‚ïx5±g¯C,¿&ZñºoþE¸|)5ÔXR;ulzä’ýÂÜWÖ5ÖEƒ›ví8ÿú…³·¼<šì©ù+±º{ª¹q(áŸW|W šè“ßX’E9y<äÒ­ÓÕk.i¬¡±ÜtôùÔð0c3ð–YY]ñÛøbL :ø9j‚!=!÷Dº‘þ¢¢-åsÈBŸ¶€®»‹uÌÐB²8ädq+SØ…½;çYJ‹ÑsÆÎñõ½gtš³Ó‰žmü^ükz¶q‰ø×èÇúÛ輦†!ï™’ÌŽ(£Ÿ³ûö›Ö××O§°Mâ㌮¤1é%BÖ|…qw„ Æ@ˆ‘ƒ :/z+Ú,½/¥z?Žþ\÷Y*oμ7OVTUÙ‹œâá¶Zƒëéäe±j@cÇÇF=õÕ¿q}…æ ¢÷Â"¶Q­î»Â¦‹»tñ‰cýÐ%YuT²´ á¬fqöHŽBd W÷ú¨¡Ï“,Ƴ/iDvÔªàÄÕ“ðfG}‡üfô·¶g6ŒþÀ*ò+þÕd:âE&·ÁÝêÇ{Æ6Ið°ºzëÒLyéÂ¥ –Ï-6ñˆy—HçΟÍhÌýísm.¼Â«l:Š9öÂMëÝMç ÀØ GÍÂ1sWGSoñ3!°‹y˜öóErÖÐ3ìe ZC{GbûBTöÛ}Òœ:‡ÿ· Ä]È"½£XSÍðV¬æ¦$šyÐbôç­œªY¿ f2´"]c»ì•˦Ãò¯ùÔ*€çzø•k뿟:#*ñL=Ü~^˜â”ñø Â%öâq©ipÑg_ /·hÚ¸…½»æ·ÌN޵q ܮ;tèk’ÿꑽ›§y%'æ¦b,HOgÖ»µð$×pÐ8áYÇVÁM¦ñ ôõ^ççéºx`~ÿÜŽí[ÇG ®‹Ù;耬3GFŸ¯?Bé³ç¿+}vôÙy` úPv¯Ôë’ŸoÞøFf.ªæqÀ·‚ƒÃÓœ_ÛL6 ½$ÓŒm´Bm¢ë€[Nq97‚-ÔV_Sþe•:_6mô¡@ÐX*ÀÎ0ãFÈaìf  ‶`QZÎÖ† æWV^,†@»-…mź?Š », š? ùÇ(àWŽè@1Ð… êû^,ÀÕž°Ûø.êÍ ?Ç/cp¡ïé¡)¬)y®\>ŠÄ[ñ$”² ÒÛaÏ{˳+iC¦§x˜;o-ÖÑÇûÎȰ}ÛÖYo‹ÞØŠ†îm6îmö¢‹°àqwc?ØV}Šº˜»<6ÇŠ]ò5Òó¹ ÁÈ_„+áá›éw}>¯»>O_ª®…Ì” …®‡Â/ìzHtÕp~×G÷qíg¿ÊGkšã‰æšH¤¦9o®‰fÞ­i‰Ç›j¢Ñš¦x¼¥&ºÑ+&ñWþ;éO 6Å-§%áúÌê_0ùØgÉÇ£Ò1Rˆ³KÜþbg‹éìq*¤×K‹±3øÒ°ó…Þþá[c©Žòòöd,–l//ïHÅ6̬^ñµòèßQ^ÑQ‹ÕwØî¯wÝ}çÖwÓ`•ÜÚÈ ¬G'Ïصn¨|ñÙ1þo…w£4…¶£Ý蔞¬«àS9Œf±Š·`EÝ9¯¶ˆ²Ò8A–¦McjdqT&ŽŒ$H—@Ö®¶gb ZJŒÕÔ˜:ûˆ^}h¹dèh9ÓK†ÉNulpjhª¡)N§Z}ñ¶¡NL…©Ä)KñM©ÁÓ’q¦Ü5É4á1,óqÜߨ$Oà!K ñ‡R--‡o]¹¯ûˆ] &·¼>˜,Ÿø\ªa—%{›‡.¤K'ðTЗŸlÂ;þŠþêÌ>ñÕÓÃýuçMu-ÎD°6lŠUmõ™ÒòÌWº_?wöCúJß«ÀÝTÐ8EÊ|+Á‰¥‘÷s<Î|½,ñÉò-4ïñ%î~ÌM ]x?s•£ X›±ä1’±Jw¶/5ãËEÖ¼Ï&@>‰óÙy5Ž •È·B«\5×Ëœa‘|šOÒ¼@Å£„êÖ¬ ±µSÓ wÂ…ì ÜúŒx€P¸Æ¥úä–ÙÉ]ú®‘MÉTi:•*kª ”µ%Së°M}´ÒÇ ÄcÒr ¤©Ã\íš]‡m:Ù£×uú}eb°m{ã–óÇ+3;íÆj «”‰ýõüÁ` ¾%éÙÙØ‡&ªp¼îé.¯$=NÇåq.‹W°ún$^a=ŠÅ+ìÛ³cÛ´^(.(ô§¸ {¼s²¿ÀâÏzt±øFÜ‹Ðýú§È5qˆ+6Äb†lDÉ> ­@šHY‘VO„ˆ‡Ø0IØ—ð‘(…È.ÙÏÉ2ZPaS ËÇ1C_ †~ç ƒ‚, 3Äó°à‘}í‘#ûörøÈEG.$;‹sö.í[šß±yfblh ·»£½¡¾ª¢$ôk žæ¦é¾–¬mlî¥deÛ£sÑ—uÿä0™ou©m¾“1\BÁ½(ôŸëöC(8ÏÖ½{0:÷è‘¥sì9¼÷ðÜö™éñÑÁþž®ö¶T²²<øTMá©h¨ ³ä±©d †ÄÌ8Añ¬¦"Ö,ž˜Àت\4€Éor²¿“Ö,.Ù}Ãqb‡Î~@yÏÕ3‡GÓ¡`Dê¬;°¼ÒûÊ'+ö_üªáͯØÓ>yËî=¶õš»Wº–w -oiÁi–ðÏjG÷Ÿ= ^ÿ½‰?»ïå-U?UU_¥*cMÃé襯Û?PV?sñìÈå‡F„®+^[ß\O Þùö;û+±~`¶ya>µåe[ñ·›çVg&Vw¶áG®@¦\³s`ÇHä=æ%Ï…¹2xôâêäzûäe#HìZ²6Î`NfÉ?+¾ƒ¾Ñ?IÏ º or'ùô:]­ªŒ ‚H߇6°ƒ{îœQ±‘Å8ãÜ‚dY> ïV {* mÍŒ‰ò2–-µ®¦¬¡¼'p¼9eäÍ=™¹Ë7?óc„Þ’‡Y*Œ“=ñb¥Þ†Šãeo]C÷|2ËQã4¿êtµ²¢„·aÕ`&X-N°æͼ(V–`ÉYk«©²¹´9Ñ¢m}‘¾ˆá8 éd)…ë%9Š Ié@ÙN>8öž \¿Ü7òò÷,/¿¹WšðøÞ×÷u^6}âä5™}4úþgŸØº0ý†‡.Í™ç·OLóçÌoÉ4OöùìÃ÷>_{%óOfî¦soC›Ð?±å,ÒÝU)#®6Tù…gª®ïdZVÅjAÞvX´ècƒD,¡ú¬>@Ó˜É §®Ë,K\¬ŸY¤ºAÆršë8¯‡•D7ç³™½ÈíhïïmßÔ±)™nOE’a_‚øk#)äɶè,ÉVò?jå€Î¼äܧ•!Fæ+LÊïÜZ9~lKãŽÖ XæówÖé=o²jçèÃ7fß6vÙBo$º5ÜÉ¿#Ǫ¿³DGØ:; Òc¼#€JôóúöŒÅJšÑ–2¯˜é)©ÿ*/€÷d6Q;ÒÙÙ9Ô9ØØÜ˜jN¥TZS›u1jGãèF=44ÀIJ;SA0OÉz!U6KIj¥Å3vœ… åÍÅH¼Ré²n^=u_¤5EQ«WËÚ ×Ø^ ¯¯·Öæ[ÃòÃçÞc% ÕuÏöN_4“Jo^ÙÑ›ŒÆü}u»ë=ç]¯˜yÅ]«¾kDs‡Ûóõ>_YuYßy×8ôös{jj":,,›ßüÉ«.}âms3c„¶{‹O;øÄ¡#tBöcE29e²Û–Ù4Ÿa¬6FÐIÆNw—¼$¨yTlN N÷V Ïë]òô9\B~žÕ§mòäâ“‘ ¸››Ø8V s:ÿ=6¡¼\Ö Ã÷„JaJ4_ ⲕãïÞzáÄJßUññ2¨ä.ˆ³Ûˆ§¡üÃ*âdAæà­:/ P¤`mEte5<àRqscº1ÕИnRæ¶?·Ñ'dZ¢± Çí"×hìùcPHdòý H=/]ý‡“Dò|‰`æäìÕ™Û~ÍÀmt“ß•À;£m ˜·ë½d*¾lå3e•ºÏÐÎþÏ‹,©ëîüa¬‚ûøùs\H’‰Lþqè 03÷:lM¯ÍÖ|%[Ñ¢Äðü<7cÁ)Aþ×X0ó³#EåTvÚ%ïg9§©³äÙWÀö†A¦jBXÔk¶nåðÖ=[wÏï˜Ö!\C}muY\ñn‹»ƒÐòÃd#óÞó2t¢œÌûÝ_ŠE®º ª[s6'‰_€÷yAkíNÐeÝÜäÙÊ =6\z\QÎáMƒíÉÚò¦Š¦D¼$¢Ê¸Œ+c¹w?Éñ’~ˆÐõ^Ø5¡Âx¹¥â¸9¥bXR_iñ•ÏjD­ÄƒóÇnU•‚ 7Ë]*Nºfïfž´#îjmin"î[cyc¼4V$”À Í<Æöe‚¬íéÞÁ¡¨®d=-)ã¢XY8±Ü?üò÷ž¿øÆž3;^?ÐuÙÌu'“‡·)RògsG¦ÞøÉ×½ü“×ï⪟»âM;gñ7ÉrýÈy×lÉü†åÝ‘ù°‡<}½í¹1ô.=œãX7YFxƒ2mH!“¦ûÇœ5FÕrš(çÈŠŠ%œßZ¥Òr4z,êUÀ½¡Á¾Öf7ÿXÒ7Žà}·0AðnG£è;Ì…#Ȫ\O7AV­7Ü=çWno¸\&â G1Çëòö ô+âíåzÐ:ÚÍÛÛëìèëíM¦Ó‘’ætõöß'ì2”gˆm'òƒ.÷Òôj÷e&+gåºpþ.¼°ëOäÂ-,?/ÁCNæÉñMB?ùó2–Ï”Q0/÷B¿ë1¾S­ïr%ò‘Ƨ0†.TåTLöQY±Æ›c5TW¬y±vÄù,Ò¥f#u’n;ö yuè¡wku`Ï7è§ÜÁ[OÁnlËÌ:ùì,€CäC;û»:;òì«™§rŽÈÅCl}¦õ_F)?N‘ÿ#t®¾TFÖ}<É ²Žˆ–ÑÃÝh4›X ‡#ÐÓÝÜÄáÁþîážá¶Ö¦®æ®šªÒ’pPSp#×X|üáMŽñÉàñaƒP`ün:~÷p÷&²÷Ÿ¬/lü‹Qu¢Iô!Ý7<ÄÉjÛeÈ6GEªÇðôbËGG×Ü£W꽤º %EäÕ|Ä=;ž55b491>ÚÓÕÚÒØÙÔY]‹†dô4Nûíçh¼ãYœÙEÂY‰{ßÂ%½ë²‰hÙÙ+¸Î½—ÏêçÎtF|a©7¹xüÒã»zËäk>zÙëo‰r¥õúÑÍÛº|oäò¿xYKå‘·î¨n¨V¶q¹î©7è­;®9yõè[›±ìxœãíÅ.Þ>ˆÞJ~þ ®ÁÍO™Ð@dBj7°}faBƒ “Ü„>R˜Ð ã,…«—pXøÏÙñ§ùìZóù÷‰î|”‰(;]ïs3Z%¨+ pˆœ_þzÌ^,ñ,õ%³>‹zà¿ߎmúÄ`þ ¬7öl¥T¾{!CI‚i”[”EÞ‹ ˆAm¹ìíéöJßz¥N’6$ˆ™³ÓØõHß#W“¹'ÐmäçX¼¢i,¨ÈdjýˆfDå`× °IY-ؽ ϘIœÒÇFºg{fóù4cAÙ<®wÍãs¸›üL¸XæÍE6TÄ&ÂðÞã³Ûc¶œ?d~›>ë=žbå±É%•®ÿÿ9Ö›ÿÂ: s {7‹G ÉÞí'E$ÓfàH˜µ˜ŠJ†E·œ|Çɟ¿a¡ÀÑnbÐw`MÊhf`rˆþ2Æ)j=­2bs½;ļ:xµÍ!O"MUTM¡qAʹë´qÎTˆ] MH{Búçˆ×!JãÖ술ã/“Ø‹ÄäÅ#A½G*ŒÛ¶ÌLŽô÷FK.9eމУ–ÕÐà ×ÊLßú©ùè¤B´†N¢ÈŸÕˆ À7óª¢-‘p9/«–å-ÐyQofÆaÛ–é©Éñ‘Mý}Ýs=sù“ [ë¾°ÄÞKÚôëkÿ€ÐiÝ¿exˆ—U»ß2 b©­rP7± ·6±Þ…Å"ëÉÔ]OvLm×''Ƽ<s=É~ød{Êæôu6§ÙÎiéEΩ° l˜6æ4½c­9QÙû:{·iãÓ7¿¸SÆ{‰)ì׎â€÷çS‡‰¼À­÷ÆÏªÇÏÑâ½cÅzìh5ð«þ€º‚|ªì÷-+Ôƒ°©ª†à1ß’ƒÜ!J³`ãµ¹ ¨ö«ë‡ÅMØÐS}òÊ‹Ä=š(kcÖ½äê;-pªÏ¯úŽ#¡¤|€ú‹-½nh1^ZÛ¸<ìÜEß~Ù[á>Õpðöª™Ìã\Må;ñ|Ïe¯˜yúZ ZEù'KË3?¾ç-•™¿­­Ém«·|9V!¤O^ýl$š9±ãÊöŸ¼üø–A|³Qo1ÎÆÐËôãì$2þª«H“µÕ‡ –ûþ>8Vêëó>\näp¡~¢G_;f¸ ^NÜSW8ya¬"øýõ8àï–Æâý­AËÚ0}ú(}úÆúF7 ¾ôÁT°àbcôùsZ{ùè?+b/{G°¶nýú}Èÿ“x)³õÎÛ³ÂýÕÛŸCËúQ0ô›ˆ}çr;WUU.~dJ°Ý+œš8Ì£ãÿÇø§Ñ¶çæÐÃÖN¥™¾_ÊóU ‡çöb&DcÀaQ©.ÔíWdõàK±Ma4½×±~þ$wÖ…gu§“,0ý~²ÀLÒu'`¬;ö¯±ñµ±îLYëŽ?LV¿¶d,!GàÙxQ^ËPо ±{› (h­E€h[š*õmðxË·º6Àµ@Á£¿ù¹™i¡Yæ–ç—·ÌNï˜Ù1>Ú×ÛÖÒØPWCoò§¸©ˆó,îb—Ös¬à‰"=´oL×q¢(çNóº¯u¢¸uvr|hÀ[X‚fÍ]î™·ê޳óDô~=šÑõh„ãÈà¢ey-G¢×IA $ÚË“ÑCô"- nƸ»³±×âÚHi:P¡ÔÙ½›»@|êC÷°ëÛË0’Ø‚eŽqߨÖ=o'%píX‘]zÝ…÷ q›ÝÅÂSÉœ@ Xx”£éhwP'ÐÓ•NGéxXƒ‘¦N<ŽûÌ…™Fç¹j0—^7·øÈÔVÜÖ±ÿª¹ÚÑJ‘kO·ÎtW`9“Í»¯å'\<¶íèþ·.viê¡„ï?0qèš³³Ž‹ZVö.þ ;를™£9ú>ÇgÔ ²3ßÁ¥(e‚)odÙú+””alòI3”–e£ø€õ×>!†OØi*G-aÓ4®È~}m Æì¬´»«£­¹±>Y3\;œ›^¤.W[¨ÊÍO~ŽÉ>×ÁÿËÞ›€Éu‡ÂçÜÛwé}ïž}¦§gßÍ"i–;‹6kÍÈÒx$Yò"Ç»ä}ÁÛÁ6Æ`ŒÍîØŒDŒå‡C $?ÈÏKBò%ðxùÞKøò¿ä=ŒÕóWs÷¾=Ócü²=x/Öt÷©:UuêTÕÙª´/ð=*Å w²OZdt„xçÁ ù§ßoÐÆçHaä:¼©«ƒ‚+nÊÇñØs$oÌ9ßõÒýv¹²÷ OB¿OðÍe›P«ñƒªXä?·E .yž€*¸¤á×¥×nô–ÜÔØ&oqëE-S²«k­kÉç,ymòÿ™ïõZtÓï XíõÝç2@X².*øé(üTf¼²e|°}«ïxôU Xî¯Ï*LšŠD*޺˖ú¶ @·Bˇ'ð þ>-&kÂb}OÓþÞö¶Æ¯¹ò¬ø²{Laަ@6Ûµ×õû¾;¥Î9pÌI·î°9I>T<œsÑÝo˜÷qο^[dcŸÞÐìÔç\©ùåw*©>ßî'Md‡6¡˜àÞcv׌ ê3 0¦|£1›2¡ŠÎAÓLã»¶,†»2i0Ñù8ü¿¡Á¡\üóË骼÷ÙæxMÅô¤°ímo;wûí…u;|ÏÓŸôÉ'¢êÒò½…^á{B÷õŸxã+ƘÊÔfÿ¸ýõ=ÅÇVܾóýŒ#Oi8N%š ‚$€S3Ï8öoÅih§Çž0©’0™0ìKŸóËE{NKp|ó>KÇõ?C¡ð<ØR~•ÌÉXÓ3: úEEUŒF#ó$©ŒX‰ˆ'ìLDÆß¡ùw¸oM©·¦Ì[DS‹ŽÇQ¼Ïç0·¡ñåºx´ ˜,Bˆ KvTå#Ðó¨ÔRzÉ^–Ñ¡Mƒýlâ`d7îhƒ=‘?Y¤ã/Š_â>u]ýרëWÝL°@0×_$Â’iñ“U×áÀ(Æü˜D,Bäð¦Ñ´°óÐhDŠD¥e:L06(ÓËû”À1¦ŒO‡Ô¡7=Û·g׎)mËèZñسRÁ=G`­¹e~QzŽxÆG¼ù0Y_x#ˆtdcãÓÀ©þ¤,X˜ $²®Þ»ò®URê­Fuf±ªÛ~ðXõê7%›©ubÍ æ¤Gd3é ?Ö’W)4m,µ®.6te ѶÁ'LID\C›1bY'ZIå;õµ<Ä*÷“1²ªK¾ƒqK `,¡%àŸýK+cëyÁµ|ߦ˜Ëa§ë+áïÐß©àï<ÁKø8­¦ÞÛºed¨¯§³3”¾-¯è¤¶­†|ÿ¯ÔÖqÑ.…|èöso{[áŽs…+Ï]‘®z¼çлëÇpû¸¥c[5}æ=÷f›bÕÚ˜°Uø¨¹9!jð|÷bŸcy—c3ƒÝ9Èâûœü©'ä/±ÜúýÕö õ«þ¢RLÃì£ó×~Y&sÛy (+i_­uÛÛ²ÍtýM‰gûE-ÛÐ@HCg z®'§â¹æL(cÛ°aÂÙJA\ìÁS-Å-?Å“Èä„8øÜ‰tÕ{z/}ôÄÞGf.û|ßeùïü»ŸˆG ¿­N½í‡þX2ÓE08÷صZøõwù^ä÷Ü;xï_‘ß(ü]X¦j ¬x«žŸ–N“²Æ{lBúŠËPSÐe²CÀç¼üeЖõIÐOØy‹ *`ÄôúÓ<“;A€)dÜ ‘e£¾¡~À5nb€‰¢ª–ÂäfqÐ\$øp¡$O`Lª³w%+W-Ÿ:zÉüܞŽ‹;·OŒåsÙXÄ'’:ÂΆ3Ù\:¥~”Ñ¡éTœ6ÓÕ ³K ÃúÕº©ÇÇ›°ñ”]öÐ_©¡£ÿÏøJ'%mËõCÇß¾W®­>rÃÍ7»ñÙÓ§?|jàÐîö¾±±mû‡Ž¿ƒ^׬-?ÙÝ?Q7yÙÌô©íM…¿>yùå'/½¼²½«èÁÉjª¦òrTÛ³ó¦ƒÝÑdg:ßóùSý‡f'o9:Ò±óøàÎ+j“Ó#ÇëÛmqçMó]¿øïù®Ê ìSªv æFÛ+ªº'…­×^¼êªÅΛj…]oë ]£¡9Æëe­`ÆÓ˜¦? £u‚ˆºÂl%Á(Öœóãv©s0“—]cÂ%4fÂæuQycÁ3—@0°²>o˜ÕÿèÒ¾½½æêÓWž<±tÃÑî½dß%»vLŽ75VVÄ£’Ž £Æ3—Óƒ9á;?XÏ—’Ôp¨ˆ¿Ÿ]¤² )XÌÞfƒ±µxÂ.j ÈÖÕW¼Ô›!½†&çç:?ß ß~þacüÌ­Ëwök35·SX‡Òéú4ÿbÈs ƒÍäé™Ê‡{œ; ýj©“4èwJÄ4žhöÀøÛ«€ñ^~=ãi`(iùtL¥Œ'_°›`€ßŒñDáðb Þâ‰8ôÿV—¾¼@C /¯–Ò—-X =~¡h´ÂtÝ90n—T›0]k4-± Î뤭Áœá/Óº¿<†óÙé*×ÐÁð jèMÏg A©ùŒþ Ô·„w ¿¥Þ _ì¿eí÷سµ·Ü‡‰Cú7ÌÝð)nÃØê㺠+ÀÏøs‹XU|ý¢¬`©ÀÞØÃW’ý«EÞt "ųRþ¬˜]*ðÍ$³a¢Vqž°”R‚Àî+¨vK_dààÖ’ù½dÿ^—Í(–RñTß²I-›zHdË'TJ>¬Š–7|ÐÅj xŒÍ+¨ø ×” bªîé&dÏî‹vNŒñT NTs8ÕIãæÝÁ>Æ]ÉàêÈSP\†øSã7}úšÛ^ØÒmH´nžß¾²½©aj×ÂÀâ»/ÚrÍG/»ô¾Á°ÒZxyîg~í±ÜäQú®¢hï»o\yäý׌mß4SU«nßyÙæëšFšSwŸ¿eù¥öÌŒïnzã{¶Ó¯Œ½úÂÒý‡ÚÅï½5æcþ kN¼F7ÁÏ¿óË͉ùrç„s:Ìo`:èJ½w÷Ì”—R›üvñ÷ yþÍl˜¿Í–:F@»x}ƒÉgsxŠ %{¼‚‹ÝH´ñ ¿6ìÆR.·cÊáÉïbï%±ñ^RíeCð[&YJÛZó¹ÚšlWE—¡¤Á:O¾gU`rOƒÕ†R¢>kñ…æ!â=mÁXVd3’Lq5iÏzméŒÆï=Nõت'¶.R†wÅùÒ¸ú9(=Gfè³úk«êSÛ³-I 2ó|«$‹¿eiÀ×N…€ù3+²èñ‹_°n¸ô󴽜"lvdùß¾9˜:Ñ™PÐ/²P‘Ræ"B,ά` µXnݳ¤Ì°Q²=;#Ñu ÛØî'œæï KEñ°à÷¯ Ë’¶Ã3 J÷3à:&¶nilÍÄã­Íù,O‰+¬"J”=âž –ܧ#I|ŒÇ?©L–'ê¡ø¶«H”Æ…<äßÔ~ÝxuzsíÞ¹ í†húÃíB:v þ¿ª,Ýs. Ò›“õ —~3S©kÒØð{è»þ’ÆZº}-µ×UÝ|DXH%·§hw4QøÂ½~«}(B¿.Ž_øZ0 oTg £¢¥p_»ðó3>ð_ç¹}è!O£CÚ@•_d:-PöØGÄEã¼–ÙÖèŒÂßlÅ“¨ýð?3oó…gÄ}ÒÛßxø4ý™%»"Œÿ}-úsæÿnõçxÓãÒiÄ%à ‚é”ϳ/:ÃÃJ°$†8kë`{k}mº" S¦hj¸%X£÷{Lž+y¿×±þ˜¿Mø9i?zžyë ¯•ÆR!>êKHÕøžóºÂ³nH³ðsô‰…—áÓ&­ßÂ%ÛÞƒé$*}€•Ñh`VªYÌ»ú·™ã&îôãK_ØÁñ2@ øÜ)>)"…ª…«_ÿ¤a)’¹ø|$`<­‚š6v=G²!'@@Vð¨°"9t!JH*™ˆÉ8Db\Žuâ]Šæ¢Óø™sô;«=âwŸtl–‚ ÿh òœM6¾¸ÚŸ®âK™< 8Ï3b÷ex&ÔÐŒê ºÞÔh?ÓhàoIu_‰b÷Pâ@µž—èÇÐ÷?ò|lÖ\ƒ\Çó±­î•¾Eöò‹,€泤e'$_úØãÙ%ÙÑÜkŠã8¡ ¬õDXM´°èU˜#È¡}Œ Qd‰Ë]ârgçD3ç~G_ï€té ®aVÿøøg¾·`“é+«W§E>þÕħà£-a‰k““…jdA\1Û8ùH£3éx4ªä*Æ‹úŠ—“1¹ˆïªŸàwW·š3‘~¯‹Ð=<ŽŽH ™z*ùâT”DãF|}ÇÒTFê¦â†)wCwë‚{ª´OÂ4÷Ô'7òÁ»Œ÷]O€G38¥š!=Í”VŽù¢Æ©rp–‰ŽCqœ~™XÔ¯8|œa§œúznõbøtè¥ š)ÃleBõ,2›ê2[hÓÐZês›-J“‰xT‘læÑð­âw‹løVrŠ{ÕœiUTûóo‡/­sYð¥0<@Çÿµòs.ÿD½L×}º®ßº~5×õ¼Ÿ²Š‚$¬ !Ò°›‰ îõ˜ï ãtæî»ÏÝq‡øÝÂ7ùEB:¬û9:8u?‘ã --8¢,F†¨ûÆ» ƺ„Õ]æXéÑ£Ì#0¬\‘—uáz›-ž:Íã½ôÏÉ€ÖËãJ@ ßZEϵ‡“”6ç«+y)×@ì·ú=À÷W>u„ã«ÿ9Ñ´ñKÈ QPI,ç°håDÅ㋨“ÔîÎ|ŽÖКxr¸9žÐßÞ¾µô‚}|B̵$i"§´`] J~ ¼ôVQ™$H˜3D’p»0Vk5D¥Ó¬¢ŸµÁ³ ™tœ(qÛîß„Àvå="†…†mWïnªP%Z™Ü3M)Ž~ñ·[OíîòŽûýݾÏWé &( Бj”MØ%{Å-q ¸šT3iÇÕ´®·nJ™¿÷^y·ÞÊuúEÂM à «÷Ö÷cîe£ÆŠ ‘¯Ã_Š"A ïNfa!%‘å¯ÕC]ôca¦"V’‰F£|¥#»Ûci­t®Ò®Ž†î\7ˇÑÜÖ܈‡_H€?,ÿ¹‹&|AaoÅÛ[UFº„i,€$¯URW¬>Ý}QÚ×ÛÓÕØàèÆêç¢~~ãKl•³î§ÀÁ áå’¼Ùf€~·ˆ—³òèO>FÈ„¶•÷QR®¶š¶Ô!ˆÞ¯ŸÞ»*0üu˜_E8ðé•;Šé:r õT‡Èr~f †õå{„*u4ËËíôPó$1æékKfÃÔ©Ë>]øãHM,^¡;žJ7„ ÿ©¬î>7Æ·}Þøí@œîŽ% ÷D+Áúx¡&š ¥Â…néÑŸÿíY¾gzoá9ñ  {<ÎS™ªÁˆbº–ÑAÿàŒL'í°Ô¢².9ýyÿ'9é3“›¬Sœß¤¢977xÁX“÷Á"Ñ$=2œˆWÜfIâ\±¤~úGîD'–TŠ$öLQ®“»aHâ>ånÔÑ«@Š]Z»Sd¼”oÓ{S[”z\û…Ù ú8àÏFßM\Û&¤Sk“t÷ ë(ý™‡µ\‹IQÇr pÝ\xÖ“lOåf ××Áh\ÄEÖÂÂl=ÛP¨ÄËò)øˆ5¢Ûûˆ #xTJÈ ³+Š»Ã‚ ýxøôÈú>ý÷Üsî¶Û¤G/D¸Oþ‰Éüó§ýnƒvôÏ¿eùçH9þù/Ïÿ’¯0üâXè·V7Ùüýn`‚"qQ¶áµï¥Tf£a¶ ^XýÁª @#®MòZ«1&²K¬äÂä·ù4|ý ›n@çW Ÿ²œ_bòK>·zäõ1ÝûÀZæŠë²dQ‰+ã×ÑŽ€”’ ² p&q¿(.EE&€ë!t“Dß"ßTf[hð'8XóMH°¢3ɶ@òi+E~èνÿ;÷ÔSO‰õÂõ»ç¦{ óô7àèu÷Qè/‡ –R¥(nˆÓ XŒµq M@·À1,ñ½öÚ‚’úºêÊt*‹@¼èIŽæüx rxdPŸÅ@ÛËÎ[Õg2ô/:ZCWvk­rP::zÔ…ª­E‚O’ðã¶Îž=#u”^ø€ðe¡§¥g/~]8,ðùñ4Û#ävÿibÛ%ôÝ&æþàÙ—xXÄåF¦2ωÌ(Ôk°ZkQuÌ3”j¸¨ep?ѽ—è7ö!–•þFü[\{ fKz¹3ª@ˆFõŠÉ™U±aå£3ª"‰úƒ—*‚_ÈÔx“µ~dKÚE»òé|²¥µÑŸë4úsì]£gõÁs¹;\ý6y¼ƒæS'“Š€EE4£~•êÍŠh X4e»»!lîè³Ñ²Ñö`m¯ ¸ª»ÎE[ƒµùXŠ®&Ûõ·5hJòý^=ANOáYûØôП°›ªEôxQÞÍ:>l*E…½Y z`î=^xNþËËú1Þeh2"‹¢šÓó/YeÓ_·°WtU  ;ÙÀ7yh»­«ÎýD‚E’—eÐèr\qÆ“ hã[GGòÉ\s3š c¶¶:!²Èøß´G.²qÊÏ%"öC¬Ç—SY!Z3¾ý@÷•÷T§·-œxúó¿íöØß_>ÙµokþÜÉãÚ =âwãÉ+r#í™ëŽ ïÈ~å7 ‡×®?ssíÖ‹7ãiÄÍgf¶^hCY®ŽIãû!¬‡{Û["À˜„caU—@\Âq¢ø”«y‘QV¿,Ù¸ DXY»]»À†ÝlgVºå NP³11’ÖQhÆÆhÆ6Gð蟌45¥òéx[k.è–=dá’R'Z2á/v„D‡<…ž»}×1LÐzÓî[æ:Ï-ŸT#Ê«ç ÿÀj4>é–øÁcñÇÓ•/‹ƒÇßy1Ê÷ÌÍ”þìÂ÷hEá`ªJ¸÷E]àÌÖ=Âî0pÛó›O×ê·qX5¯óŠ~æÜȆ@’1IÞ2?¬%äõÏ¢æ±(ÛG’)8l£®â®ãÓE-UUEiU¾ª±¾–VÒÊ|âöºNM§‹h:Gî‡OŸÕ‚ T‘aÆj7G æ YÖc‹0ãGµQüÍÖP˜SõcöZ4¥’B]Müͱ|1·4ÕT9رø9ââG¯h`I˜_ªTUy>¢KÍ`Mio¯†ì1‘±T7»çŠUA$eÉ«µÖájH±¡“› ÅMM5 Ï@_Wg{kSc]mÕPõÁWØä ÖÇ/ë¶Ø>N_§¸~ŸSwꊴ…Í!³È#Œ=õÅÙR«WÙmùp)åíBò[šªZ«[mñ†¾†|ÖKßõŒª[­ìúÊ*o´)©ïHQC]U¾:ï­ïÑôÅj©W8iªµ¢Ozr¶ßKÑ£´£­¡Î C5èÀ;GEãÉïQQ4š¶[GH…ç8ÚÚxR!Gð.ÐPAjÈñóYÙÖg•q#Ëí:ëºF ž ®ÄÐ[ŽßØ`fÙëÒÆ#W%+„èäGO¸ý`ǹ˯ÜýŽáQ~¦O.o?d¤ï˜¼Ð‡77¸ü>á9xEͳZ‰‹Q ¦‚)‚$(X)ÙeX$ÀDÙl¶ñ2=F;fÚÛZ›Ýöüê ÏùººÉ{y·Á.«RIù¡…ùI2#”<»#Ìé%ž<çW1‘Þ žX‹Õâ“1÷žNf å5åmḭ̀1"ºY\cȺǠtDãòAû˜|Ýü%$Ûø‡|´øX=UxF:/½ÕS|¬l%â›Ð3'ÍÃä]ú˜åa†cÖn `qÔ‚xl€Á‚Ç4è¶ÚÁ9~)Q…NqblÓ@UwUwGk®ÞËP-oðOmµ®®×­×9øyZ›$°êõ1ÍBHŸÌžÁ)TÏ(‘¦³í™´@{»Û:êÒm™¶P€¦„”ßV§¸è„ÞtÝØbqí¬ööþž®¢>ôr?eû×­d”Ü|~ OðÉÆñP3è>vß@ï‚WSm]àfªì“W 1ñ­”h´¨U¤S”ŒŽ oêh«¯Mµ¦[ƒ~’¤I¿‘åܺX˜€­îE“„Ú«_¸mjê¶®¾ú7nŸšºý7®Þ±²#Ÿ‡ÿì¼ÿ½šÞT\çpÇ}_:{Ó—îÛÿÞtþ}ãÃýÇ<|øÁãüß~ñ§¶ pSþGò‡hV¯ò(¡‡]qóë'Eýcþ=Jàé̃Au@[ÜQÀ÷û&îR‰Æ‹Z#שɭ[F‡û»»Úg;fícvèÉn;ìiŸ^÷ìãÒ·È5ú9—ýp‹õ¬²žý2süp ´k©D˜"H>jÚ<ÒÛm§ «;z•fzÕI¶h#¸o€÷»B¥4Õ£³£Ý¡êúÊJëêÃñbƒë«€ïÃgìóû´kî½B>?߯gG ‘ $Ù·¤X3ÐKœ-(NiÅl^R¦u|Ä·lìíiŸè˜°ËÕªÕæ¶ ùÝ­„\¤íàƒTâô r›ÀK[†Õ­Vàõ|/—áô1œ’ 'â³ãm´~Ü%ÿ'¸Vôí$•Z¹v¼õ¢’>Œ41¹¬wÐFÝÎõ¶÷vu”´¥¿,?—ýÔVgàz•ýÂôÛ‹4û&*âtÏÕ&§‡r¢ß£xëøývD;•}3T‘‡X mãvûvH@§hÜŽ(j˜r7t·±nGô¢m’1ÄÁ ŸQZµ9ÆrT¡e@ •}ŽŽÈ©rzJm°§wÂ.UTq›;¶eÓ`w§Ë®éõãNÚêÇqøÓ¿Ÿðòä@Ÿh9ÏÎ;•óp¡^Ķô3<§#õ´[¶0+1³ezóèÈPg»ËJ°}ÿo NÙjÂq:ð#¤sâ_‰Î­:[gJÒÉußVçÓùg?&B×ÉäêW.¦ÿÎ>A•ñ:µ™.SÿIvý”( •*å `MŒ­$à—ýy™¨²Ï¯ž˜!•Rˆ?ÀŠ92ãÉBÅ“dSh€úåõaü™Õ½¬ú–7ØŠ]/‚-€†_šmÜ—U¿¬žbI|§‰ßŽÄž¿h@]Z8¸ïŽícÅú䈅lõÖ¸>ýÅßòÂKûi dÕóÀ0l”…a!à 2 Ò‘K<ÒÂsìD¼l&!9 ‡Ëk ð†]ÔÚ¸?|ñ¡ùÝ»¶ÍLNlež~gÇN;/V ˜gí1ˆzÔô7ï>?Øo‹Ó{=&/¯°¹"ûñs—{¯Õ|Q«A²±ƒ÷D./#ƒõ¹é¥^<â6H(Ë®À­µ¨@¦€:`lE62<4è$ÊÏcÐÂyƒvÌâËj|8 QâÃ2%„¹Â;–ñÌV4#²¤¾ @Bœ¹³1AQ"â)/ \oê àczº)™™žîÖz´öÖlZ‘Hí `%6~Ùº…%7±•b‹JDd7²'Ñ+~!,U×.:© žüȵ[0 ưxdùýÇâÕ±¾–LM*ô°Wdü­ÐT¦f6x4¯ì}Njמå!òÙ«?ÿöý`mc{¦©CQºÝ3Z–ô97Ëbý­d?™'÷òè®/9Hª±hÕáµ8j7Z»V^k£:\-Ìœ;°ç"^ûľj•±jpÝc—Ü÷ÚËYU‰ºöL¦µ.‘¨kÍdÚëôL¢®-“m«‹ÇëÚ²™¶ºÄ…¯aý±²V'Àµf²íˆ±=ëúLçÎêµÈ~*oc²ž!;ÈGùJt˜HP ‹V¤kEÊ“%…B0Í•iĹ2-ZÔZÚÛPþ;¶o›Å“#C¸AÝ6Ó>c…èú£4Ï·ñ;ãl»Lé?oœ}ç¶nêÍGß[7õ>Vörï~F^«áçã5¹jºíŒiÏtÙ³×Øi Fû¨Ïd}—ѾYOî§òA}m<‰·à[IÁŽõHÃ;¬B¹mÁZhΕr ¼•²[ÏË[9ÓÇRùîÊÊ®\*•몬ìΧÊ®ôƒ3ZÙ0ÝÖ3ËGÔ±_ÑH^ÜÇw·¦ÍFŒÍQ WH@ ¬lT±‡l°Åº}´¤nwlÛÆ½ô¶}Ûö^´³ôî‹Ußõ§òn^öð˜aùglæyùgäú3y,wFR0°TÄRÌb‰Wž:XÚdšÅœ³µ 8âü\uúÒcw”æ(îÐÏw0ýì'Cd¯vQëý¬¶ay7oåÆ3Îötµõ·÷ÛÕ4X†šš•ËÚÚé5k;ÒrM­¬ð⎅ÇÏ”æWò«,Yß6“ß¶þ¶¾îÎ òkUn,ËìÑGÌZ…ýåÚ9YáÕcÓ¶=à´mÒ˧ô¶—º(?ëO°cZ«p™;¯p­ÝëÖm«Gmc[!öí.½ïý€s?Sš ¯2:5z>ÿ„ei׉­ÆŠá[7ÛÆ0¢ÈL?Œž‘.52¾ÃêÖ7::8UÕõ aig$œÝƒá’fr1¹mrÛ´V,ŽP•㬡XŸdò˜¢ŸúÏ'ÙéuåñK'™þެˆþ¶ós˜¢û쇵^ ÜaœÙ¬ßDܦâ.Q»Ö¡yî¹i„1›$ŸÒux >’=êÔ ¶¾ôì 2*øóð*àa²BK®îž E8ƒXGàZÓÔ”~İcjH܃™ð2·ñ3EŸû÷Æ&¡_—ŸG{ÓÒ.}žjfÿXŸ «ƒâC¾çC@Óôêy_˜ŸcL#M7ØÎ6—´#µÔN²¥YO„"‰ÓDVDEÆwq‚" §°†÷Õx5‡%2×WÙüÒf ³##=Ý#ÚÈ䨖îážáææ¶æ6ý½Ã» çÅÿ!þ˜9¿›Ÿ`p­ª­QDÉ—Å ú= Z…]\ÔOàõÕ|zF¦¢Íû ÕS©g‹Û,jɪJJsõ•­U­´‚V´57ªµú˜w³¸ñŒ. >=æEO'H?,‰œû=©S êz¡5Udª·f´ú½hU Z«ÛÚZÈ8hÚi~°ˆæ×è|zÀ‹æVkQ³½¶åÏú´f‘R\û;è D …ó¾:ãzÒƒT5åeѯf¨à7©ÂL÷ú5𠻪“å÷ë#ω­%#+È.€r²\­ª\!m- =¹–6?O¶5çB™Î¬ãŠ…~¿/—§óCù!, Çbñþ†W,âO//ejÈš¶°_?ZËF éÂzU¤ª³9§$Šå“UZuqd'(_&ébiuÈd~ßɳ—ÏìA)Ì\—oláÂéºÉ!Šû~°íñ‹>P‰ü ßy¾ï¡EäË!ö›ÌÿÞ‚ø¸n‡ïc6ífn‡ÉWàÓ—y ƒŸ¨PŸÀîj…áwý“>\—rñaŠy=Ž÷»ùƒñ¢›¹¢á;³fc°†z;"ˆ>Q8á}‘—¥m]ysMüæðw ð÷-¾7‹Á@Î…XÅ_¤(ŠyMþX-oþh|³h˜rýÚ.ÒËg““SÕä”-WwíÍÝüªá´Ønû*‚´¿TkFŠÑ>Ip_¢U©yÜq%X Èe¹ððqžuÙ’Ë׿CÈŸ—9æuE£\ú2­{ˆÝ-±CçZ/¥háÊà[.u÷ÖâÚu÷ÖPJ ƾ|7Ïé|†€Ï¬lð>õO:Ÿµ~U0 üÚY&¬ª¯N|ÎÖÈM¿ÞØpòÝm-xÓE~K±4€MíxKÑf-2Yín¢(æ ­/\Y“BÝ÷«ÎDÄÊÈ`}÷¶‘ŽÄ~pî,½_Ø—©zïÉÂÑwT«‡öôÿ·{ ¿ Ò=æü°hþ¿F^OŸ+Sl‚KÏ}÷­`¯!6eTú±9émwwq|U¦×מ5ôú“ú“§6ÂM±fÛoTÜx*¶ãê¥ÅŽ[—^ܰ;ŠÈÏy)Íî"÷‘³vJsoíÛyñSW•n—N«¹›«bmÀäÊ eK‚çÂI0ëaθÿÌ´ÚqÿÙ®Õ쩤;ßÒ}Þþû7FlØèc‰õ°.;ÈlªÔz/n<П}ë±\¶tën*D%Šº\Š=Þ0^)@@èÃl"Ë8kÍè †ö3ˆ·£qŒbþ –Ës”<žYÓ©ßñ¦çÜaœ{„ ÕŽ»Ò0—·¯þLø!_'oǹ|‹™ü6P<§í§TªÃ4-¸<–o°cÂ/L¥KõtÑÐR™W+å´0;11±cbûÌÔÖÍm- ’UùFþîbBÞçi2Fö’÷iÑÛYtµ_²ê)1­¤b‡h\®Ëa,|§X:[çV¸Ueu Y¢'˜xî½{vïšß2ÚÚÜØP]™ÏùãTæç…lû\vˆŸçÂ…°Q6òà¶ÝþT¨"šïŸéièªh›>täÐt[Ïâ}‡NþúH»š‰'šw÷ÌteÚ§-šnïZ¸ëÀÒû7‰E÷‰û2™ÊL¦e°¶e ©¶®mËüÖÙ³ }ÓCÓáHu®ºªsKCÛPsM}ëø‘Éͧ÷õŒ ÿ¼¨®!gûØò¸ìQ-2KUÙ]Cè} ? Äî=Èæ†š]ä=nh6U&K¥$Že%´ ‡Z ­+ýß}´éá×ÉÐñCZÀÒCòô®?¼ìÌË~#Å ¡êy9J™šœ¸hçÄîÉÝný5äjÐ Zɸ¶Å6WÊÐ= ¯ìÀËÏ„m2àQÂnmgiüöÌ{Å=aá]«7µ†ÅìÛÁçÉ›þdõøz}yÚ–¡Ã»/àëodŸ½/Ì—DUž§á.rL»Äniô‰o(¡t̯.¹,N Øâ`m·Õ Z4üP¾ËAC#Î&á/‘ÿI~T<'ðjÞ*¸_=aR ~9à_Zwjð)5Z4¥Ê…׆K®3ŵ̱¥ù9÷< êóìY·ŽÁX03ÇQ»øÍéšý)5/jn׆@þNi{á9±tï yƒ;èH •!Z'âNªúÑçÛ¿ˆF=Ð~X° ²$à‚UÄS¶ÐÔxW…›/¶gßÿˆ¨È‚rŠ("„6xR\›.é§Ð.°là§Ê0n·é‰gñM9“ûöîÞ>;9ÑÛÝÑÖÔ˜onŒ“X&®áY—R<"á‰_¯Ï­íZZ‹ýQ×Ýî@AQ×ð,“E®èø.w‘Ç~‰"¢^eQ¯îÔm×çéxŸ%†É)Yº""Ìj¶dU¾ÕæóDfZ­¡æ¬RËÏ/Á7<,Ew#â¾K÷ x&ô„ P*6ÀR/Gqßð°l§hDŒ\†âŒðp‡oµY#Ýe@À×DUÔÃ¥!1³DMs“@ûzšú›ûëj*³©d"‹(Í yvõ˜åx.<Ïè'¯ð{é4L)ˆ/”¾®PÐGä­`Y©âï©Ï¸,ØET/·A&§0[±=1O“¾`ð1àjÎ’…Z†éÒléÒ7à àúºÚšêJ–1tœŽ³ë‚Vºtï´Ç¨·z ×(µ§N§3fîôÖ§éŽGºªš«’ª?(mÊêKx%R¿5pÏ ®LÇcc‘DDõ/Ôw¨š-£:×°ïó<46yì$?$‡´ƒN1•âuAU–ÖjnèhsqÀ8çk{êÖMþäí„ÖJôZZ%m˹µú…ºqU¾‰ý’² Íù[=ôÔàì‰iòÅk±GIKsSÓÐ`úVÖ±²š”Vfûúúúš›óM‰|œ%ªH:vpÿÓyãö¯™¹¥UdÉ*Ò#çÍAþo×`‚ŠoE Ž‘ž¾ôæøï³4ÿòk¸?®,|%[¸þekЗŽ¾‘ªòùþþïùjXý™Oå1IÊþízæ™OÂÏk‘:J•8Ä«—Â=z¯%™•Ô_ßbæ&e-;{ÐLXý#Ì4Xsâ`"ÀD¦}<Ìð¦®ÚL›[óÍù|<é`tšÛùºÙFóg„zÀñëÞ4Bô¦È¨®:%A^ìé)M÷H¹PE´g[[0»`Kok¯þEÿEô¿ ¼°ý*P¢Ú)a£×£¿L¨bú[€þ–Þ–žÎvý`ÿ~ÌÒD†È_iÉÞnA•1%bœ•Xâñ÷ ŸºúçU­îƒÅDüdVAÑ¿&(ð[&”›_–ÒghÓ`k3pÕÄÙ‡2,­™ËôæÝƹ(C§ø£s…Þ¡ÎÊ|e‚MѮۦ®¿íšXúmb*~‰xqÑ~ÃWÞÐDÍnšVûyW l”ÿûøCÐ^’¯ð;ô¤o‘xëQ¿C&˜íÈ?J‘ÒåòöĨ,@—`mS RUÁµ¤5¡RÑ¥ìIŠkþ‡-ýû¸”†‘j#ŸÖW1è„sÅ5R"³UŒõ…Oœ Öo4<ÛWYjtÉæ10;£/ 0—[Zk·Ö7¹uw‡‘i%m­-ÍõµUé”éÔ›hSÐË©ëKsùA›²ow«ÒSnÝ­>RÓCO^àoÜmvæEúü‹õƒú2†9ôχÙcesy>Š— q‹”š·Ã|ë›üÁ¥ò µ±"À(UVQv…‰”R˜z4¸c£#Cƒ}=í-S­S6ʼn±¼f('ùžœM¯@¤NpÂ-~ãÀ°\aÐ^«àf‘Ëb4•ý+EÖ†ÕFË+–B5JadŸl´lmÝj“@$×iúû—‹dpŽþoÀö—Z¢‹d»LS–U‘–Mõ»=aiSŽö Ý9¬gÊË…ò0å”nèkkq{.ÆáÛ¾gœñÖ¡ç_ßmÄ=´ÚÛvΩ(g"‘è<‰Fp&æâf&¢ Ìã<ˆŒ¼h;7„ËÁ£ Ÿ¶­UœÍ5p¸lœLnyTžƒ0P.1züÂd-²+ CòsoÙmñæ×ÛÌD=$6Q§œœX´­%,Œ!§_ÖÀXñô«Ælá>Æ’ŸeŒ Rl‰ ‚Øš¬QR¨>h¨²v3æÇ,ÓÁ¹XX£,ë>YP}‚>֛˄cãÀ¾¼MÄF¼–Û?:E§° Ùæ‘¾]’}<ßiðòn¾v³ñòÞ-y¹/›I4 DƒË&mq‰Df8…kó3¾X/žr3Ó”îÜ>½{f·'_ ‹¯ÿ^Ä× Rlm¾"@[ÄAJº<¾Ê‡õäkzO_´c[I¾Ø^ÝÏ”jˆw42Góœêôî]BDÀõÁx› ú'ÚÂÒD/«l'¸?WŒ¢Ñ}PLã2‰Å",×yá²ËéDU…5ù:ÂULˆ­c)ȪlX¼ae ˆó… Xâ6Jæìß‹gsÛúûº;[šrõxU¥"›Ié«j ¯UJÉÅ 3®ïŠÒyÑgn¸Sß¿Š/Ò—Ø6–p×¹Â&ܪæÖ¥ûöé3wæìßÜ!ºWÉ®5íñZù`ûgß­®µ3êù¿ðµ³¡çè—Å&î—}ó°vþçRú>â²-1ŠÆõ6ehlÂSÛ¹SÞZ6x±ÂÛ @h0YrL–’° 9JO]qô’’Ö-™7÷¶¦È°¶Æ¬ _ç\ÇœS…U“uÎÓÃõ[“#æ#˜~¯„rÇ åZÆ®Ô1]©µžfamŽÿòÍvÚßœ ÿÛc±^žââ^×W%Æ×ð6»ü¢¯þxé)®â'LU!ñxt>É4,Ac±À|:m:“*¡®,5±2/m‡6] ž¦ÀŒÄœŠ›ZCqÛÑ}]rda~nÿž‹¶o›>6sÌS3ùNC^òë|}k“×+¾vÎC^hÜ·XòŠ%afÅ"KÎ(±†¬¶@û@t¥O´‰{J©¥4pßž;¦Ïö”P*ß©ëSu‘|ÎùŽÞ_h©4pÊÈØÿþ:fó×Ñâ8$¾†ˆÐƒ"+EhÊ@ oÖS@õ”ØW"ÖŒ ßV$gœŠ{¾*¾7€ñ*Ý\Ê7\T¬îzšJ¥çI:m ™bg² ËØtî ^û¥÷;xTgîz0»{õgâó|MužÈ¯éy¤?ϲàÇT—Þ1²àÛ2Ü+>A¯Ñ(,ˆÔ; ¾ÑF¯˜`mÿ¸²à7ç’MüM”7=/Э^ô8Þ±Ùè™·ÑãÙÆ gÞ¤ÇùxM§ÖœžÓ¤‚´“j¡|NÐ0«ÀbŠQŸÉá­GÕC*FáERVðFˆ´âÕJkôhàÜ¢– Ó»W³@gÞŸÄŠŒxÝo$>gk[½X¶G]Æî˯Üýö‘s+‰ !6ù–A]øÃâÒŒ»îöŸ¼Ñc¦Q®w×zÖe³Û>Vì]þ®ÿiX§>é³z«È;X^ÓÓx5s Zµ–ƒßvÃf©D¼[É©ïzpdu¼. §Ÿ¿ë¹â<U«®ù®ÆS¯rſ锭Ҏ“ZzœØ‘ªÇÐ<ÄK xޝ§lê§s¾¼BžRž±dßÀž*–‚µoq2øKªj# V^1Û{µÄ"[žŠU¶'{gGKSC}Uou¯1 A#—=çá³.^%߃Ÿ?nñPÃÞ®*»—æa©]¼µˆW/ˆ j½dy5ÕZKµòÔ¥Ñá¾"]Òyø²Ën äw‡ ¹Íeµ¸jÕ€è›ÕònS¤a5MMh·šº›ºÚ[Ö+ôŸIÇ ÏŠÏu ÙêÕH²­-âÃUÄ“’ÕHPŽªm|Ÿç9²-¥ß£ÇáßßÓ3WÁ4ØDU¥…Ê*£ËÌc?µ²Â¾Í4ÁÞ )OÏÖVŽàN,꣨x±´ø8Kî¶Æ ñ òu–ü¦sž£“UI^ô³ íx/*…%žJøÞεšùàv£5¿ã ʹ,i?+™š²¿AE«ZÛjHëåJõ´*]¾/Œ»Ó¡u§L-ay„3gÿy|¨6ßÅ×¼V ñê |ÍëÈqô»NkÕâ%UZ)ohzÖ(.€9@GKP ºã²Ó¡„y.ÎïN~åvòaù1~{u~Ÿ~¿ªís†1mhº_Y±ßÐ •fzˆb¦müFõ«GC%™–ìÃt],«i«5½ÂvÛ/Ò5ÆŸ!wÐÆó¨Îóg<ǘ—ÞPåòÇxM¯16ÊãjÛ;—`ö1þ׳xáÛEñ‚H^$½¾›ü–“î6³^шÌ`Å=eÇ ®»s]Eí¡„FkwÕÚ“”Ãë Lq©=}`_Ÿðàˬ*Æó¬*¦ñf¯*&J¢­“ïªb†ï1çÆGºÒH^ç«öÛi3ù+Kâxî3¥¡`h…åàJQ‘Ã+r. ôÛ€MÑFŒ2†G½®S<MÃÃ8"ÓÃ[7{K „`øßMÿ{¯ËÿòY)áó3¢ö2X £tË¢ØûÍySúoàM!ާ—šî“§}Ⱥ> úý!ièw7yZ 6¡3T m®÷«LX™ž¿‡åi»l9A0ÿV^oÆÎc±-{0•v6Õ:ˆOŸ¸\¢5±/šõêš››sùx2ÈÚ%<È^Å ›û}=Â8uHÝè»úµ†žP°BŠtîjÙ~ÙxõQ›ˆm’{ñ[{G™í‰xÿÂuc£69Â|K‚<ÌÏZ“8Óä5Á „\óRZÓ,Ïžžâmj®8¶B3z: }Ó·^Å:«!¢É¥%{‹E-Nuu´4çsu5ÙToº·+èï.“°.{˜Û#‹ú“³ ñœ ^E7*¶5©s% Ï1­äN>Ìø©ÖK¬Š„õ°¦Å‡‹ËV W5£…Q•ÐÖÒQ…Ðåã] J¦3‰j“u˜ó‹oÓRÏ „ÉÇR5ÒŸ„ª’Œ|_®Ž¿«ðõÇÜ/eÿPün&ráÑütcn&/œ‰¦ßxÑYÇiB¤gù~Ae÷ßw¥XBrXl­ð‹"¡ÓFáQžñV¯íãoaûz‡{GûF;rc9µ¾s ¼/PLù0¤ ˜xmuŒÜx{{{G{Gš8ÚýÎÇ{¦Á9ª…G‡Iiɪ">Ùå«¡ »0?ßuâ¬Ö*ÝM"­8[ÔñŽv× f‹‡Bÿ8¬L¾±Uì¡­ªˆrgñ]žô»R5¾Ï"’š ~UªJìIUJ¿L¨ÁÔ‹RUòþÂ?Røc5(Iõ;Áš„׊7?3Ñ ïmœjh˜Ê 7E“Éè…G§¦f„b©7zrÂãÙîl¶'{ẜoÙY¯Ëh·]ö,nú'ýÍË7-¼l ˆªì‘[f<‚‘Zr‹,ØÖÖÖ̆ÈÏÎÌyߺÆý5vg¾Kk·ÆÝHéäD¨ãRØ}×táYéYñûN\˜Eމˎí¡,pƒéÄÅèú¬‹®Wé× ›,jXÁn…`Ò Ûæž["uöÍ:lZ$—¦‡\²`/Op•EK‰õ`ê´êLŒŠà_¦¹mÐp[c{£O!—'{¯þ°þ^øûß¡µðV†i'¦fÄ%7Øqí¶ãboGÿX§'A/æ^6Pr½¨´'Âuà®Ö²Vú[Ç/0‘P+°kµQcžûúM࿟ܥץïˆè§J·°ÖGËÆÖZ_¸ŸuZØ&—eµÚ9ìk -6ÑžƒYígi‹’¦yMzÚW|P*+üMéŽÂ_>ŸJªäó«Ÿõ§"Ï~ø£WܶöÎT¥úm5ðªšM~Œþ ýa$TøX0¤›"±Bwáלv7¦g²±ÂK‘ ŽI°p^|ŒÛÉ ŽÉ»˜ýL !ýZOTfáñ‰ %ºŒ¼"–猓ÙÊ p‡¹Ê\Wc^æyi½ð¡Ý…¤Oë60:@ª—²c¬¨¨ÈU44!BvîÄñ&ðŒ Z(W/ˆRD²m>UÊKÑe}¢X¸@Cˆ$J+>¼é½bÿeñe°´]rº“•™OR·µ-ÊWD?¤VWÝ÷|ááf+äÿªF%9ªþ¥\ä·å,üu*Cï?îï£éŽt¦3-\M?é´‘œ¯Ýv9±¹PÐç 0öž×ÅŧBÊß7/³XÆÉgÓâœO\r°©ùq"€0ãÍë—Û Ž4ö VïÈ P¢}LO;u„ör̢ǢKÿ²àI.ØjÛ¯ü{™%!6(WVôv·6××V VBߪÑ÷³½†>|~ àf±wë½ zOèÝÖ‘’±h`‘ׂ>+Ï3±@ìeòwÄ5^Ìr­Å_YÁd|Ý`’ñš®¬Àw)˜K«¦Zç7`—õÃ.~¿G{àßcúþz¨û•¥&ê“}Öþ:û©IðIì{kÝ$å âÙÚÚ_oBÞÀì-³ü$Ç SçmNO=µ”||gØ»Y‚—"³¡¤§JâL•ƒ³LtlÜŸoÉäÍùaÚ›4Ø›*Òª51SÃÜ{HêêxLˆ”*eBØmÔRVãÂv<ïñ6xÜcéƒ.[úËJ´™¶TÔ³ÒÛi3ÔÛÂóYžWɳ˜ÃÈÒðv8o¤‘eOÙÖ¤+Bl¢7tÙ ;þ}ËóßÈæùØßUÕ†ôîx?*x}÷H66VT4v6v´6žÑ¯Çfc `½ñâ}DÏIø3Ôú`žˆ,íõt¿‚`¼…†fsÐg˜‚ÄãUñʦx:žV¸®pœ§8rVþ'_ÊÚª‚)ô‰>Y4ªza¯ÖŒÚ¢¼âÙ¤o˱þs¶þtñôù27mEm¼Ðµ5#*#Þü4!Ê?/˜OöÓl?Î_‡;ÖH?Ñ’2ðíï½ÔÞÌ,áH‚"ãѬž})„½2Gðõ™%Ñ’Ì7|X£¥¯ÅãX¼>˜¶y}b0§fÀ‚ÙŠbýG[E®-×ÔïÉ]´úOJNzewr;ÂöçñmÒï ïÙÆ¸¯ŒMÐP M%¡¶FAOÐÔ}¯²ïûmßûÍï_ñ„£‹y/ „@HXfö#H1‘Þƒæ±4EvFŽÈŽBâ!CVeÌŒXP°ZAK_VÑáýsÄïzãŽÙf€Cb ´LB‚N14e"@!ÍnA˜]eŸ¬ªSe£Á„’µpðóݦªJnß6;½etdxhSoO·^¡!›IÆý ­*¢Ucs>JÈeç«*lMó8E˜êzEÓÓܨb*úJOóÔ×”’é¤)X̃-Çš47”+N úÓp ÐOpáx8H¿Sž¬+Úx0™\ØöЮpcLxí¡'‹rzŽž~ÊãFˎП¬^®Û‘b*í5Âmvå³*0\Q¬L†«>XqJ‡JT`—.&YM±"¾.<Án}8™ùųgùšu Ö¬ äœe Ò—*ôªR¬²¾Jm€ždF`0æô‹©3üjL$yÙ«©‘›¯o°ú1×n<ƒaj§íiÎKqV½;.|Õà9‚î…©ÎÑEŒ¿/Ú—¡8FwŠW Ò Ì>ÃßàgÞ ýš©…¹ÊŠÄ„ðtÃmX-Öê`) ¦\2¶¦#3 ÁÊ`d9JØÎ§ÑTœÃâ’âl¸¨Uµ¶x!«¿µ¿¡®ª"ŒEa^4 ͼ6ΫõBmBº-(P÷‹/’Ú^'}îÞ=v$LÇöÑÃÊì=1}ýÈbEÞÁdñ&‹Ç@WjQ ‹‰ÚAòÅ)5ÓöÕBFÅ½æ ŠG LQœÕüw`kâò ÍøuQ‹TU‚!h®jŽ7·å~~µý‹Òõ¬ÿ%Öÿûïû´H+@f¨ f©O0ºo4“]êhYE$+Áe”åYïeÊi´”@¡%ºTÔΆLÄk×§Ì–V£ÅÅ—ÛšòMù<ÌH]o.ÒeÅõæq õz-Ê,e$¬-¬+Nîë€,tû êñÍcâó¨Ò²LkAZrþ¼¨eÐŒryY†S5òÚÝ ´Lër»•Ñò~¼qøRÄEuqÁŠ X”é/‹„uÌJ ¬~…f0[— ¨b‘5oä=Û¥¦UäÚÒÜØ‘ï¨Ì&ã‘P@¥9!§×£ºô{šæQ¿oEý~?Óïmøô‰÷èD >Á‡)˜§{·&~M:Å×U2z¼F·v 2‡kø²Ûàü¾‡Ÿ÷âϘ£‘ø¶-jaPðjZO6ç³ɲ±¹Rø=AÓí ü ý? ºÖ¡ÅTª¨= ˜œ[ÛIDE ñóI…¢" 0!l=LXML1wô²ì€-‚÷ƒŠš³$ã§Å ©õ€šÛ«„ªËž`Åh]þ¦*D]*‚´š³¡fÛ¬@wïšÝ¿mÿ´¶es®¾"‹øD:$ ™ ü• y>-žëöêi6p×A àÆdl«‘­;EÐ D8ddÈà>s[ƒíÂÐBb}aŽ·Íægh&ŸÎ'd}]xô!ÊÛYW°þ>0»xrผÖK,jÁŒ!Âúпöáf6Ý‹?`€@ Ÿ]ÉxÐÓÕ—ªÙÜ®]ë}T3½ø ¼ä¥€~öŠ]e0³ëc‚(É̪b7já/:£ËŽû”,`Óà¤(­¯Í¦i‚&š[d–“ùm Ǻ߯øú0ôyì%¬šanAâ»AÚ‹7Àª/ùŒòA·$ìW0%t¿Ñ£¢yfÞÝÁ-I ­‰7·45êkâųä§z¿7²~?ÂOO.+Á’ü…B«‘Þo‹gÅ÷ê¾áÛ î£wR †dÁGòìäS]Ë=9;ĵ’J4>2 À?¡È|9|+αFD„Ñ6Ѧ|²1ÙÔÔèçëè;À/]£÷}óKOAß{¸žeÙÊÒÇ•™Ç‘aC¿´„¢¿¨%°Š`ueº.SÇ«ÊzÜrøk¸¹}ÈṠôiÝ&" W×Â~Iî(…ëÃÂw„ÝÖÀß@ó¯ãk-Ї<5ÕA "óVI@¶0áñ K¬+òMgŒ€öÃð ™-ñœ(YYÏo*š*›h–fÛšXI@Âu~N§…ëÚ3@ËÅ0v L´yè}â顃í³Õ|¼èëQè 䪠Çÿ&|]¥eý¨ŽÓ,c2å;€ñ®WJ¶€•=ƒ0x#¡|j¦½‚fØb?dß ÈUÅÛ|ÉNšaçËÊð ¾Gá§=t7)`…_Gqß]8Dµ?¬lLj}‚ôÆ$#t¹ðnX-ÄÛµ†¦Ñì—jâÂ3©Î|ṦÿÑÙT{Œ^âÁšÈ¾Ge9Õ[WøR¾ùºæïÇ8_—#_ß&ìtY¤”àKK>A™HYÉ1Xm5`~õl:~Ú"´øüÓ_\A¾‡Ïvt¿Ã뾸úiZ«… š”¡z˜‚^—¢Ñ¸+˜²Š¥fxX€o]ÃÔ8¬jb-Ñð¦Æß²±¦6€µ¥L¬¸Ù? ²²hµv4á»ÑÉI,àݘïìÎwtø³ˆmL~}(¿m8 ?Ò£u¢Üˆ.·h9"}o}õ†gs¤‰Â?­þ>ù¼›Dœ*Ã)êeÀ; ²ˆzB6.¦fðO¯ÞÌG BX.Dk1ZE$ÊþR ±¦T¦©Y— {³¿š~~Ãä'ü|œWw/<´úûX)|µþmôâ ]_}~6ÊÁ€ ªÆ/›mD²ÆüÖtqãnv§“*ØHPÔ¥5×ÏE*&ÿT‚4 *ÅÒrë7Åï”n¢³|:¦%:´théÈŦ0Cx×h5 ²ü‡,¿¬Ëò^¡ M½ãW’ô”ä%Gì’dó¬d¹ksžíBIN.'ÿ€ómµuõk´G‹ÓeqÌ·A9„¬_UüKQ*¨*²{øHYØ’æ‚Åu†%Óe ˜ zC°Ý,ý§7lX‡ Ï“ps;¥hbÁ\u%°HÂûKÁ¢0ë´Ék¯9uʼnã/`™];f¦'µE.טn¿ÞÃì×—ýš\À~½jé¦âg¸h('ü¸EÈJ¢¤fØß%Ô§A/“0 >ea=UÞž€OT÷—l‰ïà´Éƒ.Ú5y±v1g.ÐÀl™ÈlÙ"[6Y¸E·eAàòzq‰3p0h£4ÈûçÓ.ÐyÏAd¶%h#—µVçýìÙŽ»5¸5ÁOØ BKK|€$Q„ºÄlÒªÖ&;|ì’#Nñ„õ±ƒ|þÈ!Ÿ×tùÜÏíÓ}ÿ7H“¯¸¤³®½™döÆgÙ›°c~LBp*°ÛÞD`ÚÆB¸d™'‘Hj†ýí9óõ„¸MÑô/¶e $"’Èþ’À £Æ¥KÀ†\}å—ܸt£—‰3Y=ÅlȔˆh…®~ƒÜèŽEëÊŽË KÅwx¾éAÓTá+ÿŠ45¹cÎÂ#VÌiÒ´Ÿ~Lݧ8MĤ)ÄÎúY••ž‘(®luËúYE{Ö^]^ [ëme¬‚y˜”Žóù¸#F¼‡ñáˆyan0>ŒØ—^«Åès#hÎ äi+ âu¨:k¸»˜'>ù*ƒMœ^c|‡z#8pns ¼9$ý¬ò€ ˆ$°¿ xmÈT ?-½¤Ñâ'.]<öqÇØÏ’8¤ø EB¶Ð%a.w¢ŒÞ˜{¹3d³@¤2bèm\‘å7 W`Û92â/‹ ÕáÀ:Øb:¶Ø<‰ÅR3QÏõ• /jÊî2ð’˜HbûË@©í, ‘H(r8Zzˆ7à´É;ï¸éÌu×\yù‰ãK—xErIc=È|û¾­ éí–ý8thœ€ì€áÛCxœ'X·ý]–wŸp )·a, ,gXl3wÃhúÙ]LIp9´aoX}ö—@¡ûøô¾½'Ž9¼÷Š}W˜Ó=løy‘ùù%ý¼c}K¿]j´fõaŠØ$áTq» @yö~›Áã/‹Œ×@¶#ÓeÄàƒ˜KÃ7±‚N¦µ"˜ ,EÊòqk¸B0\E4Ûiç°oqÂëÃ]c¶Ö^yå¾½W^}åÕ`ðÝ:3t :àµvß/\«ë€¹†ÿ•ü‡Õ€•eO (Ûçó}Š¿³|~Ì1þH ,T¬„σ“IFèø<Áĸìïu!šð9Þ"ïõæc,x#.éßfó=e`&q‘Ä÷—ƒSÛU6:Ý—Àª»ƒ¶«–Á…ß~öÌò=WÝSÚ‰§Ø¾'Ù»úO¾»¤£nìEÝø~6Hþ+hÊ_¨’E~±›±<¨Q¡]–yò‘ìL@‹ù ì:™õ2¡ŒÆi3Ýr»½±^A¿¹ïÀÍÞ ^)hÍ–(‹†©©î.Ní˜Ú1;=:Ü¥ukm-u5™T(@;…ÎP•M8e MÐ AA ÍREå‚0ÎG;‰_?„¼È·ƒÃ NFÀGb# iS‚]c‚2`´>æN¹@ؾ?Šga~ïîÛ¼ÅSB>¸J$ßà{wôrø÷~>5ªfÆGý-ä1SEf"AÁï7>ؘ 딆*µQH.ÊnnãK@:%T Ím¼?( ÉêXÀ³”‰DÛê€'8[ê®ͦtËáÃ8N|¾ÔhEÙ½ÉRcõy¾§I¿ð«±úW«£—”1Vòý®±š&wÑ.f4«K´Ð¥4qÚŸ AA8z"DUð«p`ýp$fžÅ!è¸NgÌa’~,–•šäX0õû›C£Í”‡Á)z”|Ï43ö×_·|ê²G.>8w`ßî]Ûf§–¦—ŠG é°m¹lÿ$Ú~*³9áwÊóŒ‚ó95Jõ2{ëÿòa,¹vs”J9@xŠ^Ü~]ûßÈe¶ppÏî©#ÓGŠå.-'\9/è>`;ü;Éj‡Ð¼íÊ`„`µf£A33¨‹EoòaдùXq¤4è:Va(b •–k´ŽÅE.âÅK/ƒPRÐ1{,RJÎSôܯ伾œ-OÎ%mï$³½—1ÛuÎÿIkq[Í(«$ ‡õºëß_ e%4ަÈv–öå¡X×þ;ª[àkOŸ:zöØÙõlpÊa[~µ¹KºÓ処Èÿ¼ø¯ ƒ °ïäiûì:<qð$HÓÇù Gý0ËÕùOV –B™‚!Ù ŽèD„Üm{JSÑ£LÑŒ3œ»`pUÍU{=•ޱ;·DÔyO“2IfÈ /÷WJ6~› °d BëàúmWm¶fë _MW'%Xšfd¨s²k²µ¹¶: úIíbAÁ¡5"Ú{fG†·R1BÓ©:a+üWn˜&i4Šø¥˜Kç|R뎱žpýÞ‹/éžïÊøì­Ï:¶_«HÉ­W IJEÇhÖ‰ _XÜѧVEzµ¹þ­Wèmß~ôU|Ñ­¦;gzú¶w¥gîþâ™Û¾úஊö͹SMþ¥o©¥cksbG¶ùÍ{o™ûçÆÉþÚžãï½üèý·ùžuñôÅź1ÞÛî-<à»Kü±Ë¶ì§øÎ¢×%¬à‡PµTL;Ct]é7’vÔé1€ø—Ë„Á¨¢Ï„Y#ªuÀ±šƒ(įø¯ë"{.Ú±mzª´]Cy­±¹<õ}#ú_´ÐA:c‡1žlã„!0Ü¢&Á`¤¤óöFq¸E™`‹d;ÏF‘°"œfæ7‹¥‰]#(e ›ìçÉ"$úx—…‹'Ÿ8ô—\Z<4_Jôu{éñwï‹ ·ê{-ve¨fîX}Æ>¢XÀ>2¡ñÁ&Ô˜N~´„޼IT^£<ë—7 Çz纸œ£´¾nvŒä1ÜDK¬MýPDˆ”Ð!)DÊÄ©]´:b`c·º×C¶ÈÕ³ëÊ+QAùaßÚjšX×N¹÷¥§¿ÒÓ·^OW–Ë×Sﵸïš\¸D ]Kcq§_ÝO1!–8+Ù¨}Mfñ}ã{¬ÙIâq‡¤tú“\=õ¥neÔPÏ·mÚ…6ÁNRíh£ËoÞ:v’ÊðÂP¾…ˆq>Úb§’”F®,o˜DÃÑÃå¡7<ù&ß}×-7ÝpÝé+/;qìèz ·Œ±ÁlæC%b;¾/ü#fCN'AˆA‚F\¥ú¡ý†‚» #ñŠî&HüËÇÂ÷õË0lM3ñk );ÀÓÜXt+U&2C/Úüÿí}˜¤Gqh÷'ïNÚÙ¼;igv÷6çüoº½œvOws{aOánµŠH„î@Â2ÂÆØDƒ…ý°ƒ@œ„àñ08`Æã„ý²ÆÃgK`“t3¯ª»ÿIûÏl¸ÉæûÄÍìtUWWWWUwWWd>ÞʉÔÁs‡Î•RK•˺³ï¿g²ù‚am?…¥Zñ2Ú݉÷y$§Óü²º]\VžµRWŠ ù°{cd›µ¡y¦Oº77é 㦓Ñ}þ<—€ó·œ¿ŒÓràß’ð³ùÿ–ƒŸ79¸ymKr°¡ŸÂî¤ë™Ÿâ+´‡ˆ ž·„CásWE++}‹ÄçÛ’£r5ðZy*‡ ð®³ÏÛCÜÈb³â’–{˜Qà®Ù"æM;+‹›@,œ•Mâ7­ÒðMkÂ_¹÷®—®½ú¦WoÎc åEÜ[Â_1È_Á¿·ñ m‘ØPâ:ÐB×lÚJ» ™‹xëà1ö"̼Ìå`¡.Ò [Œì8ÈtÆÁ£9Tæ„(Ïf”šv¯óßórÕçeéÈææ¥àNŠDðN*{"þÞ¢Y™ZuÝM“¯ø¦©à¦×BMo“•þœ5•Е¡Šå£*ºþµ¸+ŠÝC—Ä"¶p] g#x6§ÿüLÿË|OwÇçp>ÿJÜÿ5Ìë; Ï4¥z?•¥UX´0ŽÕ&Ÿ•YU0;®¬Jò2Æ(˜‘“‡w¡,‰({¸ÔØ3bPä¦L–€)Ã?33³kf×üìÔD"ÚNF£NžsŽÓ±~u’výO¾yj³çA™É>|±é6ÇJ©¡`¾kd=’)Ž@_Ý&†”á=ºt`ß®ù²;kü¨Ø+…_ËO²,WÇþè,ücÖ_JdÅ’P' /q»$L¿XÜ<‘+7ê- ã'r—îÚˆ•)ø¼åq{6…Ž˜ØÌ ËIÂÇ?}üôrªhº<%åÍ×o‰=Í»áßÑ-Ï×)›,>µlÁâ)ZãSl?Å8è°ZãSøÀf/¹Æ1`ô’ÖÃ48†ÒÃÜEÊÎÎ]Ü¿o&5›ÊŽËUz\l?ün±Ö±\áÿåVÆb¥d'ÆÞq– ØÍæe <.Én7¿”àÀ\yD$‹§ì 'NÌΜX9±s\ÌŠ<½WŠÓôñ_<^œ9U–?ï6ùÞ¢õ:M¾ôŸ4R…ôo—pÞùÂ|‘ýlK?%ÿç]öý7‚wïÅÌòO…ì/»Â²ôe“Š`>NJ•E̤š«öë͆¶Y· ´9òMO•h‚ òš››[š[ZbѨ?êµ =Àh–Ÿå4S i&{ i •"0ÿ—ÂÞžæhüçËÅB¾€Ÿ´Zÿ®áŒRMgšG-IbÓ¥ˆ ¶Ò,òmоˆQ­¼©²ºa[ž4¯Up+Ún=fo:s þ­ç6/Puì›]|Q#¶yò³·•jÇæÀBE`=|éPæ°8è› þ@b ûZ’u°@ÇÇG†²EܰÝ«˜å›£âyóæ”Á–ÐXjƒc(Ô[A/¡ÆÇ@!ìÛ9?vdüˆ•Jð6ÿ\ûgÜŸÍÓaÓ4p7næÍ›%%™GJŽ–ã´HêÌ5„üA!%3,-Qk‹.{WN6n ªˆ«\d\LdÀ«[ÞHNªÃÍVÒáÎêÖ‹9_8;¾ƒB¾\8>Ì×G@ï[ާ(ф˲}¨dû˜°+О¬n Àòû±¶"ýD ýÙ‹ë|±Ž×ôÎé9ÃÙCíŽÍ1BœvÅβòá˜î]Ùþs³ÜØ‚ÒìA¥3Z€F¾% üʃaPV·ƒÓA‹ÔŠå‘ˆƒrï´1:Üß› Wù¹X·Ãø®ðŽó&¦Ž}sZ5#Ÿ¿.~ÒK§¶ùéÚ<žò<Ÿ.àõ6ñtËlß,¾¬;?OœnÅ]b6êvo„ ƒ¤Jã!&ëPF.RFÍÜÜÜž¹=àÖ HE4«ËÉHþê¿eä[Fv/”•k0O·£WøMùVTèöƒÄ[¡T°„~$ëÕ奢«¨`ùòÊú†EIw\ÛCÚ-ÆÍGKV¯ÞA¼ÊêÕClØ'Ï´)ÔÂ’ÄÂÍ'—.Ü¿snzj|Ìʵñ7èŽG,m=ÛÓÛ˜‚pÈÃ(q€¥s˜¦ÚE³Ì›Õ ›ÁPÞÚ` «[DcﮊÜúÛLXn¦öÛ‹˜¦`WçôÔÈpç|×|v½º£ÍKñ®žž»Ù¼I²ÔéB‡®xu.ö’ /¥Úæ§n ˆÊ³ÆäÛ`bÉÚò™ïÉ1?—e­ìqZÃÎ];÷íÜÚ³x>*77y§ ÿ=Wm>öîÞ`>6°gü”ãß™=óè¯C¤´n¥•áa)ähE…xš°5ƒ¶¼›±h‡ ð‰m#Žñ„zˆØÚül3˜õ š9ï6…[hËäžÝ`ÖN,-î^Ù³RÚ°ò÷ì…ç#Y¹0ÈG0PáÞ½O¬"Ë#øò °,hù%×o®–-Á=E‹l“÷ýý=]–{Lk^±¥ÿº¼êë±äÕñ"^Í\#N„èÃ…œšÊj‚¢#¬ŠÒ_zeÓhB%ÑðÌ2Ù¹m<±9¤àlvƒƒ¶B1^ ƒP ëƒÆpsi-PÉÎuߘùÉ®>Îj!¾‘eIý« ümør€ŸÒÖa,í¦[ÁËÀBA,YCd,T*+©âßR†Ëï÷×økZâQ¯¨7œùôwT}O6'ëר9òwÒÿßnåI$:UUmÑF±jˆ½\ßq"Ûyßù ëéøý㣽ÝEiÒãõÙøå/›ã‡‘E¤Úc³ŽbÀtL ÖQÔ5‰²Å’`€,³Â©Ae¾×š @z½¶`»ßÛçí Fƒ*L%â‘0ÑÈ|ì‰}lÄØ_ìo¾f´á Ò0}kúÜý¿¿{ï¯ÁïHìºnü©û‘ÖW²¹bg¯ÌΔ:ùo„œ¹TEe…Šª0Qe˜•“+ÖEx²ZºQêéªXkœG"0^ɹþeÑ?žžd^Æè0ü„|’íý1uø¯F͸¸$ë ‹cÕbIUÍ/ëˆã]DÕ‰nR(à@ÂK‚Ä'ö놗2*Z[[»Z»b|œÙº8ßÉ“ä“E¤©Ó×þ¼Ž³¥hœÿã¬æþaÞ8g2tÊÕs„¼åž‚™rÕOœvbw®Ø`Íiª¢-»¨ Ï!«Ë¹Ó˜õäp0²º%¸”Qðó“Ô[‘憺P•¿#Ð#p7´çÖÃ#Eëa ÖÃ8%ªIw„( ëÊ +2Ë ¯£2Ê‘Õ2­€“K¬±Áßh:tFGæßstäñq ë+áºø!¯c6A•'/ìÂÂÆ:IJƞY».)Šùe=‘ [Ž8Ö´Ôœ¡öö€¿½§½§sGÁެƒòo%è½–Ë÷ÙŸ*½Ýëè-/§S §ïD9ušó=@ pŽb9u‚¼¹s;­õÔr¸u‚º`ÊhêêYû°uÒêu›Öl_æV gžû´Y¾FÔé?îÒo\þ=õsT¦ßÈ<®~©kÙuÐ+šº\¦£nrWxs#R—äÔmO¡ŒZÌyßuÏ ê®ê^Ë©ëÊN›;«_0Ú¬c¡H-  -$œ\OmCÀäЮœb‹Yô([[÷2âÑœ?¥¯!¤ÍH”q¥ÖùJdŸÀ<Ø—Ã4ý{›Ã”䘀—Äð/”;àe|»…{½ X– L¬¢fq’yiB—•žÌòÌÎxFP‹¯ãT0à_Ç!G¯“ùcFOÜÔ ‰·Á‚Ûë{ó‚ïT©Úý}Þ|§i³®½ñ‰<Ÿé ˜HMþòåÑ<ŸIñÝcÿ"ëë|Ñš{&³ßNñ5׿þ¦ÊËEwÉs“†ÉkÙvÅ-R†7àE›jªýÉ2ÞfÖjmú)å„ð/_ËüK¤ãÕ¼¾½³Ú&ÚHe‰‰oŠÄο°A½KZ1Ë ÏaÝö zúÙªêU¼p®sܼɵIØ,$h¢¥|ÿ˜Ç–¾<ý”ü9îç¼yó"¶åâ~ÃÓFU =?…ÞfÌtîàw¨ú A=ÀU¹¢`uBÏ—.W^äÛÞ•M4OðOZ¢þdÜÉ£ï‘"ú¦}3úôbúv€hUÕ¯ªfRÇ2vðæ¨ñ7Ѧ9™îíIL&'¡ö&ÜC=œyNþ®ØC=̤ìŸEtÌÿo)®pY oM“ÍùÊ­«:¢@ ³Ôú_a«LRšìLv¶·â4¶v´Ø¸¾á}ï-ì5df€kHòÐËœ†Fpu) ÌÚ,ˆ¨gEÖPÜË6+*(Ý·gÆìÜ!¨°›rÎéxO$òNB^Á×Y+Ú ³Ð²3Ç ‡! ¢897ò4Õ!g\˜Ç¯¹ i»X²n]M±uu'N›fŽ#FTX1*úäæ&ÀjaÅy3ºZ¶øHãØH_orªuÊZ=²ŽÏSäýb}MÃ÷_ã{NdžŒá§ÝÁº×)öï´Ih¾ø+YiuäˆeKK‰Ñ`Hº±ÓØ bR8¦°6Aÿ4}ßÏýó³åè/#çSLÎåÜeÊÇq‚¸:‹åÜÒš—ìÈJЇ9ä:Aß4eÄçf™¬/Ø?{bî„¥´Wæëì{×É Ïp{›;ò-°·¯©µ·bñ 4Wum¹lKañãêè[gñÊÐ:M?uZ;Ú-hµ”•¾ÓËŒ Zß´þ §µ';ÕY†{<žü/0ë±Rè0ÐÀ¨ÓqÒcA{eãü §–9ý[” v‡†YúŒî²®Ÿ¥gGÊá&ÿ¶¼YŽá=^ì«©3Ç ¹ÛÌü¬ë*s)î  #\–ÒÏrÖÉ8 iÙRŸ–|dŠCtI i#°Õ€Á&ØëúÁÁ¸5™€–-­qÝ×N½F©¯wRèï”-“Òm”³ûE¯6Œt8f,õÝp¨×Yé³Iz…ßþrW Â¾ÿÕïY>ûæµá‹bù+C׌5%N½õŽžk¯»¶·2äÑ'n»åö‰_þü…ɾ•G®‘n£³wäóñü:¿ÿòøvž{½a¢ÙÁ£Ö”eîlX¹Ýat»ÕµlK+ß;„Ü¿¢#9Ñ:!8ç0çtö™Ïóøë9ô»¿ î6:Â0t4;ÛÈ„fUŠ8±xZ·7^yn ¯½Íë­Žu¶—¹~J]cºÀ<øo¦£†³C’$RKyáuXHe»e™²Í0» £ÔKùÓÆìß°=ÌI.Ø…vÒΨ·®9 ¿ÅÌsêW°ˆüW¾s¢¯^ï»ÔJ »&d‡Oø`^ÍóÃð¬¶bV ïü&,¨0'B´$:Ö¾Õm)«v)£~nN¢üèÍ£¯¸ýtc°ÁoÉ÷/Þù̃;OíÙsjçƒÏÜyñû/±û‚§oÅèÍnV[<©xBMÞ¶=S‰ý ñ`íÔ│,`-ПŒ,$ZǨ»²p~½,ܳê ÔÞ:yueá{õ‘ôLY?k†œ"w‘?6g©Û“ÌÓ\©pËæÊ&4òZ%³€9Œ”¥ì Ns¹Ü2Ž”Ñ67KÉwܼvýµXòïÈ!¬ó0{jîTñ„ú¶?¡´¾ ß Ñ£¢Ø‡l9ÏÛ`ú»‘8½©¦ÃH„Çú»ªæ¥[j;–ÈXç[ó§~¼ºg_ß¶§\}¢µ­sßPc]¸~©sÿPc}¸.½´^ À·âºà¦ ÞÄÖ½=ö;‰ ì-;ûÎ\=*kÃÕÅrs½DÊhÄ™5·Zų麂لi¨‘6м¹Ó¶=o½6·*G¢ß¾žê‰ÁîÁ®í¯È¥ñ›êr¶„‡w'¦¦Ú"}¼NNþ|œ%çÛö¼i)ÖÍ1v¯)xíΫ3æÉ¯3f­Yû áH¬œN5Z–—qƸ-5o|Þ†¶£V­ç-dÚØäx"H·>¿cskr¦¯§o¦ïzacãs+£´â*Íâ£ÜÈþüÍåÊéMÌe£|U¦Ò4‘|*i 9~¥sùçÁFÞ2eÎås£+sñ+ŸKn$sþ>¿w6ý}< MâäêŸàûç ǵÔ]‘ï÷O˜>O)ëYÁŠZgkÊY›ÏŽ£¤ùÜ’”ѹrfv5wܾvã™—­¼ ÓŸ->°o×ÂÜéùÓÅ{mÞìÞ¢=ŽA_cÝgìæ›°Mî¾Êo©¤l_3d7âNn÷,ˆðìîÙÝ»v‹®sû¢{¥2š9‡9¹m‰”ÿ\HŸÉ›ãÌG{Û¥¤Øû²¢”YŸ¸8磊å½Ür T×:Aò0A‚ ç²Dʈ¡–(ïwU\u¿kÛ–ú‘RÖö ö…uþ®ôEuJþrÑú8H?ëãeÏ(\lûoÎÏI»Þb `— rÈvÙW7€2Ú‹xúØã–~;28¸Ï®ù9ëC™í“stgþ‘k²øTÃépa¡…â³ w>Ňk‰¸\5®\¾ +,®Õ- ‰±Jñ‘ÉVñ`±ì(Äåp/‡IáÅ-)¹þÚ3§N¤ø=ÙÕßm¶lBü}"j˜ý²ÝóŸQÜx utÁ޶•Â;j‡â»w^á–³7šÛjüxöu]-5MM; o¤äÉÖ `ómô‹%N!ÜYïÈÅ[Hš›TzØa„;ÿ$¡¢¬Àì´”í`ë`yƒËÈ̦fÏKö»Göl †%õxÊb4vm1q‰\QV¨øK¢o¤äÆÛn¼íæµsן=srÙúTÅ{î¿ån»PÐëè•íÖ¸=’ù+ÝX¶äd_ífÛÓvž³|èŸçQô¿Ñ‘–à_sm¥g«¹6à‹»Õzm¬+"V™_UËJš×ÙžëLÉv±®Bôy¤ykÃâìÊçÒ(óÊ~•Z›@FL\®ÕU‰®Þºzë-7Ýpýé“ǯ±¶®^–§›Í¿õ~”vŸû ú¸á¸Vz[ó,/Ö¶’±f–IxQ¥‹€E!*A¸?çÚ±œåK%.ŽX"ÎËc¾-Ì(R‹s³¯jVƒý‚¨­rI“˜¤1­IÀè0ÊCA¿— à~œõ!¿X_)BõJÀN¼JTh0Ÿãÿ½,s (ìT—l:X¨ ¢,kN‰u ËtIåò\YI)¾éééJÄ››*ë+ëü^ZA+’w½àÙDæy¥‰k{¶ Ýõ¯b\÷C×+M 6º(%L4XUƒ #– Å¥¡â’±h”2«²ƒ‚:¨Òk–øcÓŽöh¸¾6ôVVx6Ú#õ¸yÜÂcÀãûÅÛêǘüzXg.–—ôèJ®Ù‚`!él•Àh«ªº:âѪHU¸xÖë8oï‹õûêÝ8¾»q|>¶‚ßOÞ ðýF‘À(­f_ŠK‡U˜©Š¬}ƒTê.XgÍëÓëQj3u•¿A¯EœA¦i{À˜rÿSÝI%TÕ'=hã(Ø8Ô½D=ÉO¶Åc,›&)е“Ü(Z5M5£#‡ìÚiLŽ,. öÇÃÞX2öF1óm–¾½ùô1Yý yCçÕÏ‘;stb0~<Û[ÁÛuDF³Olrô-¯£/h¾±É£ÍÃiëÈ2ÔÕ±£-ij£6œxJ:Ò¿Ãú9L>`TµPEë£(kJ&©NwS‡MáOò‡M&Bž…/›RÊIñ¬Ý¬0Vã`Å[ŠAaǼ, ’Ã2¼û÷3{qVµOÑAtñ¸c×ÒN².^„mPºè$5}@=·mýI_/ã5Q±ƒéèõø}‰Ñʼn¥Ñ¦—ß{ÛmƒKcMµF EÝîJgrø·žxøT—¿m®÷ôÍS7¾n_ïùÃí ©í»[ÎNÔ=bxcµSñ]ÇîªJ 47ô%ªBuUöÊÙ~£}×Éž†©±žÊ†{NÌݲ¯5XܬÂ]µàóö.Þ:žo.‹(‹UB¿|Z¼ëY|f3ÈŸ.“#¥ÄÊxaÄÀ¬¥HàkÙ"X®¥j©rÂkë†ë–Søþ¼H®\uL~)“‘ÿT¼§ù%¦«CLSõ¼kPÔé`«Íµß`7½,þRˆ²W46Y®÷Ų­ JÚZ)5&‡»;[çÚæh’&BUA¿ÏQ't7§åî"Zdò^ò‡ðmÌ䀘‚ …Ö•ïFÝ``ò{Ûz³ømø…>þ%6JÀOâFDàå˜ ŒŽ‰A­cï¿8}A’ íøº­VÔ˜ÏÀ )!lhÜR ø¼ze{wêš>I‡&å¡Á¡I:F½Q/ìÌ=öåàt¤ãK°_zB‘m~;m5Ž´]÷*gs`í Y¶ùlRrÿàâøŒÊ_¾|³G•¿Ù#œ¸sì—>XYÿB§ôk¾ö*“þtþ©A­º#ËÛ½…¼eZtZHî“ ¹§r3Ž[›Ÿ¡xº«sngñL)Å=Ê[–‡v.o‹9~ÑQÍ-ÖxÆ9¶SIÅa:¦|Ä%"QUZ¶±2\ ëGæî@SSS²)Ñö‚BŽf똀UþNþjžÍ­ƒÀÿ®óÁÄâtÁÿÎÕ\áзMm8~ìöÇ„œü2ÃQ?´` .€UhJS%¶Ç¦ÌýA¯‹º`Ïö”ù— ?ÊuÄŽ0.tÄ_ NïNïªR–rǪUjfW 0ÿ½BøVuFnC(ÛAåÿ2ìÌï 1O4ëßBÌ1óïi«˜ã¯ K¹=? ÞÓp æÉ1ŸÁf]ŠSfÒ%Áœ:#‘ï  («–)#ä»iæì/+ý•~ŸpÔ<¿=:Ãy4Œ”¶ =.xJßf8PzYþ™ò:4uUthª„õòü‰hĈ‚´¹¸íÿJæyjêί09í2™çÚ =¬c<¹t72Pp¹€ì¤òþœ2Ü”65ý`í“þ t¯hÏ_Î/dZØÜ.œ!¤Êð㢧‡Qàœ×«Ù”ÖÓié˜ è˜íø ÷VJÞvŠo|ëUíG?’îyûÊÂ@_ø™Jö¿GÙ³€ç䡸`ú;ò=™k¡ŸF£NB>Óý¨0¯*%óìŽCöÊ•íj0< ßsÙö›wÝ…ã"÷Àá¸HæZâ·Äi[©â“ë€×é éïÐWæÁÉöÿ–6†$¢Oé…°™W½Ø°ðý%ôýôפ/–—Åê t!ƒ¹ð‹Ð^óöKB‰ÆÆÁD(”llH„¤‰¦d(”hÂoð닇WýÍsê×Õ{‰ô“]ä:ix@ï¹@㺩]Gÿ±–¥ P5 LlU%]“ôsD'²C‡ý£8œv÷Ñ®Æq:é’Mèo®Æ6g€ø–ž§[-Daq¶à×w·º J¬°3Û@ ˆÅ¶; e×;Xô2‘æºo…£ßÙ¸a¡¡cz´ÒÞìîMLUHç_qïd—&ƒCƒÔ|†/&Og›ï@”Gíu¡k^¨·×ûSô~‡m°ý¾ã_•êê;ùÀw·v¨N§³:6ØÚµ¸1<²Gÿ\³Kþ£þÓºpº¿ÿô…C‡/žî´qäHß¹»î:wþ.uËû£K®Jù¤mlpxpåƒVúZ Íî xÓ'FG—'ázÇåËû/¬ ­\Øoþ;rb*üê;î|àX§È³ò.¥‡­ø ëùcLGVJ\G^‚ßß)~¿Ä~ÿ8û½Bü¾y^½[û@ÖGá™~ŸîÊ|˜¼ä©˜L$ú&x‘Ô)LÖQ%tÍÀzJž¬*dÓ‘ÑW‰‹¦’*A{ª®Y¶Kž±±±có‰dG{2n3Ï=­izmšîXG“*aF IÛ&ÖTÒ$q¨iMSÅøØôÔØÜø'ª©=¦_-¤‰þ%ýóÌ“ô!~ñT9Fuõè‚$éŒDyV‹M!û;µé T²±Ÿü)` bÙdwцÕµ¢‚#,©à†•0ï¼—ØtI·I«Y©D·m`)ô²›é,¶ÍÎŒ¾,¤é’væF×è¹,¨å,ñRw-U-l’ìÜãst¦Hn>BžÍ%î.0ßò›BŒÀj{u–[6Ù¶FtSib u|çevë§±Ãf;XÖGIiíe] ª¯™ëƒïê’h×h×Hok2®¯ øv›N;¥NgmYßG}4ê2ôdm¥i\Ê£‘µ—m@£Ú¬h\ÊÒØ 4ôu w—¥ñW‹i ñQÐoáË×Ùf|÷˜$Û%S9xØŸ¨Ã>Fe‡lê…‚†Åm²Ú  k·­`ú~ó»~ÐÎü‰á]‡]¶;`ÏdòJ'vǦÀ‘¶îئ»]I·ÆpR¶ÙeÛ9˜.»ž+†_ÊÔD=¸ìó3ÆèH_OñܹòçîÌ:ùúíNÞ"t»i—³Ó¨5åîÊÀ›Âí«e€×Ã¥ŒÊÛÞÝósÆäØÈ@×îÅ´WÚA| ôH!íýÆ­:y¹Yi´ÜzŃ[v„[£cŠ5sSr¹æ·MÕÅlYÅ„9ØÝñîÌóÊÛùýÄn¤ê_ÄéaßWxsGU´šJÎÙi6õªNq÷i¹ÍÔBããím°cŸ›žèkk 7Uzh«ÔêñÎÖ}¾+lô©â8–Q5ì“¥·îs‚õ9cŒÏOÌ—ëóu…}Ò¯ÐkàßÏòº!þq°è×€EßE©Áì?sØO»À¦³¿³õï+ °±lbZÂÁkÚë˜m•à¥#ÚW¢k§YÞ,’º Ý‘7tAaÁ[`Î.Ø…pòDyÝ’s›é7°½~·Ý%W8ÓKGî_ØiLε³`®̵DžÜCÈãO áâaCƒ@<Ø= ö²i‹‘†Épƒ´¹8NF‡«`½& s3tp3Ü97Ô—Ò¥#öïݽsΘ?6q¬p¾&ÓïVÞ®œ/¤Ÿ~ã ÙeÌgW¡k–«!ßÀþðÒ³°»àÓ½™ç”/¨ïÄ~îÅ~þŸXû;I’ü¹A™ÆmxX%®ªºÁÓ÷ =@j“@±ËVy{^Òh cr³·7í¼ˆÐ&àXžé|¥Ü…ñ(èÍHè˜0Ÿ‰Ã•€á¨êꢴk¸k¨¿ó‚ÅávÌiîW¬Çû^òtÉñf=^p¬óéfÙ,sã]*ïfàLê—òÆ«ÛÐ3’låÇ»”o7Œ—û;%ÇûáÂñ‚Î;O’ÒK¹Ò å\É‘e€jê½€ðo${ö7ÅÔ}e@%AKBåô V©”ìäžÍ. /â´É VE‰½èwZÊ›‹åÂìºj: Yþol–¸\×}ÇFý®¤ÿê+ï?ÂÝË£þ˜cbk°ë– ÓÑU4ëÊåÄÒÅâX¸L>P “ ›O$ɇ,Và(p’Ú%¼ý›qž‚|n«¡Ln¸x,F#®.ÜâUl_oמî=¹áTðœ÷÷‚®þ×Õ¹ñÐo|‹‹ãi³\×Yª }¼Î•^¡£ç£ÌÑËÑè`4^þŒúiD;ÆÏfÕ¯}/”?·þÉÁÐï*·Iwjv¦ŸæòF0™O)·‘i¬» ˜|ùòÊAm†®mA|&Qz9sÓ¡—Y-Žz£ãù8{ÉÀÀÙô90ŸÏõ‰ð—g8|:õ³ONfž“oæ~úI¼gê¢lgù¤á SEîVñ(HÈöìFðl˜ÕH±-V8<2®4" ö„PtsvS<ü¦DÏnM§¶í›\†#wé×¶s~|lG;ÛØy€Ÿ öö´îGëkB¿ÓNÛ¤¶l¼îžÌóò||{p|c6¾BÎ×Uc ¶X}•ñ Œ`Í¢ežQÆcw˨‚]#Á¸¯®—Y§ml´§;™¨19:;6;Øß=Ò3ÒÞ–èJv5Öý•tÖ"µ°ØÝL0þ=œv{•I?Ï4âõðmÈèÇ{+pñ$MÆÃ~¬*ÁU€2 ºœ V«CUÞ gÀÐUêv¾ßÜ cûÇ»ÇvÓK™$!{Œžçש9dvb7€66;mÀÑþ¾¶Öª DAõ÷óæÆ`²*érЀp1úßDˆôx–þáë#“}ùþŠÛé†ñTë’«’¸ù=¤¨J‚nx3«‹NÝ!£÷/³€S~%iÏ6Ä/lHxD€ 9”mlì ¸ã’”U‹ö¹KMÑ÷ñX]-¥mÉXG¼£¹±6ZÅKÎp,q‰xáó°~—ó<Žó5Y~¾ü©J›ˆ ˆ¬Œ]É¥Þ•©¸^[·ÕswÌ%芇Áš ºšÁžÎqºš‘®WsþÓÇÑ{ O#¥º$ÙG ‹kаä?Y¡Ö\³|½yn¶°0tS?ì»Èè!K  th³P¸2q1,‘¨©§;ÑB)_;ÚZº]4Nã‰h<õú*øùf$óœv‰#‚ã¸ÈÇ¡à-ê'Ê>J-^I’>)7’ ¢0hÐ5륞Å@¥_Æ„vØê<ìsKNgc^²)¦Â™Ø,"P–lfj£Öýûfg(=rhßÑýGwíœÙ;»—NSðÆFG†º;al~6¾ ?ó®‚yý>¾*ôÂ\~¾‡õB`ï&¡ø ˆ7ñµi×l|mæ–…ã¢E-òãPC¢¸`­É¦ÆÎÉžÖžX¤1Ñ”ðÇ;–#9}F¹Ž|UÄôå:äsŸbÄŒ0×7ª¤ ¾¡s¨bò•‹†k>Ý£þ%yNû;açÿ¨' ~‡ÑÊ ñ¢•£ ‹Ì¦,Xãù3è{HÛ•£C¢é{x…–:)'i ¾„(õàµd>© ¸L¹ÈL˜Vé ãk»ÞDñ†:Ÿ×íÔ5Z%UéBï§¿ þíÕß/èÿ Waýv;0p‡ûŠ Ì·"3Ã4|d®* 75ÔÕñ:#®×óšÊÒÔ—1\ðdDF=n4‰½‰QÄ®x¥9‰æhQM´zåÀƒ\,1‹r!É þ?aacÇ1FuØU O‡n:7›Ì·õÐB›Yld­[YÙnc÷íe·%‡ÆYYØŠœ ú³Ac„=6a¾Óªq·°ªDì6,>YhjÙNÇMËØYne§ñÑÁîùžùõ6–§dþ+’s:˜ºžÓõ71º[ÅènEf¥Y¤Eâ-Ž–¹¥z« ‚ø:ÔZ­–E.Ø0ÖBØØ—ØX߯ó0Ä„'‹=C6‹á§O^ 0× ø×²ñ«ZgÏ“}^‰LÜD˜™ƒ,Ô„ú²ãú¿¡ 5C7‹á*À7¥ëɳ™Ï3{öy€ÖÒX¿©Î¨f‘ÒÒQó©¯WÎ<+8= Ç¢bÊü4a>y©rVÔ|Œ^ø üB ×`Ô&¬(­ûÅË\"áùÌë7)gɳŽhFÖãä§ [<úfì ²Ùh`.EÎŽKä#ȳYFYð×Á`›Œzœ•›òŽ™ Z¬çÆ™…Åvå(ëò0F‹Yõfë¨ìÿØ{ÙÐ.]"f6ýýô›É™ï°¾¾­ÝéOqïßÉ6"øtb |.H¥†ÎEãñì8¤ßLyð7"n‹Éî¦Ù•¨8S#ÍA+¼F‹-}Dy«AxÚþýãTÿQžyЛ±û'À÷éªm™èDÓµc¬®«äµ$wx½QXÄ-¥wËÇä*ð¥/ù­i`×{ÓCë¼`q:lNð†‰Ýa?VÚ‘Û4¤•A:ulñðÞÝÓSVþ³‡éùÌdz7ùtæCŒÏ‘¤Ÿ†‘„Fó™/Èe»Ä-Ñ`Hœó8ó©ô™Î|›Á~›ŸvþðoÊŸ«_úãÒ›3Ï ŒïXåøú±ÚÀ×{Àp)UC rô‰ôÇé,oOž9ð2©+ Ádï'óµƒÙwÉ+Æz¼¢Ò ‡_ÃUÅ]`/NnmîÏÀçƒÙ_eÜUÚýQ4caY¼Ÿ' úñ³~÷>Q øøñêiø?T¸të /þXîo©K¹3ÛÚ"ýHðáœà¥ØNw?j°Ã¸PsZ„¦?•þ;jd>JêÈqÃ]û}øp‚%:Âñz¤†âSÁe³bm-ÓMøZKü7»iO•”„‚•§CWI­Ó*ÚC“Û¢{htÒD‹¦'ð=`Uh’¾aþ”çÛDsä>)ÜÔ‘ïõœšïÞ íÞµû@ó¥]­ú–6JcÑæ»c‘HŒïŸIš,öχ@êªXÜ«]fq¯™„rˆü­øÀï²TÅ8,Z0}tµÚ¼Y9EnÑÞ(Úœ‚6¡tŸà>S%À6Dã áwí²´à­õÖfuø›ÕgÈ-ú'ĹJC5ƒŸàç>|ú!KÇ„I'><ʨ.…„¢.‡›ü†R)ÛÕ?c¸á3ð©~8mTù+¡u„â:@¿A2bQp~xzˆ˜Õ¹loÀö5.ʦú”*øŽþ㱑ÐD†i:æKÄ|˜Þäû³Ð׸è—ÀgYªacz‘{fkë§Ù·–n¥?ÈÜ"lê-Ðwíå ;/p8t‰/ë¯ ÷ÝyF¶Î¨,V°©'kª%|·9uù‡ä]9}.Õ1ŒK\:¼L‰ÍšNü,Â&‰‘‡,·‹tT(¹:£—'¬W°>8Šƒ9G&…šÄï:|YY¤Ïk*~–¤&ñªX[÷)‹äâw¿ËRƒXjvMl¢ 9¬ÜH[Õ4kƒŸ%©1«³0·>Ì'L¶QîA¼Õ^a§Ö”ÉŒ€%+Kx³hràÉeAú´ú,ƒ‡Ï@{¯t—= „}&ø!–'Ž]VG(3ðãϲÔÄöu[Ævuqe®OW ™?`r÷€©9ýþ9x,í&Ì|’ýþI6ßøáÇr¿§ß•vÓGÌßIT}f9ó7Ø|ŠÛ%§ ¦…ßQ¤÷iUŒnü,IQ1ü'Ð qP²y¡Ú² ÎBßWè¿É¯¡ƒÐú{Eþ[=‹0½0'q¨NÇrγ‰Œ—%]8óñ½‰ÁNlN|XXŒ©¤ 8¾yP P8{÷ìÞeLMN”panþ—ò ùuÁOü”¥hV¾7à([W $ý*ò<_?ÆVÿ˜1Ü 7Vù¼:¡m­»ªI˜œƒ0•¼ÊïgûI<Ñ‘ðÅWí©kP„¯™~6ýNÚœù ÃûÀO'á‡N£ÓÃÔàÑ;>ï ð™-?ƒD,k‰%cq[ÏàÞª´J;´i6>ø MZÏ9T @–N+Ì ;ƒË€ÅÞGµ°7ŸèçáÏä¦ü_ºü@ÝÊûÑTÁ+©©ÔS^ØïUñípf^i%-ú'п,µ0ý„(`sôS§JÏéßȬ?¾»®ðh0o÷~²à~C‰ÃFüÔ/W´SÌóÒ”p$~äP£¬Ô½þ{ÿð¥›;?“§ÿþWÓßþï®ï¼ùKÿû»éÏÛ&w-ž®îœi½+1ÑD]1¯~Yi?`c†Ï0æ!úë±w[×,{—(¬,Ÿä“yüã ¿¡œ’oTÊó÷/Ñj¶†Cý²|£ ƒp:^8²5:HJ ê²ö†>ŽVh{W¹Ø”Þ§¼K>ªÆíïB˜Žˆ=?gI\y{þÞ§壢ÂûxáH¹>àã é×ê_Ê8A)ìØ¤§ÃΆÂÁð úÍ?øµÿ¸ÀßÀ^£Ü Ýz½‘ü†çJ©û¡*N‰xA;Øô£{,/⦋K(ö ‚¨ÀÏx+G,š -h*n®„óǤ¹!Ûœ@E¦Ëy-YäÚÓ¾xÌhÁvŠy;|T(ŠÊ(}½ƒ}Ú=OrñW.ÞÿêGö½ìæ×wœzt…}{xßËny½úÆÇÞÙÐÞ×Þ°xláöý­ í½ø‘ÿ6åý3¿ã×?ó×_¿îƒ¿í7ßsêM>6póïÜ~}ö[©ñƒ|›ãGÙƒÏTrà‚༠?ƒ¼à{ 2¯<¤‡ÔßãºFylÌÜÕ=Åø¿\s™ œzÐ=ðæß² ?l§ ×W«þéÌÍà8 < m ;Ýü–‚/´P#ÌUß íù¾þ÷ý>õg_¥‚~ýÞ3@ûÒKWåú ?KR'à9d쯧²š¤š®R¢É3YgF%˜QhÙN5j“ˆ,Ñuv„ÐçZâmkw·t75€Ÿâ«¬°ë4&ÅÄ‹Ì.¥—<#ú"З,uf¼/Fo?Ù¾Hàÿcõû¬¯ÆÃ.¼4*ÉAªH¦C3/ætJlxû«²×C’‚RªÆë:œÏ6‹f楷žÎAqÇ-) 4Õñ˜D“‰X[¼­®Æïõ¸6•¢"†?3 |ø² ó¡+ãþY îg›62¡ŒÏjO2Úà3Ìi7PÖf$ðzXVxÖ%?kA…ÄïÿU¼ÿÏ|p8Çqñ§€ƒto¿!øÜÍ䳇ïYjAž’«4{TŒñµÕÁúªze  yê€9ûÇ‚9ëI_Þ¶«‹‹Àz§ªþ;õ‹±0õ]6Pˆ˜îpFE£ÁòdHâTdIÆvëªhU,ìiìŒuèúC‹ÓÕ›þñö°1ž]e|$ 8~Cð-ÊÆŠ§^†1‘#Y+'+G摼_[¢ àClJÔõ !‹Æ¢æ¹Væ(ÐúgsÒ—é½Ø_\ܤç_¬“^Æ|¡¸ÇXðQUª­‘DJÔ(UûAw¿ÕeX%@ÉAL<{w ŸK.„ª$ÚX_ EþJM£A)hcòÚôÿqv-"ýýL^¯b/l~Býƒ O¾$ú9ÈtËôÒgt+Àþ ÅL@ìq¿¸[Xæù'A+¶c±Œ£>3ÏOf/èª?ÏÓU²4~a»¯>>rB‘L;}‚wGQ»4p÷"Ü€L”¨ñ”¥ø‘ȇmº*ËDFtQø#þX,bæ'KðY™FÓ÷^ Ö'Ù x–„?´›}0ön—BÄm-^bQè@bà”Ã*”"øŽòŒN0/oyùí¬o…ô emù9ÀGB°gEîêÓ-ÃðÃË ‡ö M š$‘Q1,nÅÁ«°iШ {þƒF'.G¢’]u`¸Y¥ø]em5Ü`°¦$×2e$Šihý>oe…¼Õ°æzèVÐoôpý0œ9öÓ£ˆñüg&‹7}“ù~/ìdQ4˜wŠ… œÝæ0ŽVofm5ÊÎH¦©Ôö`ˆí>#`tFÛ(‘x@‹ì•è °är‹ÌXHFq/’„>ÿ¥ ÏÑLxóÈUÂAvÊÿWú?êó |†qŒÁF›„Á§,E#èh᫇,%“ÉV†IN`÷™„H;Âc¿²,mŸTüíòÕãEIŸŠ“@}=Eï8‘Îø[«ª>_c¢ªªµÑGÿ'þ%”hôz!ü‹ Í[>X•hðzU¡VlÜJë«xó¨§MšÿpìŽ7m}ì˜CxøugñpœãÁ­©$ñ[LÌ ‹by]ž²2<Ä+cæU–‡g`8kÀ¸ÙñZ\oûªÑRS¼u0ï"rI£³?ÉŸE-‹¤Ñ—Z¢ÉÏó“ê6“ç$à@m5/Å„dæûLòŒåoÅXbcÁû–9®¶*º ¾ø\'òg± øŠÏÊDÊwµ{˜5œO=09çgHàŒáï™ ØQ;Éž´çÅFG`Œweå ÇÛÃߨh.äW407þp,;7×Âx>!`¯eýõŠxyV¿IÅlÈ ç‰5âÂÿxK<¬qŸén€¯ðw3ø>ÿ ‹~ͼî¯`ÐbŸKž–0O3˜~îw‡*×åÕ—0ò7/D¸jÀÛªòy~ôýÏÏžž[¿Be…0–_S°˜Jsé­ Wñø&—÷Èåi‹ÛM8W2Uòƒ_UÔ E œÏÁ1Àõ€ÀuŒáå§fn¶8Ùd#JQÆÖæAc7Xý€²¹±¶š†hU[ا‹÷¤ pΜU 瘨œ¸Ia!àû•·SS Þÿ¶Êw*õ¾•õ?Á³fÇC6¥Cb&s I8òî s¾Þ ¾M¼qÁÏ’ü —áltùY° :>¶–;VäÊ^ŸAŽü1FØ=H_†/J&þz›¬a¨éY|³í÷â›mކßé¼K™%׋»|¢ÌôÅL-q®»$ à˜ˆc–<2‹q.±¿K]òV{ýÙóŠ7y•üRi^e¸á3ðëa^ñ ogš¸U3MòúÆÜ=Í".3ÚL›£±v6ï™f PÔ•AÞÉï`EK¿öf~,_òRðöE „…©~™Å#ëT$g dMrQá¯à!¥òšE#<ÍÍÍ-Í--‘d$îµq}à™ü*¯™åe}‹Z ωšYµoá›—æJ˜Išº¼=î¯b{ã<©‡Ì\$ò;h¯R}ù;™ÏPY5Òf>C¾f8¢€‡ˆŽæ¬êv¨°Œ©¨È¿H‹¬¾H`Öi—4-û˜á—ì¤Ðlʉb-õEL!˜×•~ü %º¶\cr±…èÐæ BÛ"ÁJ‚¡àVÖ ‡›‡'‡'ÇFúzÚÛš‡ÂCŒ .á¤_| øÐ!øðmƇéô¿þBñab̚カ㯰 Î\þWòoXQ;ýÁÌ’7“œ.äÏ¡æB`%›Ñò#ºU}¹Â‰„±àYþ¥h|ÕŒsy·‚Ã(NÜu´‚¦„ÇÇÂÍöíœ;2~dh §»«£ ¶ŒÍ£áQÆœJsiÃ5¨NçÖ?«sk¤¥ÌŸ•ìúG¡hæëNåóŽ ûŤ¶¼ŠX¯`nþ²ÿéôޱÿd®¡ƒBù:k2N>‰Y@¸æÁd±’¥Zó&ñ†JàºP„ëck&ÆhN‹ÆÓí Êt+UnnoK¶ÀDu„;ró_Ò?b|:Tħ©ô-°‚ßS¸‚}ì@œSª­Øò²MµÓf–³—µ'«³-Ñ%X¢V@½áæ‚Õhç~ØÅô§”«³VXªOò|Ý Ê$¥°6@õTU[‚=¶WÃs2?õG™쳇ÛY.£ï*Åíæ2’v‰ªC7ûñC°'ŒcÕüL+JÊt•—R°¤ðK¥ZÊ<^ äåç‚QIä•ZÜ‘èIöägäp׿Õå4ìÂXÿ Þ+*MN »Œy;æ‰ÅDóËfeRg®2©£¨2©Ya¢°B© ÜG<ü°2.? {ÛYòÃW‹º˜y„[/;ýã[»s¸ðƒÝFí©Ü¦þ‰Ú³'{^¥½6a4–¹<,ɦ ßøØÔäØìøl<Ü–ˆÆZ"ÎP{<[¼/Ü)çªîÉü¨^Óןñ%z‡ÂFIz¼íÌÀñ_¿uÒW#ý²ÜyèöyãôlW¥³Rë §n¸eàÜûïÛ9>R\bïS÷>æ½üíˆq²ÊëÇêÍ­õËœèhˆ5ØmFb$î›»ø‡÷_ÿþ™'òKëýêëï»n!™ŸGm¯™GMº(æ²MÔ†z»ú9ò–KÕ"ËQ OA•e3“¥üšV9¶ÈU]nrJËbLÊhFq\:²g—19Ø¿>AŒÛôAåoñ1 ~òYe’¾[½ÈðãgI„Móè%öÂE`÷²wÒgH~Lv%nänÊûSê#U±8¿ã—2I^!pÀ)«“?¹"¬,¿ipý˜ãeŸ/« w…xÉ‘ÌózvõÕÐWŠÁüšßÎÒÝäûŸº*£À2<à•5V’ ëÇ+Ü’ÓE\NpÍÌž¾ŠEöt휗Vz¨ËYéJÙ™Å)G†½YÃ#-¦Ê&Lpô«· Ÿ2šöìÙs~Ïù³+§N¦Ž]:|³Gý±h"KF}fmXSª_ âØnƱíåc£×ÂÏ®TsŒ7U€“İbv]b¹¶V°CÇaž>ˆEMUÌ:IœIÇÍfdµL»”Ñ8;KéìÊìʉÔ5G>°Ïn:C§‡:Úw$ÜümLîú‡UªÎ¯Ìõüàs dŸcÞO“‚ŸŠYÀÍ=‘ÇQ‘6mö"?H$´ódä\L~vèñQ$%Áìð‹ —Qšp×îÚ†Læ±È!q{#dáŒbÔö{£ç'ýNßvñõ‡ƒ¾Q]s? ѱˆc>9Mž¤éx‡eŠlýt–¥2*É3±äÓ€Ó0¸(B1/Òi„,[ç®=|ÐæN̹“óÀåκ„ã5c[pó É—Ü MæàH²à"ã`&ò3¸^ÛN{â­ëŠg)мöÁsÍêC;À¾ÊúûlÊHL¡-`ßÕ7V¬ßDh†êóXwW½¥bOà%…LÕ)ÊÞúeØ¥Žª ‰Ðo{ªmR ñtœ_í\g8'1sr®+Ò=/ÍCÔ{  UøÀÌRy{½D ýÎXêöRïá[ýX½1N©+WYå8U½3msf²¾ùtÚ7fü³v}ÎRû/§?ºÊUM©ê¯¯;êß@þòêmËY/'ƒDùU¿‡©~›Œ‘ƒo¡?Ûøü«Û¼¸scûëÓTÎ¥³Rû«wjwþ³ÿ(­WÓþþz·N žêm™„Ô³bƒ•£ß¢íÃ{’÷àý ¦þ&UxÚc`fƒÿ31`6^ÿÿxÚ%Ò7LUq†ñï”ÿñÞ×–hPŒuA5–DŒÈ%ÀÞ.ì T0–è¢Î–Á²Šˆ`I4ºé¦«í‰Þ“<ç—w¸¹7ùÌ3³öÿã[Û¿·‡òé,+@…V„‹­¯·8iuø ÕãkÆ-vßç ìWižWå§™ç§ûéø“üy¸È/ÂÅþ |Ó¿c¾7¸d^дZ´…ûÍ “a#m ÏÐ aí »hOØC{Ã^:Òwá'ú5üfž3§).…f¸ šé²,p“].pü—p –*·׺fÚâN±œvOñ3÷Ü|÷½ÂýnEãÌ‹ÆG,ˆR#þK”MÃ9ßåEytz4ƒeæ°ïæ û Ì…±[æÅnÇâGñæÅ?Å?[ÿÿ‰Åã?ñ?æÉäY _>äp¤áx„Fš¯Q…Gk ûX¥â‰JcOWK¦²X&k ÎV6žªœ«\œ§|¼_pRI\§ƒøáÃ:‚ëŪAGq£¸S>®ã¸Y-ø„Nà“:ψ»ÒYÃçu_ÐE|IÜ¡ZÕ†/ë2¾¢«¸]í¸C×p§:q—ºp·ºñuÝÀ7õ?ÒcüD/qŸúð+qKê×k< üFƒxHCø­Þá÷ú`Þ_NAŠ”xÚí]|ÅóŸÝ½žÐ¤Ké]¤ƒôH½H'„ zSš Ò,øC¤P)Šb "©Ò‹zï%ûÿÞæIH};Ÿ·7onoïÞ¼ÝÙùÎíí#"‡F³¼$:ìÕƒ2tíÕ9˜Jôhß§'UÁà_-e ’’8ÊŽ‘I¾‡ãdQ&u5ªM¹üý䢂Müë增ý‚lÅ1Ô.ÑÈÇÃéäK©:¾Òûj¤ò¦*o­ò*ï¦òžÁ{õ¤>*¤òa*­òñ*WåSpYîïŠ=gqÊ9~ujÏ·Üî/Ä¡'Ô¢}áÛ—Ô¯Ó© 5Â÷\øÍE±wm¢ÂtŽ¥¦Rì( ¥^•¿Lb†ø„^ß‹C4N{N+C‹µ*Z]úU›¢M¡µz=­ÓWè+h½~X?Lô#úQÚ¨ŸÒOÑ&ý¼~ž6ë7õ[ô—.ñ‡l5J%i»ñ¼qŽvÌÖl’ÙÖüˆ…Y—¬K¼šÅÎÆ_´sØÏðZöB{oi³ÏóNI§$ïçÌr>âýÏœÏø@g™³rv;»ùħWqÿá‰5ßþ‹Bè3ü–\D~4†*%C©åħ.þ½§ä%¹™Ê˿țâ‘Â.¡ŒóL û/ËòªÜ/S*hx§ òê,æ$Óàã¶ÒÆQvÕǧº,ñ]÷ló£#ÃäB¹K®“;©Õ¡úòùŽ\þ¿­Í‚øüOŽ£’1ì/,[PÅf·†ò¥Xêë+×ü‡µù\âöG{ÌyyX^”ÃÐzOɲ³üGž”R¾%_•½±ç,Æayè_©MÿXöwO@óåoò8òò&¾-P²Óò x¹]~çŽnò dçÿeÚlªòƒ1îï/ç{Ø,qªog{TN†N{ɲ¼|Q¶–7åpù®¬)k»ûkÞ)/'P ¹M®’ŸÈ6²½ü£ÞŸ2ç*GÈÑ8j“ I1Úl‹Ï20­bØÿ¢\éa‹ÅPE *Me<ü³é2óÉ/Ñ–Qž‡v†¤Ñf{S!š}ÿàÓ~gxJ“L.ÙW®À'T^E~T‚m>%Oø$m΋m?| ðôDj¿…ñg&ìãeÏ÷ïåwüž‚ì_b7¿tǃpÿ'Úýs"¸³÷ȯ%ÃSBnyÌÚÜËþÜ.Ïö úXVyƒyM†%+mæ—ëå•Ǫͭ&W ûwGÓ6³ªo¥£ÜÉJ›•äϹmî‰eÿ‘Õz#ª-o»y$Ú<ËþbÜåFÛ¬dõcœ;Öè±ióJ$Ý<¸äÙû¯=Ù5 S†>fm^õ0­c÷óRHo;ñÏ}ën¬âþØEô㑼ìi¥TOÏœ¬ÚæGòÔcí5³ÍËþ(c“¼îÆ4"$“E»<ó˜/ cô¨2š+ýî1jic<Ëo–;¨òc¼Þî2ˆÒßí%òÙ•2ÉêàÞ’C•¤¢l¨PòçTD.ˆˆÄG­éðC¸ºå”:‚_„ÏÏιë7{pF´O!{(;ŸW{Bîz -6F.'Eâ·Çá—¯ o*£vÖE¶ŽwâIíïÿWRdXþ‡òŸ{uö/oÛä1yÞ E©äÕȾÈc½ªÐÞ¿tu‡*[&ÿ‘Ç1J­M84¥ióàÝ+—7"Çc’eœ6Ù£MºðõøT=; ÏÇ›â£Í ñoJ¼6/=`ß ¯~’ÎÞ{µomzÆšè|#ï8om^O–Wu.…jóÖ}^QqÊ oþR¸ ·Açd[ÒU-M¡Ú¼-ÏËÕþ°Ü¦Fùõîœ ÷'G¾ Û*ÚìMNÿR~¥ææ-‘ë#ô¼>òDy ¶ùIú§ÔÎ7ü.¡üFþ‚|¿\ Oºó¡Ù#r¹¼(wÉcê÷{ìÑû”ÐÓOÝñ;Uì#ÂcRVó²W?ñÕ¦gŒqÈ#ÒÕˆû¯¾3‘bçûzÚá¥û±QäVú¸zLJÔf¤vz"’6ÝóûŽ=λñ)ÙŠB«GRBûÁryÙ°nÊ ÍEÌÕœvtJž­ôX´Òñ¾¶3ÑÛȾͽ×Âÿ‹­®'rŸlw$/eƒË‡ÏÙ2¹r/––=ÛCj>ÉÅÿ¼]Ú!ÿ¹_ rg„6Ocÿ®ûãòÚQsäOGþöÖæÑ¨±ny0¢Í]{ÀœPoŠªÍh}·_ï\||ÚÛÓn‰ÖíÞ»3i]þ~Eñ\…ëû=î§,’•&/‡ÏEŒiþŽRâðý3m"Ú°;w>ôÑG“µ>ÝXâßòê[y§­Eø`®.‹ÂÒ®—‹=’øœTãtñó¿U/¡Ö31Ú×÷"joRúØÙwŠûßwî»ì…"iæÜ}åLìÀßJA×¼³ îÑÍ7Ö}ï÷È[oŠ—6oÅ¡WOI˜Ê¨{ˆ‡¼Šˆ«×ùûÅ}d_õÌ»B‚£žÑŒ\­¨árÈëÉÞî~ù¹š#o‚’›_ëz¦O¨ksS:*ãSPª)óccî_ásvîú/r_Ęk—aô$¾¤MVM¢°g–&lÿi,Öêq?U¯$¢DÞ­á³±ÝéJý¯MåK{ΰÝa¡aÇ®JCf••e5iʇ9þ¹‹Çn/ǦÐQsa²¼ªîäMÁÛO̽à¨.^öám¯þ£ÕêV¹S’Wd˜¼Mi)lô‰°±js™Ws±jÖ3_\ƺ&rØï^mŪÍáσ…źºhØŸ^mE«Á°È¾aø a±®º¶Ï«¹ôyɽ+ë>±|göþwv=@›G½z‹A›·0A£ØnÃö”û~ÏXá^½ÅÅkBåòéXKzW | õ”ŸcÄÔ@…¼Iº¶V^ƒzBnGÏ?$Ï ój%ÁÚ<ƒj‘­Ö˜M>·:qØé?Ýï]»éP³Áð9á)pFsrÑf>Ïóx‘0‘W› ÖfAôk·ErèP½+vÔûö…„jÓÓ3¯z»’ûFRî]]6Ú,Š–˜:Ì ]>­ã}3H"´ùLwÍk7­Íg1 YáúSž&O¹ï»HÚ|A½Á9-=]Záo ¼wNO˜w=ÔØµxgÕœXŸBñj3†dD õ°;«šÈØÞöæy6MκïßøŸw’a+ùUÍÚr¹_î‘oJ¶í:šgY“ËÕªÕúÔ|B¹M½+}œ«DÕc6Äòvý8§ž±–ÈàUÒR6Ï6f»êx•Ä© ÑErÒP.Ú ÊB;@yèoPvÚz’ö€òÒ^PÚÊOûAé ¨å£Ã ltô4…‚rÐ1ÐSt”•ΊÒePq’ Ù̦2Ì—ùRa–Ž¥£çY6–J²\,Ù,7ËMϱgØ3ô,+ÃÊP9V–•¥ŠìöUbåX9ÊÀj²šÔˆÕeu©«ÇêQ=VŸÕ§:ÌùQCÖˆ5¢¬ kC/²¶¬-Uc=YOªÌz±^T•õf½©ëÃúPÖ—õ¥Öõ£ú¬?ëOéÙ6€ê²l Õ`ƒØ 2Ù`6˜j²!lUgo°7È—ac¨{“½IO°±l,¥fãØ8JÇÆ³ñäÃ&° ÄÙD6‘¶ˆ-"?Á§´Ú m5×kƒ)P¢ ¡.Ú0muÓ^×^§ÆÚ(mUÑFk£É_›ªM¥òÚ4m¥Ñvi»¨©¶[ÛM]µ=Új¡íÕöR+mŸ¶šhûµýÔR;  fº”i£3QGëœ:éBÔN×t:èHô’nèuÖMݤֺ¥[Ô^·u›Ú=žTÛxÙx™ŠŒTÖ¸d\¢TÆUã*e6+™•ˆ™•Íʤ™f=c¶2[‘0[›­É0‡›Ã)£9ÂA%Ìæ Êd®3×Ñ V^+/u·ò[ù)تdU¢ÜÖ0k•²öYû¨44J‚4z™Ü ÷!í±7¥õ¦ô -*õB[Ê<¨Úa^JƒX€ú Ä1…@©©0È—Š€ÒQ1PzԞ͡çp¶¶TÔ‰ÊSêH©5u¥6 ´ØÓòv 9Ô$¨hƒæàêþ:ãŠzƒïêDýh Ê¡WQv(B>4[õ¨Vô&¨ ¥¢q —h<¨=Mµ ‰ –´ô$­¡µ 'i3h„ꇯ«~8’¶úÓvÐ`Õ'Ǩ>9JõÉ×h7h¬ê™}UŸ¨úäÕ'‡«>ÙOõÉÑôè Õ3_¥£ 7Uÿªúç0Õ?ÑIкz†.*ª¾Z•®Ñu*K7AtT^õÞZŒ3N¥ÑÌtô‹YTEõçæÃ|¨8zu*ÈÓ°4è?éXzjÂ2° èÛYFjÌ2±L蟙Yȳ²'éÕóŸgÙYHr²œèáO±§À»¶ {š=MþÊ"”`yX^Èó±|èùùYðYAòc…XaðEXªÍв¢°ÅX1Hгâ°#®áÊ‚èÊ‚Vä VU Ì¬"«H«Ä*¯Ì*ƒ¯ÁjÀ¸öESö%½²/ã”}i®ìKAÖ˜5† `ÄX ïÚš¼ÊÖäcÁ,˜ (+ÓLY™@e_Æ+ûÒTÙ—‰Ê¾ôTö%DÙ— ʾ¼¥ìËÓʾdSö%§²/¯(ûò”²/Ù•} Rö¥;{Ÿ½Oï²)l ½Ï>`Ð6•M¥Il›Fo³él:½Çf°ôû}H“ÙL6“>`³Ø,ú›ÃæÐ öûŒ¦±ÏÙç4“}Á¾ Ù\6—>dóØ<ú„Ígói[ÀЧìKö%Mg_±¯è#¶-¤©ÊºåfGÙQêÅBÑŸû°cìufÇÙqšÍ«óêÔ›×æµ‘×áu¨¯Çë¯ÏëS:îÇý¨oÌý!iÊ›QÞœ·€¤%o‰½íx;È;óÎtá]ÀwãÝ âAô潩3ïÃÑy?ÞºòÁ|0$¯òW©-¥.|$Iíù(>Š:ðÑ|4¥åoð7 ÃÇਠ|ÊLäo›¿Míø;üìÄ'A2O?O§—øL>“‚ù,þÎ2—ÏÃQóù|üº|Îø5ÿš:òoø"õ-ÿ’ïù÷8ãüHóŨg ÿ üÏüÔ¶”/ű+øÈ×òu¯çëqÍøŸÔšoçÛq%»9l=ßÇ÷¡Ì~~~ˆ±‡ùQÔv’Ÿ„&OñÓ8ö,?K©øy~üe~×v_£®Â}ý|5êt†0¨½ð>àS‰´ÔYdYh¶È.²ÓK"‡ÈAmDNñÊä¹°7@`o ¤v¢¹hNiÅ 1ƒ²ˆ™b&³ÅlÊ*æ‹ùÈ¿_"ÿF|ƒüGñ#UKÅRª –‹å ~¿ƒÿCüùJ±ü*±šJ‹ b•›Ä&ð‰¿Pr‹ØBUÅV±ü±åÿ»Àï{¨¼Ø+ö¢ü~±’âê9$ArX¦&â¨8ŠV^‡ÊKøDy ³”—ð©ò¦+/á#=£ž‘æèùõüð- èág¬Õׇآúaý0õÖÿÑÿ¡±z¨ ?ã´ŽVªŸÓÏ!¿¨_¤†ú%ýUÒoê7©ª~K¿Eúmý6eÕ¥.©ˆD]çŠô²QɨDO•ÊôF£ •1ª71ºAdt‡$ØèA>Fˆ¾ÑŸz+ߥ¦1Ȅڃ!Y`||‘±ùEã"YʳyÖ¸l\&ËÌd»0sš9©ˆ™ËÌEæÓfH š%©·YÁ¬‰ëñT6[˜-)ò{2(¿§¨ÙÆlI;³òŽæêc4á/šƒÍa”:’WTÌi¾N¾æ(s ¥3ß4ߤ,æ8sòñæÛ„ûL›s©ƒ9ÏœG Ìùæ/à—™Ë¯2×R[åQ•2כ멳¹ÁÜ@YÍæFÈÿ4·P's«¹•*™ÛÌmà·›{©£¹Ï<@å̃æqð'Ì(yÒ< þœy%Ï[‚º¢Q;”Öò±RS'+­•yv+;u±ŠXE¨ºUÖ*Ks¬ÊVä-­öÔÙê`u¤¬V'«U²:[}!ég½ ÉkÖk µ†BâzxM¬QÖ(ä­w¨õ®õ.Õ±&YSÀOµf¡þ­_¯²¶Ðlk«µ^²vX{©ò _´ö[)•uÈ:ùQ+”ü­cÖIð§­ÓÈÏXç 9o]¤öÖeë2®ùºuÚÛYì,ô²ÕÎJ¾ö³va×µýi°Ýá±vO»'Ͷ_±‡‚i¢7íÉödêlO±¿¢!ö7ö"ªhkKUíí©ˆý“½”ÊÛ¿Ú¿Ri{™½ òßíß©‰½ÂþƒÛ+í•8ãj{55´×Øk _ooD¾ÙÞLìcö)êeŸ¶ÏSû‚}g¹jߤَáÔÁ1“::¶cSgÇÇñÜ×IE]œÔNjòw28P&£“e2;™Q&«“ežt²¡LI§$½ì”rJ¡L3§ò©“³ÜYNYœõÎ p6:!ßäl¢JÎng7${œ=äOŒBàA‚ù¼Ì­ð(ÿ†'¹¨n<ǃðÃG …Wxhí2Iøt¾ðß²ÁË ,VÔ ðœjÂ'r±–0Vø;=©zÃËé §?|›ði×y>Ì›ð]ÆÁg™_e‘à°lƒaцÁ’‚› ˵ k,Õ>X¨°L IÀé°@&, \ò2úô%ã*0HeôÄVè{ÃÑ«f˜ë€/òW C«áŸ¿$AOÛD=á©0¬7%U*ìUA’¥ç€^SyÕ$©téëUCÞ è$³‡;.kÈrageù’Ì%Ÿ“Ådþ°+aWd!™ZæC^Ô«©8èrcøœ}$Ðåf¯’L—»îÎ=ñÎ{N¤.÷FéÝÞgDªËq ¼›.yf›I·‡ãü:I°.½ó“.¥•»Õóßaò¶Ü*¯Pïu n—Ûä)õãUõ>óKÞw\%F—wü!ØJïs‰ÃOŠòŒ˜7%P—¡Qg“{Suy)ê\roJ .]$tßóLÞ”@]‰ú¤W+ Öe”'ü)¡X(êó´Þ”p‹é]… 1ú}OÍ{S"tIÞ1ç¡é÷5¹QÎÓåçØnôê# 5{cý1¯’0yÇùÈé)¯ ’M_?,ÿ‘7d(<Ò«Òû„r’éÒ«xèm¹+Ƨ’‹äÏî ÎÒQon¾{V”åÂNQõlRiªDUä‡ðûCe9ðþß’¯ËòwJ#ûʉò;ù‡ü ôZØ_dÉÍòGßC6•#å6á#_–[®É`l{È-ÿ*U–•{0Ö´¢"îý ÷·ÉòË(ú®*«É@5¨  Ô ::£žœÏ†|•\ƒü 5—s"3G–÷ ðùH. ŸI"åÈ›ÀûDþHnܪȿ¦UVE«éмZ͹“Üw¸ÞÙ”¯ªÍ3÷Ý™òCcÞ*¿–§åݧÁÝyvÙÂß&wzŽ'9ÿZVÅfXa°|õø¢gd–³å9_v øžY~)3ÉoˆC²7Ö7=|“ÄÉäP:¯]ŒAï;ÕZ;Â{!=`_ÁXŽº?Ãcy÷ßóñ*mߙÅ~FΓÓä9=ŠÆKÊ–ØT¥ÚÔF ·×ðȦ¦1þK eþe?÷º–XN…­œ…1ê 97î¨Õ½'-'Sùƒ|ˆ÷½{A^ˆgù_=ÛU ïäWàîóƒÜ‘%QWäY#<ü>³üÛ³ ÈÖ¸¿] ØöÔcé¶"ž¿ôÌ-È?¦LeéŽ#é1fNºüã^ä£>nüíOŒ¸ÿœËÅC—<,ùê2¹X½#衇åz ÌKwlŠRø1c8ê¨ò¯¶Ê­lµYþ%·ËrFÿ$±ðÉqmÓ¸ÙËè|Æèm4!Ùÿžè¯¼´ô{Èç­!{ÉPôý=žï½1öœTÜž{Êý&SÛ-jåÙVŠŸ ÿ'¿†ä Úå[øþ&Zj(µEm³å/r)ZûWr•ìœøwºÆûWŠ.þ‘Uçc^ <¶7©<8*Þ÷IJÏ_|âδ^ɹö¾ûd éFg ¨µüzÂ+‰Ã=Ô4*‚ÿÆõÃßì-¿“+ä'r‰\,çÆä‹Ë‹1 ŸêÉø·ÿT½5OΔߠ?Dó=9ÍÃ䡸Rvʯ3|¤¢÷gä<ßx@ý?ûù8XîòDŸŽ[»O²â¹?•0ͺÌûÿõTr€qŸì ГØVSÞJ*õ€x¾¡îo!GEÛ6„÷S¹Nņ(Á+×$ >œ™rÆó¾CbtýØó‰ü$‰{êû=Ø„Žüê>èÝuº1^Éã‘ìüõðm‚¯ód ô‡þñ¼òbì¶Ó㹞eßéùhž`tmº¼~e1Û«DÔ}!Òè–òâÏÆgu¢ðñÚô¼;Sþ§v3þoΔ}dŸ~›z¯ZéGîÀ'à>—‹£Ïä«F ÷"þÆ;Ò‡žr%¢FûÎkÙ]vxÈýãi9V9"Ú!_•ãÃßM%ûßSn–Üî[È õV”—¹eMx#ÛäMø!¥ð=¶'©‹¬&[Éžò5ù³û¿Ê.òÿ²XPTL³îú>ñî}+€ÂÖÄái?›¬úœ¾y&Eæ´úþ ÞÊèUÓG‹3²Dòï<ϯã5óÄŒë7E§³)b­š'ãù«<üˆ_d9N¶qGˆ{Jü$ÓKw$}¨;€ÚÁrWöŒÆk©˜üS‰WÈ·e#y@ù,Ëe{eüUÝ÷ ‘ËnC^Åhôq¯q«< <1’žgÓcšýøù:rqŒwœÂSñxÖwLݵ¿Žm(FòhfGÈ SªAA‡òÆë »<÷vßé¯ò24s&xªà#î=â?QïüÝc€€Â’f¼ _Íç®…Œ#“çc9öb2éã">vHÝ£ºÏùïEF䦸ÏÁ„‡c,>6dÝýÉ»³]R ®WO¨$}­›ð9a—ªõ}VËŸâ5x,úø4EŽìwægöŠÀ9ÓåG –E»»”°>’Ä¿òPâÆƒ;ǧÈ9~O$¹6×…ß÷zˆÿ×× :JÍ5’×î¶BWháQõÄŒeq6Ç+•íVôpÄLœ3ò¨kc£xNï9f/>ç”ç\ÍÇ<'wâs0É®iÇCø¡M²"Ÿñh#âÞwDtú~¯ä9•{⊮¯ O©˜^¸{:A÷Ô’FïÿŸÁÕÂçLôÿ'ÆÝD=½½³½;CnGG(Å¥¤¿çìÑànÏ·SÉð7_!ozpJЍ{ª¤¿È=ruJ­C™£Q$Ü;vµ]&Ñø¢fë®xtO¦8›—Äv:1†zÂßcäî„Οˆ>ºò(Vù9ÃèÇyLnº;'åŽ/©Ì©øÕyϱçï­'n#;Ï7ÊœŠîºrjåÞULðÿ°"½¥—%uO×GÍWÙîY…÷¹ÞØþÐÇSàÊWn¿pÛpÔ{@Ñß’¡Ê+ßEÞö•ßc<á;„Çá7ÇúlR3:ÝHì`P—Üy‰á3n¼«ÆËÏ–'"Yâc÷"øä›’; Æ|†ÍwÖzqãÆ¯óœÜÿà™íwûx¤˜¿FebŠt¹­ßÅÑÑ˜'[¢Ÿ›¥Rî¤A¦á÷`ïŽÇ}Nè¬ÅhS|feOt¹7%vE7ôÁ-*¿ü—ê—û°Ý’øþíMÉÒò^“«¢Ü­”¨9ƒ[¢Á̾r ì™ˆË´ÔݲÓ䨘mh8–Bˆ»ïæÜïG•&çÑ1þ«ó¤ðvyØ}2 þÔ!yöáÎιüWkó\Q¢Öd½wFs„ô ™à¹ááÑ:ÙÅóíÿqm¯áÓ/韜ù¾±üJÖKD½§c°Wq°M²uBüùJ"5ñ;yS²C°ác/FÅó^m$R—×Ud/Éñ»<*ÏÜ™¥ößÑeø{x<ßN%E¤9º.÷Ä?,wÆ<ç!qˆç>?žÉÛ_“¬­~Ÿq\~&ÏË…ò%Ù]·9ÚV¹K­OT&žåi„ÆšäÝá”?' FµjP¢ŸŠZﱈ+vgYGsß-< –˜2ÜBx¢i»UœcðÅî¸_#ìWÔ(Úo½aúÆ'®£îkîÎû?áwÇ[ŸòˆëYÞëGÜ‘ŒeÖvtZŠ«|töÒŸ <’'Xÿ3‰×1LLœP=á)¹JèáhŸz{Ï%õÛ8¥Ëƒ‘zöÅ„ÞÑŽzÏþaüÎGÒί&d’/›Éf’Åf±Yd²9l¥aŸ±Ï( ûœ}NiÙì ÊÈæ²¹”Ícó(=›ÏæS:¶€- LìKö%=Á¾b_Qf¶-$eGéÊB©8;ÍNSIv–¥’¼*¯Jþ¼¯Fµø‹üEêÊ«óêT™×à5¨¯ÍkCR‡×¤.¯Kùy=^^àõy}*Åð”‡7ä éyÞ˜7FÉ@åxÞ„êó¦¼UäÍy êÈ[ò–T·æ­q®ö¼=ÊtäÁwâèEÞ™wF™®¼+Ž âAà»óîàƒy0jîÁ{ LAý¯ð^Äûñ~Tšà(˜ä©&‡@þ*•ºó×øPj·ñaÔŠçéÅG¡äh>šªð1| âcùXÈÇññà'ð (?‘¿…ÞæoScþ{ßãøT›OåSq–i|äÓùtÈgò™Ô†Ï⳨ÿˆJÝøl>üþø¹|øù|>®çK¾5Ë¿Cþÿªò%| øŸù/¨çWþõà+ùjäkùZÈ×óõÈ7ñ¿(€oçÛq=ûùA*ÊñC(˜Æ5œä'¡SüJžágp® üä—ùe”¿Æ¯¡¶ëü:øüäa< 5H.éIá¾f¢»@¢ aƒ‚…)Lê$laSMá#| ñ¾Ø›J¤Ÿ]d§V"‡ÈAD.‘ ’<"D ¥Ä‹ÈkŠZÈë?ä…?5"gi&šAÒ\4§*¢h¾£è‚¼»èŽ„üeÑy_1ù`1ù«b(òáb8ò7Ää“Ä$äÓÄ4ÔðûÅ~”9 €?(Ò³â8„½‡Åaª'þÿÐsâˆ8BeD¨8IMÅ)q òÓâ4$çÄ9äçŪ#.Š‹Ø{I\ÆÞkâ:•7Ä HnЛܷ‘K!©žæ.ÓDãG.4ÊhºfPSÍÔ,HlÍFGóï«ùR°–ZKMeµ4ZÈ«hU¨™VMÈ¥U×jP VS«IþZ-­ÕÑjku°·®V—Êkõ´zøiPsc­1Öü5HÞÖÞ¦¬zn=7uÓóéù¨¶^@/@ÁzA½ UÕWè+ÈOÿC_Iô5ú”Ù®o§æúaý0uÕÿÑÿ¡Öúý(UÒCõPuJ?E5ô³úY Ò/ê©°~S¿Iþz˜Fmt©Kjàïägè†NÁ•6,â #•‘ž¥ŒR`Fªl 4"d ¹ƒÁ1†Q c¸1œ^4F#¨…1Òx’QÆxÔ0Á˜ˆ’oï žw!ùĘ ÉOÅÃ1¨‡c:&uw,Ç¢nŽíØì Q€ã뤂$µ“šê8œ (™ÑɈ’™œLgv2£d' J>édƒ¤¤S’:;¥œR(ÙÌA qf9‘¿ó±ó1Õv>q>¡ çSçSjãÌqæ@ò™ó•s~s~£g™³–Ú:ëœu”ÇYïl fÎFg#jøÓÙDMœÝÎnHö8{¨pQ.…‹.ª©QN…²*äSC¡ÊÊ¡pN…s²)œÓ]áœ, ç<©pNG…s²+œœ/Yáœ& çàœÒTÊ€Îi ”ÓXÄÅ3¹©-(h¢Jº¨¦M' Û¼lÓ ò`P§ÎÿQS`›^à{ƒ„B8 á4C­© ½|ó*ŽJði4ÈŸÞ5£1 Â çÔU8§¶Â9 ÎñS8§‘Â9uÎi¤pN!ÚÊG[A3h(B>3éoP~Úú„ö€>¤} Yt”—Ž€>¢PP:ú”Žƒ>¦ ‚ #ÍVè¨BGÝè2hPl–BD#êJÔŽqÆ©‹BAÃò™ŽNgÓH…|^g¾@>Ãòy•¥ò™¨Ok…|ú*ä3žeò™È²ùôgÙX6DzÿLTøg{Š=>ËEmÙÓìi b¹Yn \”ò|,ufùŽ&*t4€:š¨ÐÑ›¬(+JÃFšÈгâô{ž=OYV†Š²²¬,ý{½@¯°r¬š‹ ¸BPL!(®c5X z—Õd5©'«ËêYù1?úkÌÓÛ¬7ëM•YÖ‡Þa}Y_z‘õcý¨:ëÏúS16 ¤ªlDØ`6˜ª±!lUao°7¨<ÃÆPsö&{“žgcÙX*ÍÆ±qÀ‡ãÙxjÉ&° T–Md©¢Âo¦Âo¾‘ð›ÂoŽÂo–Âo†ÂošÂo©~³~K§ð[…ßÒ+ü–Iá·'~ˬð[…ßR+ü–Vá·Œ ¹åPÈ­;;ÆŽ£gÇ\ä6I¡µœ ­MRh-§Bk9NË©pZvîÇý€CsH\„6Ia³l ›uWØ,»Âf!¼-o‹¼o‡’.*ëλð.à»ñn(ãb³î …ðÞ¼75å}8z™Âcýø`>ƒÕS,˜ä#ÉO¡¯& }åæoð7 q1X ôÕL¡¯`…¾új Ð—ŸÄ'Aâ".?…¸òù‡(ïâ®§€»¾À]”¬P–?_Ààì_ò/Qækþ5Ï7¸ë[È¿çßã\ôÈóŨs ÿ ¼‹Áò¥|)jXÁÿ@¾’¯A kù:ìu‘X=¾ÿI½øVŸ^á±|7ß³ïãûPÞÅfO+lÖØì(jv±Y`³Ó¨á,? ôxžŸ™_ÅպجŸÂ`MØÀEbÍ„.tä.óSx¬™Bb ƒ5DH²ˆ,@.k¨ðX]‘SÀ«S¨¬©È-r£L^‘y ²ÉÀcõ7*›¬PYo…ÊüE vr±Ùd…Ír›½¾°Ùd`³È{Š^ÈûMŽÜEb“›¬Xo…ÄZˆ™b&pÈl1›ê‹ù@bõ«/¾ß ÿQüHÝÄR ®áb¹XN#Ê®ðU7à«•àW‰ÕÔEaªNb“ØÞES#Ž©T7ñ·Ø~7TW ¨½(é"¨‘ A WØ©“ÂNÅQqy¨¥¾â$°Ó`§3œgi„ÂNv RØ©/°Ó%H.‹+ÔY\WQÒÅNýÅ-`§‰"L„Qg…šF(¼Ôn¶ î!½¤PSP“Cý5`§± ;ÐRi©h€BPÂꯕÑÊÐp…£FGU£ ÀQÕ©T]H\Ô¤ðÒxà¥@ÔÐGëCB¤ "]¬ &mˆ6„|µ‘ÚHzK§£÷´©ÚT*§MÓ¦Q)m—¶‹Lm·¶›m¶‡,m¯¶— mŸ¶4m¿¶ŸRi´d»ƒ<¥ÃpÁ(ÎuNéÑôeÒñóè ‰24”A7u“Rë–nQZÝÖmʨƒHèùõüüVëkõµ@n[ôÔO¡µœ ¡5ÕOëhÿú9ýò új¦pÚxý’~‰Fè·ô[4R¿­ß¦ú ­ÕQ­ŸÂf½6k¦°Y°Y% µÊ@hB› Z€BhB› ZM…Ð&(l–Ó`  ¡ ›ÕQØ,'ÕBÊj|m| ~PVN…©j—ŒK4D!« )åRÊIuR ¤T’‚ÀK9R©pQ3À  ×:ê¡ÐQ6³•Ù H¬µÙšÚ+¤”M!¥…”²)¤Ô]!¥, )=i7‡S+s„9‚>P¨©£BMÙšÞ’ìÔBa§ì /…˜3Ì4Õœ Ô¢PS…šÆ5ý~R…Žê™ëÌu4Ha¤¦æs°œ‹”ê™)š[Í­4Bá¥@…—+¼4 xé8øæ ”< ìÔXa§æyKP?……r[>Vj ´ÒZÙˆ‚"ªeå±òP +¯•—¦(\TÜÊoå§ç:zF¡£g:*i±ŠÐ(…‘jYÅ­âÔÌ*m•¦—‹ÊAU¶ª ¯ DÔ ˆ¨r5U(¨¾BA# jªPP}…‚F(ÔÔf £‰ 5³FY£À¶Æ€ \ÔLá¢Ö»Ö»4èh x[ù[K­?Pfð¿Â? þ©kí³öQ  ƒÀE.þi¨ðOðÏIð§‚*t‘ü¬ËÖehæ:‘ŸBD“"ê­Ñd…ˆz+D4Y!¢É¶e[È]DÔÛND4Y!¢Þ MVˆ(@!¢Ž "ò§™ 5SøÇ_¡¦ íÌV§›Â3užéjÿjÿ 4å"™‰ ÉôµWÚ+v\$3Þ^c¯|½µ­ª™ho²7ßlosQM…jºÛì+8ËUû&°‡‹pš(„ÓXa›¦Žã¹‹m‚¶ Rئ‰Â6ªiêdu²¢Œ‹j‚ª P¨¦‰B5Mª Q¨&Pa˜IÎ\g.ð‰‹d&;ËåÔBa˜‘ Ã4q6ÃŒPf¤Â0AÄái õJzÊH©bI œ’“ÒÑÓð¸ÓÃ/A™Õ}•¼ÀÕ൷‚] £ðˆ‹+J©{&ÏQ *qï„ÔPwBjÒxܵ€ ÆÃû_ ï¿òþ)_¿±ºËá¯îr¨»M”Hçá»7¥+ðÔ[2‚_Þ9ðȃսˆå‘÷dOÀÿ~YÝyè£î<ôe9às÷S÷°ð§²Âð¤+úUuŸá5åC…¯\¾õûð/Gÿ\?ø(|Äi*º?CE÷?„XƒfÂ/lHŸªý<øMh>ü¿–ô¥òù©xü·¼#¼½ïT ~1ü¼Z¢bíK•o·Œài9Â‡Ó øs£i­òáÖó±ðÕ6¨Hù6þ¼´íðÏæÓ.ó>¦bÞ'•×uZżÏ(묊yŸW1ï‹*æ}EyZWUÌûšò±n¯:L·àKbÌp3áF¸™æF¸™/ê3x˜ f»þó…çd²Tn ›¥ç”–¥…Ÿ”ƒer}#–ÅX³¬nÄš=éF¬Y~7bÍJ»kV¾Q« ¨9«æF©Yu7JÍê¸QjÖÀR³†n”šù¹QjÖÄR³n”šµt£Ô¬ë±¶®oÄ^r}#ÖÞõX7JÍ:ºQjÖÉR³Î®oĺ¹Qj$¾ËXwøC«X±Fl`C܈2.6‹l„Ø%ް±ðcN³ÉðZ.³⚸É>qcºì3!5bŸÃ/ñeság¤aß¹¾û¾E=¶XóÓüÙr7æÊVº1W¶Ê¹²Õî˜ÍÖè+ô•lgeÝ8+û#w(ÛäFUÙ67ªÊ¶»£5ÛáFUÙN=Ì ö7\ƒíqc¨lŸ;N³ý§CØQã£?Ïe 4¾åݘ%ïäÆ,ygw|å]1²^áÝŒk¦Æ»»±IÞßÌ`Và܈#ÿÈ8òOÍš¦?ŸÑ´%ÿcç0¾#ât¾×üÐü‰_51׊Œîx&ò¸#™ÈëÆüD>Œa—E~óªy[Ô2¥ÅECw”þnÄN´p#v¢¥;>‰nÄNttG&Ñ cR!ÑÙ„¥{N¹ñ9ÑÃ*cU!ndNôvG Ñ#ÐP1ÀQÄKÞ Ü‘C,qGñ«5×úJ¬´[¿ˆõÖk¯øËÚo3±×µìÙ¦Ic°àYµt°Ýuµôv€ÝS+îÆ«´ºöp{²ÖVû;í{‰½T›h/³ÿÐÞ³WÙ«µÜ“6Í0iÓÝ“6¶ø¼ö!,¯£};›Z[ ›E[èÚSí;ØÓRÚ®%Õ–À’~ªýèÌq>Ó~…õܤ-ƒ•Ü£ýõÿstUxÚÝ™tVU¶Çÿû|·}›2Ô@hB@T½J‰ˆÒ")@€ -¤Fºt‘&PDDˆŠeAä¹|ŽKP„Á²”§>†yã "ƒo߆æ×{ë±æ­³¾ßÿ˹÷ž¶Ï·÷>7 L¹^WRÆJGøQƒ†#'=);;À GZG )ðË/—{G*! ¤¶âÑ=ñ C‘‰ñ˜ˆ©¡»-C”GTG}܇ÑÝñúa Ò0 £0¹˜"÷!ôD@z,{šˆÃýx­Ð½ñ8’1#…±˜„é(‡@§=:¢cB÷‡#0°WB—,Ñ6ÊÁ ˆR¸Q5ƒ†h‰6脇‘€¾HD*†c$Fcòô‰ ô‰Ú¨‡Æh޶è†iZ/J¢,* ¢Q Ð-ÐнÐO`Òñ$Æà)ÌõnÃE Ñß¡2j¡.bñÚ¡#zàQôG#ÙÈÁd<™)qY)8eSe e[ege”¤ôì@oe¢r°2C9F9)%)kP O9W¹H¹\¹F¹1%eÄÈÀ6eò€òå å)å÷©éC‡.û´ŒÒKÍÈa•T†++(#”ÑÊeÜàQI)VSekeWeoå@å0e¶4œdMRNWÎV.HÏ=ÂZ¦\¥\¯ÌWîTîQîKÏLI·ö++?P•‹£¬Ê/•ß(Ï(Ï*/*¯dŠØ–’•¥•唕”‘ÊZ™£R3ìúÊÆÊæ#ýúxe{eWe‚òqe¢25KldSŽTŽQNRæ)çf Íl/R.W®QnTnSîÊÊŠmh(ßPPV~ <*Œ³O(¿RžVžU^òéa#‡•aÊ ÊHeeœ°±Ó\¯l¯ìªLP>ž5:9ËIT¦*‡)G*Ç(s³FÌr¦*g*ç+—(W(×fË*9›•;”»•¯*ßRT¾'n‚ğ܉F~±ƒ’üÊïDKH±#½¯š\eù ñî—zƒÁ^cÏ0D䯵sy³Ô“ ;¨;Éñÿ{|úïî,Ñ1Èiñ«eàMeÞ-eïmË»7•O­¸Un)-ÜY7åmJ°æ%°ZæsÓÒi))u/ç ™m’“†Î|g>Ê:œm(ç8¨â¼éBUç°óGÔvŽ;ÇÑÐ9åüqnU·*Z¸ÝÐÒmã¶Aw¶;Ý…îrt V VDÏ`å`4‚µ‚ ÐŸßæ·%÷5Í,7Á[èöõVÈÊZâ­Çº¼•×öQ)a_‰ÅÅŸÇ‹úYp¸Ý ÆŽ·; [Ù]„­¯]i£WÚê•v7\i¯W:ê•z…äÚ^»…ÝRÔedú«Í¡¾-Ijcä[¬”:’ÿ7— ú!) $×N’º4)dç —\=KJñ3ä¾we›‰w9"™þÇRÚᘔö8.þ¦NJé„/¤tÆ·Rºˆ:-9ü÷T‹ÿ‰D5 XdÒ7ô-ž4­L&²«°>Pø [­†ÖýøÄŠ·:ã3+ÛÊÆwÖrk9þÓ.k—Åi;ÂŽÀŸí(; gìƒöAüÅþÚþÿeÿ‡ý þjŸ±Ïào¶œµ¶/ãœý‹üøpÊ;åqÁ‰qbpÑiä4ÂOÎ}Î÷¸äœuûS›è®3Õ¼Ê^e3Èkê55ƒ½x/Þ ñÎ{çMZ°|°¢*Öm`Fwß6“ƒßÿf¯¯˜U,ŽÕ¬f‹-³†Ksió<‡q˜YË•¹²YÇ‘iÖs4G› \—ëšÜˆ™MÜœ››ÍÜŠ[™-Ü–Ûš|îÀÌVîÊ]Í6îÎÝÍvNೃûpó'r¢ÙÉÉœl^äA<Èìâ¡<ԼĜavó“ü¤y™³9Ûìá±<ÖìåÎ1<‘'šWx2O6¯ò4žf^ã<Î3ûxÏ2¯ó<žgÞà¼À¼É‹y±y‹—ñ2ó{~ŽŸ3ûy ¯1oóZ^gðÞbÊžþwó壿#>ÆÇÌ>Á2¾ßo¬ž.©æD:QNu§†íÔ¼ÅóEý/ø¾@(+#‰Ìêooò~wg,t,æ:û7 #çõÚxÆzÚšiͲf[Ó­Vž5ÇZh-²[K¬¥Ö2k9=M3i–Dù94×ZAó¬•4Ÿž¡´ÑbZb¦¥ÖZf=c-° ~<%íÃ<)l¡ø©ÎÔ6u§GáÑ@ˆR”Ey(m·Æ£º5ÙšŒv3»¢ƒ'ƒ'Q3øuðkÉQü¬Ÿ%Ömì€mÙ¶íÚAÛ±=¹bdË$0¶Æˆ y¨P^ègÎ×G)?v]½#2t‡¥m´—+ Í´KWÿêÚ¯Ôn¡|ÚJÛh;½@/ÒÚyK¿FN¥h#m¢gi9=G+h%­¢5´–ÖÑzÚ@«éy?w¦Ôóú£¥´Œ^¦=ôжcÄ;–§¡Öf+Ÿ2i"M¢§h2M‘ÌjM§”w󳡧\J£4ÿ0’Mé”.£Ë  dusa‰Ý棤µÂZ©ïZü>ÒäzeÓhCci—ümå^oašGóýg¤Õtíc( •>2)Sú˜(V+\â:†póf”³ò­|9eÔGø íçHÛ×ÏgºÌešÌhÌÿæ÷Jz%¼b^˜wŽAúöj€œ ËÉï[“'óœíΑ¾®y}÷Yw¹gy¶çx®çyA=ÿŒCGÔZ¤çJ(vûÞ|[êÊAGÐ玆ž»z–¶®ö®W?¾Öjy‰½‰VâM#&©`òn‘¶5NŸ|M>í¥Í’•ãðàÕS¶œˆÄãÆÂ4Ü'ÌÕž›\û6È?è·q×êJ ɱ)°ß?cº¡¦c®ä(«$Ù=xñ>ŽÊÙùÎà<.“EÅ)œ*QuŠ¡Æô µ¥®Ô‹úSªd‰£$¾O’X>[âö2ÉV×K–»Sbö>ÚO‡è}úˆ>¥Ïé¦ïéGºlŒñLIn*˜mbLœijZÀ+ðä­UŸ3º¯ÉxD5½TSÑ[u°}Huˆœ7|MCÕ\³ÉW:§o1²²Ý¿ûjGº?ûêE¸—U«áaÕ(÷ŠjutS­îªÑ’]øZ=Uý÷›¾ÖvQ1›Uëy¤ZßlUmàÕXªqf‹¯ì™|Õrh!k‹x¡ÿNßzm„Éè,LAa*õ­f¿ã¿9Ãc¾mí£ÂáèçÛÐ>&œhÖJÞ|}`dŽ?ÀÈ ÏÁÈüÎ «¡­0ÊýQXí„5d÷™UaMtÖB'am÷‚0ƬÖsÖ7… ÜKÂX÷¢0Î>"lâ‰góZÚ ãíO„+Ìz™Ùa9É{‰ R%+Ì–}öÏ÷Ó%?å’F婊ì©:KMdWµ¦ŽÔMöU_9Ó –ÓÂ4Y³\YÅ]ªÉÒª¯)˜¡šj¶ûJç¼7|•3÷k¾zÕ¼—T£¼ÝªÕ½—UkxªÑÞÕšÞ^ÕZÞ+ªµ½WUëyûTë{¯«6ðÞTå¾²ÇUËá)ߊÞzµßdµßµßTßNf›o!o§oo»ooƒoÏ_ßêÞ&ß*^¾oo³oo‹oo«oo›oo‡oïßÞ‹¾=¸¸¿â\AWœtå¾cµ[ÙñZ³MVÜ ¹øšß|öÀJõQ¿9Û·²ÿ”guacamole-client-0.9.9/guacamole/src/main/webapp/fonts/carlito/LICENSE0100664 0001751 0001751 00000010232 12612226251 027245 0ustar00mjumpermjumper0000000 0000000 Copyright (c) 2010-2013 by tyPoland Lukasz Dziedzic with Reserved Font Name "Carlito". This Font Software is licensed under the SIL Open Font License, Version 1.1 as shown below. SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting ? in part or in whole ? any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1) Neither the Font Software nor any of its individual components,in Original or Modified Versions, may be sold by itself. 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. guacamole-client-0.9.9/guacamole/src/main/webapp/license.txt0100664 0001751 0001751 00000002137 12612226251 025642 0ustar00mjumpermjumper0000000 0000000 /*! * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ guacamole-client-0.9.9/guacamole/src/main/webapp/translations/fr.json0100664 0001751 0001751 00000075357 12645510255 027526 0ustar00mjumpermjumper0000000 0000000 { "NAME" : "Français", "APP" : { "ACTION_ACKNOWLEDGE" : "Confirmer", "ACTION_CANCEL" : "Annuler", "ACTION_CLONE" : "Cloner", "ACTION_CONTINUE" : "Continuer", "ACTION_DELETE" : "Supprimer", "ACTION_DELETE_SESSIONS" : "Fermer les Sessions", "ACTION_LOGIN" : "Se connecter", "ACTION_LOGOUT" : "Se déconnecter", "ACTION_MANAGE_CONNECTIONS" : "Connexions", "ACTION_MANAGE_PREFERENCES" : "Préférences", "ACTION_MANAGE_SETTINGS" : "Paramètres", "ACTION_MANAGE_SESSIONS" : "Sessions Actives", "ACTION_MANAGE_USERS" : "Utilisateurs", "ACTION_NAVIGATE_BACK" : "Retour", "ACTION_NAVIGATE_HOME" : "Accueil", "ACTION_SAVE" : "Enregistrer", "ACTION_UPDATE_PASSWORD" : "Mettre à jour mot de passe", "DIALOG_HEADER_ERROR" : "Erreur", "ERROR_PASSWORD_BLANK" : "Votre mot de passe ne peut pas être vide.", "ERROR_PASSWORD_MISMATCH" : "Le mot de passe ne correspond pas.", "FIELD_HEADER_PASSWORD" : "Mot de passe:", "FIELD_HEADER_PASSWORD_AGAIN" : "Répéter mot de passe:", "FIELD_PLACEHOLDER_FILTER" : "Filtre", "FORMAT_DATE_TIME_PRECISE" : "dd-MM-yyyy HH:mm:ss", "INFO_ACTIVE_USER_COUNT" : "Actuellement utilisé par {USERS} {USERS, plural, one{utilisateur} other{utilisateurs}}.", "NAME" : "Guacamole ${project.version}", "TEXT_HISTORY_DURATION" : "{VALUE} {UNIT, select, second{{VALUE, plural, one{seconde} other{secondes}}} minute{{VALUE, plural, one{minute} other{minutes}}} hour{{VALUE, plural, one{heure} other{heures}}} day{{VALUE, plural, one{jour} other{jours}}} other{}}" }, "CLIENT" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Vider", "ACTION_DISCONNECT" : "Déconnecter", "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", "ACTION_RECONNECT" : "Reconnecter", "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", "ACTION_UPLOAD_FILES" : "Envoyer Fichiers", "DIALOG_HEADER_CONNECTING" : "Connexion", "DIALOG_HEADER_CONNECTION_ERROR" : "Erreur de connexion", "DIALOG_HEADER_DISCONNECTED" : "Déconnecté", "ERROR_CLIENT_201" : "Cette connexion a été fermée car le serveur est occupé. Merci d'attendre quelques minutes et de réessayer.", "ERROR_CLIENT_202" : "Le serveur Guacamole a fermé cette connexion car l'ordinateur distant a mis trop de temps à répondre. Merci de réessayer ou de contacter l'administrateur.", "ERROR_CLIENT_203" : "Le serveur distant a rencontré une erreur et a fermé la connexion. Merci de réessayer ou de contacter l'administrateur.", "ERROR_CLIENT_205" : "Cette connexion a été fermée car elle est en conflit avec une autre. Merci de réessayer plus tard.", "ERROR_CLIENT_301" : "Connexion echouée. Merci d'essayer encore.", "ERROR_CLIENT_303" : "Vous ne disposez pas des permissions pour accéder à cette connexion. Si vous avez besoin de ces droits demandez à l'administrateur qu'il vous ajoute à la lise des utilisateurs autorisés, ou de vérifier les paramètres système.", "ERROR_CLIENT_308" : "Le serveur Guacamole a fermé la connexion car il n'y avait pas de réponse de votre navigateur Internet et qu'il l'a considéré comme déconnecté. Cela se produit à cause de problèmes réseaux (mauvais signal Wi-Fi ou réseau très lent). Merci de vérifier votre réseau et de réessayer.", "ERROR_CLIENT_31D" : "Le serveur Guacamole interdit les connexions car vous avez dépassé la limite de connexion simultanée par utilisateur. Merci de fermer une ou plusieurs connexions et de reéssayer.", "ERROR_CLIENT_DEFAULT" : "Une erreur interne est apparue dans le serveur Guacamole et la connexion a été fermée. Si le problème persiste, merci de notifier l'administrateur ou de regarder les journaux système.", "ERROR_TUNNEL_201" : "Le serveur Guacamole a rejeté cette tentative de connexion car il y a trop de connexions ouvertes. Merci d'attendre quelques minutes et de réessayer.", "ERROR_TUNNEL_202" : "La connexion a été fermée car le serveur met trop de temps à répondre. En général, il s'agit de problème réseau comme un réseau Wi-Fi trop lent ou un réseau très lent. Merci de vérifier votre réseau ou de contacter l'administrateur.", "ERROR_TUNNEL_203" : "Le serveur a rencontré une erreur et a fermé la connexion. Merci de réessayer ou de contacter l'administrateur.", "ERROR_TUNNEL_204" : "Le connexion demandée n'existe pas. Merci de vérifier le nom et de réessayer.", "ERROR_TUNNEL_205" : "Cette connexion est actuellement utilisée et les connexions multiples ne sont pas autorisées. Merci de réeassyer plus tard.", "ERROR_TUNNEL_301" : "Vous n'avez pas le droit d'accéder à cette connexion car vous n'êtes pas connecté. Merci de vous connecter et de réessayer.", "ERROR_TUNNEL_303" : "Vous n'avez pas le droit d'accéder à cette connexion. Si vous souhaitez y avoir accès, merci de demander à l'administrateur de vous ajouter dans la liste des utilisateurs autorisés ou de vérifier les paramètre système.", "ERROR_TUNNEL_308" : "Le serveur Guacamole a fermé la connexion car il n'y avait pas de réponse de votre navigateur Internet et qu'il l'a considéré comme déconnecté. Cela se produit à cause de problèmes réseaux (mauvais signal Wi-Fi ou réseau très lent). Merci de vérifier votre réseau et de réessayer.", "ERROR_TUNNEL_31D" : "Le serveur Guacamole interdit cette connexion car vous avez dépassé la limite de connexions simultanées par utilisateur. Merci de fermer une ou plusieurs connexions et de reéssayer.", "ERROR_TUNNEL_DEFAULT" : "Une erreur interne est apparue dans le serveur Guacamole et la connexion a été fermée. Si le problème persiste, merci de notifier l'administrateur ou de regarder les journaux système.", "ERROR_UPLOAD_100" : "Le transfert de fichier n'est pas activé ou supporté. Merci de contacter l'administrateur ou de vérifier les journaux système.", "ERROR_UPLOAD_201" : "Trop de fichiers sont transférés. Merci d'attendre que les transferts en cours se terminent et réessayer.", "ERROR_UPLOAD_202" : "Le fichier ne peut être transféré car le serveur distant met trop de temps à répondre. Merci de réessayer ou de contacter votre administrateur.", "ERROR_UPLOAD_203" : "Le serveur distant a rencontré une erreur durant le transfert. Merci de reéssayer et de contacter l'administrateur.", "ERROR_UPLOAD_204" : "La destination du transfert de fichier n'existe pas. Merci de vérifier que la destination existe et de réessayer.", "ERROR_UPLOAD_205" : "La destination du transfert de fichier est actuellement verouillée. Merci de patienter la fin des tâches en cours et de réessayer.", "ERROR_UPLOAD_301" : "Vous n'avez pas la permission d'envoyer ce fichier car vous n'êtes pas connecté. Merci de vous connecter et de réessayer.", "ERROR_UPLOAD_303" : "Vous n'avez pas la permission d'envoyer le fichier. Si vous avez besoin de cet accès, merci de vérifier vos paramètres system ou de valider avec votre administrateur.", "ERROR_UPLOAD_308" : "Le transfert de fichier s'est bloqué. En général, il s'agit d'un problème réseau comme un signal Wi-Fi faible ou un réseau très lent. Merci de vérifier votre réseau et de réessayer.", "ERROR_UPLOAD_31D" : "Trop de fichiers sont actuellement transférés. Merci d'attendre que les transferts en cours soient terminés et de réessayer plus tard.", "ERROR_UPLOAD_DEFAULT" : "Une erreur interne est apparue dans le serveur Guacamole et la connexion a été fermée. Si le problème persiste, merci de notifier l'administrateur ou de regarder les journaux système.", "HELP_CLIPBOARD" : "Texte copié/coupé dans Guacamole apparaîtra ici. Changer le texte ci dessous affectera le presse-papiers distant.", "HELP_INPUT_METHOD_NONE" : "Aucune méthode de saisie utilisée. Clavier accepté depuis un clavier physique connecté.", "HELP_INPUT_METHOD_OSK" : "Affiche et utilise la saisie du clavier virtuel intégré dans Guacamole. Le clavier virtuel permet d'utiliser des combinaisons de touches autrement impossibles (comme Ctrl-Alt-Supp).", "HELP_INPUT_METHOD_TEXT" : "Affiche et utilise la saisie du clavier virtuel intégré dans Guacamole. Ceci est nécessaire pou les périphériques mobiles ne disposant pas de clavier physique.", "HELP_MOUSE_MODE" : "Détermine comment la souris distante se comporte selon les événements.", "HELP_MOUSE_MODE_ABSOLUTE" : "Appuyer pour cliquer. Le clique s'effectue à l'endroit de l'appui.", "HELP_MOUSE_MODE_RELATIVE" : "Glisser pour déplacer le pointeur de la souris et appuyer opur cliquer. Le clique s'effectue à l'endroit du pointeur.", "INFO_NO_FILE_TRANSFERS" : "Pas de transfert de fichier.", "NAME_INPUT_METHOD_NONE" : "Aucune", "NAME_INPUT_METHOD_OSK" : "Clavier virtuel", "NAME_INPUT_METHOD_TEXT" : "Clavier", "NAME_KEY_CTRL" : "Ctrl", "NAME_KEY_ALT" : "Alt", "NAME_KEY_ESC" : "Echap", "NAME_KEY_TAB" : "Tab", "NAME_MOUSE_MODE_ABSOLUTE" : "Écran tactile", "NAME_MOUSE_MODE_RELATIVE" : "Pavé tactile", "SECTION_HEADER_CLIPBOARD" : "Presse-papiers", "SECTION_HEADER_DEVICES" : "Appareils", "SECTION_HEADER_DISPLAY" : "Affichage", "SECTION_HEADER_FILE_TRANSFERS" : "Transfers de fichiers", "SECTION_HEADER_INPUT_METHOD" : "Méthode de saisie", "SECTION_HEADER_MOUSE_MODE" : "Mode émulation souris", "TEXT_ZOOM_AUTO_FIT" : "Adapté à la fenêtre du navigateur", "TEXT_CLIENT_STATUS_IDLE" : "Inactif.", "TEXT_CLIENT_STATUS_CONNECTING" : "Connexion à Guacamole...", "TEXT_CLIENT_STATUS_DISCONNECTED" : "Vous avez été deconnecté.", "TEXT_CLIENT_STATUS_WAITING" : "Connecté à Guacamole. En attente de réponse...", "TEXT_RECONNECT_COUNTDOWN" : "Reconnexion dans {REMAINING} {REMAINING, plural, one{seconde} other{secondes}}...", "TEXT_FILE_TRANSFER_PROGRESS" : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}", "URL_OSK_LAYOUT" : "layouts/fr-fr-azerty.json" }, "FORM" : { "HELP_SHOW_PASSWORD" : "Cliquer pour afficher le mot de passe", "HELP_HIDE_PASSWORD" : "Cliquer pour masquer le mot de passe" }, "HOME" : { "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", "INFO_NO_RECENT_CONNECTIONS" : "Pas de connexion récente.", "PASSWORD_CHANGED" : "Mot de passe changé.", "SECTION_HEADER_ALL_CONNECTIONS" : "Toutes les Connexions", "SECTION_HEADER_RECENT_CONNECTIONS" : "Connexions récentes" }, "LOGIN": { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "ERROR_INVALID_LOGIN" : "Identifiant Incorrect", "FIELD_HEADER_USERNAME" : "Identifiant", "FIELD_HEADER_PASSWORD" : "Mot de passe" }, "MANAGE_CONNECTION" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_CLONE" : "@:APP.ACTION_CLONE", "ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_SAVE" : "@:APP.ACTION_SAVE", "DIALOG_HEADER_CONFIRM_DELETE" : "Supprimer Connexion", "DIALOG_HEADER_ERROR" : "Erreur", "FIELD_HEADER_LOCATION" : "Lieu:", "FIELD_HEADER_NAME" : "Nom:", "FIELD_HEADER_PROTOCOL" : "Protocole:", "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "Active", "INFO_CONNECTION_NOT_USED" : "Cette connexion n'a jamais été utilisée.", "SECTION_HEADER_EDIT_CONNECTION" : "Modifier Connexion", "SECTION_HEADER_HISTORY" : "Historique d'utilisation", "SECTION_HEADER_PARAMETERS" : "Paramètres", "TABLE_HEADER_HISTORY_USERNAME" : "Identifiant", "TABLE_HEADER_HISTORY_START" : "Ouverture", "TABLE_HEADER_HISTORY_DURATION" : "Durée", "TEXT_CONFIRM_DELETE" : "Les connexions ne pourront être restaurées une fois supprimées. Êtes-vous certains de vouloir supprimer cette connexion ?", "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" }, "MANAGE_CONNECTION_GROUP" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_SAVE" : "@:APP.ACTION_SAVE", "DIALOG_HEADER_CONFIRM_DELETE" : "Supprimer Groupe de Connexion", "DIALOG_HEADER_ERROR" : "Erreur", "FIELD_HEADER_LOCATION" : "Lieu:", "FIELD_HEADER_NAME" : "Nom:", "FIELD_HEADER_TYPE" : "Type:", "NAME_TYPE_BALANCING" : "Répartition", "NAME_TYPE_ORGANIZATIONAL" : "Organizationel", "SECTION_HEADER_EDIT_CONNECTION_GROUP" : "Modifier Groupe de Connexion", "TEXT_CONFIRM_DELETE" : "Les groupes de connexions ne pourront être restaurés une fois supprimés. Êtes-vous certains de vouloir supprimer ce groupe de connexion ?" }, "MANAGE_USER" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_SAVE" : "@:APP.ACTION_SAVE", "DIALOG_HEADER_CONFIRM_DELETE" : "Supprimer Utilisateur", "DIALOG_HEADER_ERROR" : "Erreur", "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", "FIELD_HEADER_ADMINISTER_SYSTEM" : "Administer system:", "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "Modifier son propre mot de passe:", "FIELD_HEADER_CREATE_NEW_USERS" : "Créer nouveaux utilisateurs:", "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Créer nouvelles connexions:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Créer nouveaux groupes de connexion:", "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", "FIELD_HEADER_USERNAME" : "Identifiant:", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "SECTION_HEADER_CONNECTIONS" : "Connexions", "SECTION_HEADER_EDIT_USER" : "Modifier Utilisateur", "SECTION_HEADER_PERMISSIONS" : "Permissions", "TEXT_CONFIRM_DELETE" : "Les utilisateurs ne pourront être restaurés une fois supprimés. Êtes-vous certains de vouloir supprimer cet utilisateur?" }, "PROTOCOL_RDP" : { "FIELD_HEADER_CLIENT_NAME" : "Nom du Client:", "FIELD_HEADER_COLOR_DEPTH" : "Qualité couleur:", "FIELD_HEADER_CONSOLE" : "Console Administrateur:", "FIELD_HEADER_CONSOLE_AUDIO" : "Support son en console:", "FIELD_HEADER_DISABLE_AUDIO" : "Désactiver son:", "FIELD_HEADER_DISABLE_AUTH" : "Désactiver authentification:", "FIELD_HEADER_DOMAIN" : "Nom du domaine:", "FIELD_HEADER_DPI" : "Résolution (ppp):", "FIELD_HEADER_DRIVE_PATH" : "Chemin du lecteur:", "FIELD_HEADER_ENABLE_DESKTOP_COMPOSITION" : "Activer la composition du bureau (Aero):", "FIELD_HEADER_ENABLE_DRIVE" : "Activer lecteur réseau:", "FIELD_HEADER_ENABLE_FONT_SMOOTHING" : "Enable font smoothing (ClearType):", "FIELD_HEADER_ENABLE_FULL_WINDOW_DRAG" : "Activer pleine fenêtre de glisser:", "FIELD_HEADER_ENABLE_MENU_ANIMATIONS" : "Activer les animations de menu:", "FIELD_HEADER_ENABLE_PRINTING" : "Activer imprimante:", "FIELD_HEADER_ENABLE_SFTP" : "Activer SFTP:", "FIELD_HEADER_ENABLE_THEMING" : "Activer thématisation:", "FIELD_HEADER_ENABLE_WALLPAPER" : "Activer fond d'écran:", "FIELD_HEADER_HEIGHT" : "Hauteur:", "FIELD_HEADER_HOSTNAME" : "Nom d'hôte:", "FIELD_HEADER_IGNORE_CERT" : "Ignorer le certificat du serveur:", "FIELD_HEADER_INITIAL_PROGRAM" : "Programme de démarrage:", "FIELD_HEADER_PASSWORD" : "Mot de passe:", "FIELD_HEADER_PORT" : "Port:", "FIELD_HEADER_REMOTE_APP_ARGS" : "Paramètres:", "FIELD_HEADER_REMOTE_APP_DIR" : "Répertoire de travail:", "FIELD_HEADER_REMOTE_APP" : "Programme:", "FIELD_HEADER_SECURITY" : "Mode de Sécurité:", "FIELD_HEADER_SERVER_LAYOUT" : "Agencement clavier:", "FIELD_HEADER_SFTP_HOSTNAME" : "Nom d'hôte:", "FIELD_HEADER_SFTP_PASSPHRASE" : "Phrase secrète:", "FIELD_HEADER_SFTP_PASSWORD" : "Mot de passe:", "FIELD_HEADER_SFTP_PORT" : "Port:", "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Clé privée:", "FIELD_HEADER_SFTP_USERNAME" : "Identifiant:", "FIELD_HEADER_STATIC_CHANNELS" : "Static channel names:", "FIELD_HEADER_USERNAME" : "Identifiant:", "FIELD_HEADER_WIDTH" : "Largeur:", "FIELD_OPTION_COLOR_DEPTH_16" : "Faibles couleurs (16-bit)", "FIELD_OPTION_COLOR_DEPTH_24" : "Vraies couleurs (24-bit)", "FIELD_OPTION_COLOR_DEPTH_32" : "Vraies couleurs (32-bit)", "FIELD_OPTION_COLOR_DEPTH_8" : "256 couleurs", "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "", "FIELD_OPTION_SECURITY_ANY" : "Aucune", "FIELD_OPTION_SECURITY_EMPTY" : "", "FIELD_OPTION_SECURITY_NLA" : "NLA (Network Level Authentication)", "FIELD_OPTION_SECURITY_RDP" : "Chiffrement RDP", "FIELD_OPTION_SECURITY_TLS" : "Chiffrement TLS", "FIELD_OPTION_SERVER_LAYOUT_DE_DE_QWERTZ" : "German (Qwertz)", "FIELD_OPTION_SERVER_LAYOUT_EMPTY" : "", "FIELD_OPTION_SERVER_LAYOUT_EN_US_QWERTY" : "US English (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_FAILSAFE" : "Unicode", "FIELD_OPTION_SERVER_LAYOUT_FR_FR_AZERTY" : "French (Azerty)", "FIELD_OPTION_SERVER_LAYOUT_IT_IT_QWERTY" : "Italian (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_SV_SE_QWERTY" : "Swedish (Qwerty)", "NAME" : "RDP", "SECTION_HEADER_AUTHENTICATION" : "Authentification", "SECTION_HEADER_BASIC_PARAMETERS" : "Paramètres de base", "SECTION_HEADER_DEVICE_REDIRECTION" : "Redirection Périphérique", "SECTION_HEADER_DISPLAY" : "Affichage", "SECTION_HEADER_NETWORK" : "Réseau", "SECTION_HEADER_PERFORMANCE" : "Performance", "SECTION_HEADER_REMOTEAPP" : "RemoteApp", "SECTION_HEADER_SFTP" : "SFTP" }, "PROTOCOL_SSH" : { "FIELD_HEADER_FONT_NAME" : "Nom police:", "FIELD_HEADER_FONT_SIZE" : "Taille police:", "FIELD_HEADER_ENABLE_SFTP" : "Activer SFTP:", "FIELD_HEADER_HOSTNAME" : "Nom d'hôte:", "FIELD_HEADER_USERNAME" : "Identifiant:", "FIELD_HEADER_PASSWORD" : "Mot de passe:", "FIELD_HEADER_PASSPHRASE" : "Phrase secrète:", "FIELD_HEADER_PORT" : "Port:", "FIELD_HEADER_PRIVATE_KEY" : "Clé privée:", "FIELD_OPTION_FONT_SIZE_8" : "8", "FIELD_OPTION_FONT_SIZE_9" : "9", "FIELD_OPTION_FONT_SIZE_10" : "10", "FIELD_OPTION_FONT_SIZE_11" : "11", "FIELD_OPTION_FONT_SIZE_12" : "12", "FIELD_OPTION_FONT_SIZE_14" : "14", "FIELD_OPTION_FONT_SIZE_18" : "18", "FIELD_OPTION_FONT_SIZE_24" : "24", "FIELD_OPTION_FONT_SIZE_30" : "30", "FIELD_OPTION_FONT_SIZE_36" : "36", "FIELD_OPTION_FONT_SIZE_48" : "48", "FIELD_OPTION_FONT_SIZE_60" : "60", "FIELD_OPTION_FONT_SIZE_72" : "72", "FIELD_OPTION_FONT_SIZE_96" : "96", "FIELD_OPTION_FONT_SIZE_EMPTY" : "", "NAME" : "SSH", "SECTION_HEADER_AUTHENTICATION" : "Authentification", "SECTION_HEADER_DISPLAY" : "Affichage", "SECTION_HEADER_NETWORK" : "Réseau", "SECTION_HEADER_SFTP" : "SFTP" }, "PROTOCOL_TELNET" : { "FIELD_HEADER_FONT_NAME" : "Nom police:", "FIELD_HEADER_FONT_SIZE" : "Taille police:", "FIELD_HEADER_HOSTNAME" : "Nom d'hôte:", "FIELD_HEADER_USERNAME" : "Identifiant:", "FIELD_HEADER_PASSWORD" : "Mot de passe:", "FIELD_HEADER_PASSWORD_REGEX" : "Expression régulière Mot de passe:", "FIELD_HEADER_PORT" : "Port:", "FIELD_OPTION_FONT_SIZE_8" : "8", "FIELD_OPTION_FONT_SIZE_9" : "9", "FIELD_OPTION_FONT_SIZE_10" : "10", "FIELD_OPTION_FONT_SIZE_11" : "11", "FIELD_OPTION_FONT_SIZE_12" : "12", "FIELD_OPTION_FONT_SIZE_14" : "14", "FIELD_OPTION_FONT_SIZE_18" : "18", "FIELD_OPTION_FONT_SIZE_24" : "24", "FIELD_OPTION_FONT_SIZE_30" : "30", "FIELD_OPTION_FONT_SIZE_36" : "36", "FIELD_OPTION_FONT_SIZE_48" : "48", "FIELD_OPTION_FONT_SIZE_60" : "60", "FIELD_OPTION_FONT_SIZE_72" : "72", "FIELD_OPTION_FONT_SIZE_96" : "96", "FIELD_OPTION_FONT_SIZE_EMPTY" : "", "NAME" : "Telnet", "SECTION_HEADER_AUTHENTICATION" : "Authentification", "SECTION_HEADER_DISPLAY" : "Affichage", "SECTION_HEADER_NETWORK" : "Réseau" }, "PROTOCOL_VNC" : { "FIELD_HEADER_AUDIO_SERVERNAME" : "Serveur de son:", "FIELD_HEADER_COLOR_DEPTH" : "Qualité couleur:", "FIELD_HEADER_CURSOR" : "Curseur:", "FIELD_HEADER_DEST_HOST" : "Hôte distant:", "FIELD_HEADER_DEST_PORT" : "Port distant:", "FIELD_HEADER_ENABLE_AUDIO" : "Activer son:", "FIELD_HEADER_ENABLE_SFTP" : "Activer SFTP:", "FIELD_HEADER_HOSTNAME" : "Nom d'hôte:", "FIELD_HEADER_PASSWORD" : "Mot de passe:", "FIELD_HEADER_PORT" : "Port:", "FIELD_HEADER_READ_ONLY" : "Lecture seule:", "FIELD_HEADER_SFTP_HOSTNAME" : "Nom d'hôte:", "FIELD_HEADER_SFTP_PASSPHRASE" : "Phrase secrète:", "FIELD_HEADER_SFTP_PASSWORD" : "Mot de passe:", "FIELD_HEADER_SFTP_PORT" : "Port:", "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Clé privée:", "FIELD_HEADER_SFTP_USERNAME" : "Identifiant:", "FIELD_HEADER_SWAP_RED_BLUE" : "Swap red/blue components:", "FIELD_OPTION_COLOR_DEPTH_8" : "256 couleurs", "FIELD_OPTION_COLOR_DEPTH_16" : "Faibles couleurs (16-bit)", "FIELD_OPTION_COLOR_DEPTH_24" : "Vraies couleurs (24-bit)", "FIELD_OPTION_COLOR_DEPTH_32" : "Vraies couleurs (32-bit)", "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "", "FIELD_OPTION_CURSOR_EMPTY" : "", "FIELD_OPTION_CURSOR_LOCAL" : "Local", "FIELD_OPTION_CURSOR_REMOTE" : "Distant", "NAME" : "VNC", "SECTION_HEADER_AUDIO" : "Son", "SECTION_HEADER_AUTHENTICATION" : "Authentification", "SECTION_HEADER_CLIPBOARD" : "Presse-papiers", "SECTION_HEADER_DISPLAY" : "Affichage", "SECTION_HEADER_NETWORK" : "Réseau", "SECTION_HEADER_REPEATER" : "Répéteur VNC", "SECTION_HEADER_SFTP" : "SFTP" }, "SETTINGS" : { "SECTION_HEADER_SETTINGS" : "Paramètres" }, "SETTINGS_CONNECTIONS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_NEW_CONNECTION" : "Nouvelle Connexion", "ACTION_NEW_CONNECTION_GROUP" : "Nouveau Groupe", "DIALOG_HEADER_ERROR" : "Erreur", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "HELP_CONNECTIONS" : "Cliquer ou appuyer sur une connexion en dessous pour la gérer. Selon vos permissions, les connexions peuvent être ajoutées, supprimées, leur propriétés (protocole, nom d'hôte, port, etc) changées.", "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", "SECTION_HEADER_CONNECTIONS" : "Connexions" }, "SETTINGS_CONNECTION_HISTORY" : { "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Nom de connexion", "TABLE_HEADER_SESSION_DURATION" : "Durée", "TABLE_HEADER_SESSION_STARTDATE" : "Ouvert depuis", "TABLE_HEADER_SESSION_USERNAME" : "Identifiant", "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" }, "SETTINGS_PREFERENCES" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "ERROR_PASSWORD_BLANK" : "Votre mot de passe ne peut être vide.", "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", "FIELD_HEADER_LANGUAGE" : "Langue affichée:", "FIELD_HEADER_PASSWORD" : "Mot de passe:", "FIELD_HEADER_PASSWORD_OLD" : "Mot de passe actuel:", "FIELD_HEADER_PASSWORD_NEW" : "Nouveau mot de passe:", "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "Confirmer nouveau mot de passe:", "FIELD_HEADER_USERNAME" : "Identifiant:", "HELP_DEFAULT_INPUT_METHOD" : "La méthode de saisie par défaut détermine comme les événements clavier sont reçus par Guacamole. Modifier ces paramètres peut être nécessaire pour l'utilisateur des smartphone/tablette. Ces paramètres peuvent être écrasés pour chaque connexion dans le menu de Guacamole.", "HELP_DEFAULT_MOUSE_MODE" : "Le mode d'émulation de la souris détermine comment la souris distante se comportera dans les nouvelles connexions. Ce paramètre peut être définit dans chaque connexion dans le menu de Guacamole.", "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", "HELP_LANGUAGE" : "Selectionner une langue différente pour changer tout le texte dans Guacamole. Les choix dépendent des langues qui sont installées.", "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", "HELP_UPDATE_PASSWORD" : "Si vous souhaitez changer votre mot de passe, entrer vos mot de passe actuel et le nouveau mot de passe en dessous puis cliquer sur \"Mettre à jour Mot de passe\". Le changement prendra effet immédiatement.", "INFO_PASSWORD_CHANGED" : "Mot de passe changé.", "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Méthode de saisie par défaut", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Mode émulation souris par défaut", "SECTION_HEADER_UPDATE_PASSWORD" : "Modifier Mot de passe" }, "SETTINGS_USERS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_NEW_USER" : "Nouvel Utilisateur", "DIALOG_HEADER_ERROR" : "Erreur", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "HELP_USERS" : "Cliquer ou appuyer sur un utilisateur en dessous pour le gérer. Selon vos permissions, les utilisateurs peuvent être ajoutés, supprimés, leur mot de passe changé.", "SECTION_HEADER_USERS" : "Utilisateur" }, "SETTINGS_SESSIONS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_DELETE" : "Fermer Sessions", "DIALOG_HEADER_CONFIRM_DELETE" : "Fermer Sessions", "DIALOG_HEADER_ERROR" : "Erreur", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", "HELP_SESSIONS" : "Toutes les connexions actives Guacamole sont listées ici. Si vous souhaitez en fermer une ou plusieurs, sélectionner les et cliquer sur \"Fermer Sessions\". La fermeture d'une session déconnectera immédiatement l'utilisateur.", "INFO_NO_SESSIONS" : "Pas de session ouverte", "SECTION_HEADER_SESSIONS" : "Sessions Ouvertes", "TABLE_HEADER_SESSION_USERNAME" : "Identifiant", "TABLE_HEADER_SESSION_STARTDATE" : "Ouvert depuis", "TABLE_HEADER_SESSION_REMOTEHOST" : "Hôte distant", "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Nom de connexion", "TEXT_CONFIRM_DELETE" : "Êtes-vous certains de vouloir fermer toutes les connexions sélectionnées ? Les utilisateurs utilisant ces sessions seront immédiatement déconnectés." }, "USER_MENU" : { "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME" } } guacamole-client-0.9.9/guacamole/src/main/webapp/translations/nl.json0100664 0001751 0001751 00000102767 12645510255 027524 0ustar00mjumpermjumper0000000 0000000 { "NAME" : "Nederlands", "APP" : { "ACTION_ACKNOWLEDGE" : "OK", "ACTION_CANCEL" : "Annuleer", "ACTION_CLONE" : "Kloon", "ACTION_CONTINUE" : "Verder", "ACTION_DELETE" : "Verwijder", "ACTION_DELETE_SESSIONS" : "Verwijder Sessies", "ACTION_LOGIN" : "Inloggen", "ACTION_LOGOUT" : "Uitloggen", "ACTION_MANAGE_CONNECTIONS" : "Verbindingen", "ACTION_MANAGE_PREFERENCES" : "Voorkeuren", "ACTION_MANAGE_SETTINGS" : "Instellingen", "ACTION_MANAGE_SESSIONS" : "Actieve Sessies", "ACTION_MANAGE_USERS" : "Gebruikers", "ACTION_NAVIGATE_BACK" : "Terug", "ACTION_NAVIGATE_HOME" : "Home", "ACTION_SAVE" : "Opslaan", "ACTION_SEARCH" : "Zoeken", "ACTION_UPDATE_PASSWORD" : "Wijzig Wachtwoord", "ACTION_VIEW_HISTORY" : "Gebruikgeschiedenis", "DIALOG_HEADER_ERROR" : "Fout", "ERROR_PASSWORD_BLANK" : "Uw wachtwoord mag niet leeg zijn.", "ERROR_PASSWORD_MISMATCH" : "De opgegeven wachtwoorden komen niet overeen.", "FIELD_HEADER_PASSWORD" : "Wachtwoord:", "FIELD_HEADER_PASSWORD_AGAIN" : "Nogmaals uw wachtwoord:", "FIELD_PLACEHOLDER_FILTER" : "Filter", "FORMAT_DATE_TIME_PRECISE" : "yyyy-MM-dd HH:mm:ss", "INFO_ACTIVE_USER_COUNT" : "Op dit moment in gebruik door {USERS} {USERS, plural, one{gebruiker} other{gebruikers}}.", "NAME" : "Guacamole ${project.version}", "TEXT_HISTORY_DURATION" : "{VALUE} {UNIT, select, second{{VALUE, plural, one{seconde} other{seconden}}} minute{{VALUE, plural, one{minuut} other{minuten}}} hour{{VALUE, plural, one{uur} other{uren}}} day{{VALUE, plural, one{dag} other{dagen}}} other{}}" }, "CLIENT" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Wis lijst", "ACTION_DISCONNECT" : "Verbreek Verbinding", "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", "ACTION_RECONNECT" : "Verbind Opnieuw", "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", "ACTION_UPLOAD_FILES" : "Upload Bestanden", "DIALOG_HEADER_CONNECTING" : "Aan Het Verbinden", "DIALOG_HEADER_CONNECTION_ERROR" : "Verbindingsfout", "DIALOG_HEADER_DISCONNECTED" : "Niet Verbonden", "ERROR_CLIENT_201" : "Deze verbinding is gesloten omdat de server druk is. Wacht een paar minuten en probeer het opnieuw.", "ERROR_CLIENT_202" : "De Guacamole server heeft de verbinding gesloten omdat het externe bureaublad te lang niet heeft gereageerd. Probeer het altublieft opnieuw of neem contact op met uw systeembeheerder.", "ERROR_CLIENT_203" : "Er is een fout opgetreden bij de externe bureaublad server en heeft de verbinding gesloten. Probeer het alstublieft opnieuw of neem contact op met uw systeembeheerder.", "ERROR_CLIENT_205" : "Deze verbinding is beeindigd omdat het conflicteerd met een andere verbinding. Probeer het alstublieft later nog eens.", "ERROR_CLIENT_301" : "Inloggen is mislukt. Verbind opnieuw en probeer het nog eens.", "ERROR_CLIENT_303" : "U heeft geen toestemming om deze verbinding tot stand te brengen. Heeft u toegang nodig, vraag dan aan uw systeembeheerder om u toe te voegen aan de lijst van geauthoriseerde gebruikers of controleer uw systeem instellingen.", "ERROR_CLIENT_308" : "De Guacamole server heeft de sessie gesloten omdat uw browser zo lang niet heeft gereageerd dat het er op leek dat uw verbinding verbroken was. Dit komt in de regel door netwerk problemen zoals een slecht draadloos signaal of een erg langzame netwerk snelheid. Controleer uw netwerk en probeer het opnieuw.", "ERROR_CLIENT_31D" : "De Guacamole server geeft geen toegang tot deze verbinding omdat u de limiet heeft bereikt van het aantal toegestane gelijktijdige verbindingen door een individuele gebruiker. Sluit u alstublieft een of meer verbindingen en probeer het opnieuw.", "ERROR_CLIENT_DEFAULT" : "Een interne fout is opgetreden op de Guacamole server en de connectie is beeindigd. Als dit probleem zich voor blijft doen, neem dan contact op met uw systeembeheerder of controleer uw systeem logs.", "ERROR_TUNNEL_201" : "De Guacamole server heeft deze verbinding geweigerd omdat er al te veel actieve verbindingen zijn. Wacht u alstublieft een paar minuten en probeer het opnieuw.", "ERROR_TUNNEL_202" : "De verbinding is gesloten omdat de server niet op tijd reageerd. Dit komt in de regel door netwerk problemen zoals een slecht draadloos signaal of langzame netwerk snelheden. Controleer alstublieft uw netwerk verbinding en probeer het opnieuw of neem contact op met uw systeembeheerde.", "ERROR_TUNNEL_203" : "Er heeft een fout plaats gevonden op de server en deze heeft de verbinding gesloten. Probeert u het nog eens of neem contact op met uw systeembeheerder.", "ERROR_TUNNEL_204" : "De gevraagde verbinding bestaat niet. Controleert u altublieft de verbindingsnaam en probeer het nog eens.", "ERROR_TUNNEL_205" : "Deze verbinding is op dit moment in gebruik en gelijktijdige toegang is niet toegestaan. Probeert u het later nog eens.", "ERROR_TUNNEL_301" : "U heeft geen toestemming deze verbinding te gebruiken omdat u niet ingelogd bent. Log eerst in en probeer het nog eens.", "ERROR_TUNNEL_303" : "U heeft geen toestemming deze verbinding te gebruiken. Als u toegang nodig heeft, vraag dan aan de systeembeheerder om u toe te voegen aan de lijst van geauthoriseerde gebruikers of controleer uw systeem instellingen.", "ERROR_TUNNEL_308" : "De Guacamole server heeft de sessie gesloten omdat uw browser zo lang niet heeft gereageerd dat het er op leek dat uw verbinding verbroken was. Dit komt in de regel door netwerk problemen zoals een slecht draadloos signaal of een erg langzame netwerk snelheid. Controleer uw netwerk en probeer het opnieuw.", "ERROR_TUNNEL_31D" : "De Guacamole server geeft geen toegang tot deze verbinding omdat u de limiet heeft bereikt van het aantal toegestane gelijktijdige verbindingen door een individuele gebruiker. Sluit u alstublieft een of meer verbindingen en probeer het opnieuw.", "ERROR_TUNNEL_DEFAULT" : "Een interne fout is opgetreden op de Guacamole server en de connectie is beeindigd. Als dit probleem zich voor blijft doen, neem dan contact op met uw systeembeheerder of controleer uw systeem logs.", "ERROR_UPLOAD_100" : "Bestandsoverdracht is ofwel niet ondersteund of niet ingeschakeld. Neem contact op met uw systeembeheerder of kijk in uw systeem logs.", "ERROR_UPLOAD_201" : "Er worden momenteel te veel bestanden overdragen. Gelieve te wachten tot de bestaande bestandsoverdracht is voltooid, en probeer het opnieuw.", "ERROR_UPLOAD_202" : "Het bestand kan niet worden overgedragen, omdat de extern bureaublad server te lang niet reageert. Probeer het opnieuw of neem contact op met uw systeembeheerder.", "ERROR_UPLOAD_203" : "Er is een fout opgetreden op de extern bureaublad server tijdens de overdracht. Probeer het opnieuw of neem contact op met uw systeembeheerder.", "ERROR_UPLOAD_204" : "De bestemming voor de overdracht van bestanden bestaat niet. Controleer of de bestemming bestaat en probeer het opnieuw.", "ERROR_UPLOAD_205" : "De bestemming voor de overdracht van bestanden is momenteel vergrendeld. Gelieve te wachten tot alle taken zijn voltooid en probeer het opnieuw.", "ERROR_UPLOAD_301" : "U heeft geen toestemming om dit bestand te uploaden, omdat u niet ingelogd bent. Gelieve in te loggen en probeer het opnieuw.", "ERROR_UPLOAD_303" : "U heeft geen toestemming om dit bestand te uploaden. Als u toegang nodig heeft, controleer dan uw systeeminstellingen of neem contact op met uw systeembeheerder.", "ERROR_UPLOAD_308" : "De bestandsoverdracht is vastgelopen. Dit wordt meestal veroorzaakt door netwerkproblemen, zoals een instabiel draadloos signaal of gewoon een erg trage netwerkverbinding. Controleer uw netwerk en probeer het opnieuw.", "ERROR_UPLOAD_31D" : "Er worden momenteel te veel bestanden overdragen. Gelieve te wachten tot de bestaande bestandsoverdracht is voltooid, en probeer het opnieuw.", "ERROR_UPLOAD_DEFAULT" : "Er is een interne fout opgetreden op de Guacamole server, en de verbinding is beëindigd. Als het probleem aanhoudt, neem dan contact op met uw systeembeheerder of kijk in uw systeem logs.", "HELP_CLIPBOARD" : "Tekst gekopieerd / geknipt binnen Guacamole zal hier verschijnen. Wijzigingen in onderstaande tekst zal externe klembord beïnvloeden.", "HELP_INPUT_METHOD_NONE" : "Geen invoer methode gebruiken. Toetsenbord invoer wordt geaccepteerd van een aangesloten, fysiek toetsenbord.", "HELP_INPUT_METHOD_OSK" : "Weergave en accepteren van invoer via het ingebouwde Guacamole on-screen toetsenbord. Dit toetsenbord op het scherm maakt toetscombinaties mogelijk die anders onmogelijk zijn (zoals Ctrl-Alt-Del).", "HELP_INPUT_METHOD_TEXT" : "Laat het typen van tekst en het emuleren toetsenbord gebeurtenissen toe gebaseerd op de getypte tekst. Dit is nodig voor apparaten zoals mobiele telefoons die geen fysiek toetsenbord hebben.", "HELP_MOUSE_MODE" : "Bepaalt hoe de muis met aanraak-klikken omgaat.", "HELP_MOUSE_MODE_ABSOLUTE" : "Tik om te klikken. De klik vindt plaats op de locatie van de tik.", "HELP_MOUSE_MODE_RELATIVE" : "Sleep om de aanwijzer te bewegen en tik om te klikken. De klik vindt plaats op de locatie van de aanwijzer.", "INFO_NO_FILE_TRANSFERS" : "Geen bestandsoverdrachten.", "NAME_INPUT_METHOD_NONE" : "Geen", "NAME_INPUT_METHOD_OSK" : "Scherm toetsenbord", "NAME_INPUT_METHOD_TEXT" : "Text invoer", "NAME_KEY_CTRL" : "Ctrl", "NAME_KEY_ALT" : "Alt", "NAME_KEY_ESC" : "Esc", "NAME_KEY_TAB" : "Tab", "NAME_MOUSE_MODE_ABSOLUTE" : "Aanraakscherm", "NAME_MOUSE_MODE_RELATIVE" : "Touchpad", "SECTION_HEADER_CLIPBOARD" : "Klembord", "SECTION_HEADER_DEVICES" : "Apparaten", "SECTION_HEADER_DISPLAY" : "Scherm", "SECTION_HEADER_FILE_TRANSFERS" : "Bestandsoverdrachten", "SECTION_HEADER_INPUT_METHOD" : "Invoer methode", "SECTION_HEADER_MOUSE_MODE" : "Muis emulatie modus", "TEXT_ZOOM_AUTO_FIT" : "Automatisch aan browser venster aanpassen", "TEXT_CLIENT_STATUS_IDLE" : "Inactief.", "TEXT_CLIENT_STATUS_CONNECTING" : "Verbinden met Guacamole...", "TEXT_CLIENT_STATUS_DISCONNECTED" : "De verbinding is verbroken.", "TEXT_CLIENT_STATUS_WAITING" : "Verbonden met Guacamole. Aan het wachten op reactie...", "TEXT_RECONNECT_COUNTDOWN" : "opnieuw verbinden over {REMAINING} {REMAINING, plural, one{seconde} other{seconden}}...", "TEXT_FILE_TRANSFER_PROGRESS" : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}", "URL_OSK_LAYOUT" : "layouts/en-us-qwerty.json" }, "DATA_SOURCE_DEFAULT" : { "NAME" : "Standaard (XML)" }, "FORM" : { "FIELD_PLACEHOLDER_DATE" : "YYYY-MM-DD", "FIELD_PLACEHOLDER_TIME" : "HH:MM:SS", "HELP_SHOW_PASSWORD" : "Klik om wachtwoord te tonen", "HELP_HIDE_PASSWORD" : "Klik om wachtwoord te verbergen" }, "HOME" : { "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", "INFO_NO_RECENT_CONNECTIONS" : "Geen recente verbindingen.", "PASSWORD_CHANGED" : "Wachtwoord gewijzigd.", "SECTION_HEADER_ALL_CONNECTIONS" : "Alle Verbindingen", "SECTION_HEADER_RECENT_CONNECTIONS" : "Recente Verbindingen" }, "LOGIN": { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "ERROR_INVALID_LOGIN" : "Ongeldige Login", "FIELD_HEADER_USERNAME" : "Gebruikersnaam", "FIELD_HEADER_PASSWORD" : "Wachtwoord" }, "MANAGE_CONNECTION" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_CLONE" : "@:APP.ACTION_CLONE", "ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_SAVE" : "@:APP.ACTION_SAVE", "DIALOG_HEADER_CONFIRM_DELETE" : "Verwijder Verbinding", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_HEADER_LOCATION" : "Locatie:", "FIELD_HEADER_NAME" : "Naam:", "FIELD_HEADER_PROTOCOL" : "Protocol:", "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "Nu Actief", "INFO_CONNECTION_NOT_USED" : "Deze verbinding is nog niet gebruikt.", "SECTION_HEADER_EDIT_CONNECTION" : "Bewerk Verbinding", "SECTION_HEADER_HISTORY" : "Gebruikgeschiedenis", "SECTION_HEADER_PARAMETERS" : "Parameters", "TABLE_HEADER_HISTORY_USERNAME" : "Gebruikersnaam", "TABLE_HEADER_HISTORY_START" : "Starttijd", "TABLE_HEADER_HISTORY_DURATION" : "Tijdsduur", "TEXT_CONFIRM_DELETE" : "Verbindingen kunnen niet worden hersteld nadat ze zijn verwijderd. Weet u zeker dat u deze verbinding wilt verwijderen?", "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" }, "MANAGE_CONNECTION_GROUP" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_SAVE" : "@:APP.ACTION_SAVE", "DIALOG_HEADER_CONFIRM_DELETE" : "Verwijder Verbindingsgroep", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_HEADER_LOCATION" : "Locatie:", "FIELD_HEADER_NAME" : "Naam:", "FIELD_HEADER_TYPE" : "Soort:", "NAME_TYPE_BALANCING" : "Verdeling", "NAME_TYPE_ORGANIZATIONAL" : "Organizatorisch", "SECTION_HEADER_EDIT_CONNECTION_GROUP" : "Bewerk Verbindingsgroep", "TEXT_CONFIRM_DELETE" : "Verbindingsgroepen kunnen niet worden hersteld nadat ze zijn verwijderd. Weet u zeker dat u deze verbindingsgroep wilt verwijderen?" }, "MANAGE_USER" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_SAVE" : "@:APP.ACTION_SAVE", "DIALOG_HEADER_CONFIRM_DELETE" : "Verwijder Gebruiker", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", "FIELD_HEADER_ADMINISTER_SYSTEM" : "Systeem Beheer:", "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "Wijzigen eigen wachtwoord:", "FIELD_HEADER_CREATE_NEW_USERS" : "Nieuwe gebruikers aanmaken:", "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Nieuwe verbindingen aanmaken:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Nieuwe verbindingsgroepen aanmaken:", "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", "FIELD_HEADER_USERNAME" : "Gebruikersnaam:", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "INFO_READ_ONLY" : "Sorry, maar dit gebruikers account kan niet gewijzigd worden", "SECTION_HEADER_CONNECTIONS" : "Verbindingen", "SECTION_HEADER_EDIT_USER" : "Bewerk Gebruiker", "SECTION_HEADER_PERMISSIONS" : "Rechten", "TEXT_CONFIRM_DELETE" : "Gebruikers kunnen niet worden hersteld nadat ze zijn verwijderd. Weet u zeker dat u deze gebruiker wilt verwijderen?" }, "PROTOCOL_RDP" : { "FIELD_HEADER_CLIENT_NAME" : "Client naam:", "FIELD_HEADER_COLOR_DEPTH" : "Kleurdiepte:", "FIELD_HEADER_CONSOLE" : "Administrator console:", "FIELD_HEADER_CONSOLE_AUDIO" : "Support audio in console:", "FIELD_HEADER_CREATE_DRIVE_PATH" : "Automatisch genereren station:", "FIELD_HEADER_DISABLE_AUDIO" : "Uitschakelen geluid:", "FIELD_HEADER_DISABLE_AUTH" : "Uitschakelen authenticatie:", "FIELD_HEADER_DOMAIN" : "Domein:", "FIELD_HEADER_DPI" : "Resolutie (DPI):", "FIELD_HEADER_DRIVE_PATH" : "Station map:", "FIELD_HEADER_ENABLE_DESKTOP_COMPOSITION" : "Inschakelen bureaublad compositie (Aero):", "FIELD_HEADER_ENABLE_DRIVE" : "Inschakelen station:", "FIELD_HEADER_ENABLE_FONT_SMOOTHING" : "Inschakelen font smoothing (ClearType):", "FIELD_HEADER_ENABLE_FULL_WINDOW_DRAG" : "Inschakelen verslepen compleet venster:", "FIELD_HEADER_ENABLE_MENU_ANIMATIONS" : "Inschakelen menu animaties:", "FIELD_HEADER_ENABLE_PRINTING" : "Printen mogelijk maken:", "FIELD_HEADER_ENABLE_SFTP" : "Inschakelen SFTP:", "FIELD_HEADER_ENABLE_THEMING" : "Inschakelen thema's:", "FIELD_HEADER_ENABLE_WALLPAPER" : "Inschakelen achtergrond:", "FIELD_HEADER_HEIGHT" : "Hoogte:", "FIELD_HEADER_HOSTNAME" : "Servernaam:", "FIELD_HEADER_IGNORE_CERT" : "Negeer server certificaat:", "FIELD_HEADER_INITIAL_PROGRAM" : "Eerste programma:", "FIELD_HEADER_PASSWORD" : "Wachtwoord:", "FIELD_HEADER_PORT" : "Poort:", "FIELD_HEADER_REMOTE_APP_ARGS" : "Parameters:", "FIELD_HEADER_REMOTE_APP_DIR" : "Werk map:", "FIELD_HEADER_REMOTE_APP" : "Programma:", "FIELD_HEADER_SECURITY" : "Beveiligings modus:", "FIELD_HEADER_SERVER_LAYOUT" : "Toetsenbord lay-out:", "FIELD_HEADER_SFTP_DIRECTORY" : "Standaard upload map:", "FIELD_HEADER_SFTP_HOSTNAME" : "Servernaam:", "FIELD_HEADER_SFTP_PASSPHRASE" : "Wachtwoordzin:", "FIELD_HEADER_SFTP_PASSWORD" : "Wachtwoord:", "FIELD_HEADER_SFTP_PORT" : "Poort:", "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Persoonlijke sleutel:", "FIELD_HEADER_SFTP_USERNAME" : "Gebruikersnaam:", "FIELD_HEADER_STATIC_CHANNELS" : "Vaste kanaalnamen:", "FIELD_HEADER_USERNAME" : "Gebruikersnaam:", "FIELD_HEADER_WIDTH" : "Breedte:", "FIELD_OPTION_COLOR_DEPTH_16" : "Minder Kleuren (16-bit)", "FIELD_OPTION_COLOR_DEPTH_24" : "Echte Kleuren (24-bit)", "FIELD_OPTION_COLOR_DEPTH_32" : "Echte Kleuren (32-bit)", "FIELD_OPTION_COLOR_DEPTH_8" : "256 kleuren", "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "", "FIELD_OPTION_SECURITY_ANY" : "Ieder", "FIELD_OPTION_SECURITY_EMPTY" : "", "FIELD_OPTION_SECURITY_NLA" : "NLA (Network Level Authentication)", "FIELD_OPTION_SECURITY_RDP" : "RDP encryptie", "FIELD_OPTION_SECURITY_TLS" : "TLS encryptie", "FIELD_OPTION_SERVER_LAYOUT_DE_DE_QWERTZ" : "Duits (Qwertz)", "FIELD_OPTION_SERVER_LAYOUT_EMPTY" : "", "FIELD_OPTION_SERVER_LAYOUT_EN_US_QWERTY" : "Amerikaans Engels (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_FAILSAFE" : "Unicode", "FIELD_OPTION_SERVER_LAYOUT_FR_FR_AZERTY" : "Frans (Azerty)", "FIELD_OPTION_SERVER_LAYOUT_IT_IT_QWERTY" : "Italiaans (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_SV_SE_QWERTY" : "Zweeds (Qwerty)", "NAME" : "RDP", "SECTION_HEADER_AUTHENTICATION" : "Authenticatie", "SECTION_HEADER_BASIC_PARAMETERS" : "Basis Instellingen", "SECTION_HEADER_DEVICE_REDIRECTION" : "Apparaat Verbindingen", "SECTION_HEADER_DISPLAY" : "Scherm", "SECTION_HEADER_NETWORK" : "Netwerk", "SECTION_HEADER_PERFORMANCE" : "Prestatie instellingen", "SECTION_HEADER_REMOTEAPP" : "ExterneApp", "SECTION_HEADER_SFTP" : "SFTP" }, "PROTOCOL_SSH" : { "FIELD_HEADER_COLOR_SCHEME" : "Kleuren combinatie:", "FIELD_HEADER_COMMAND" : "Uitvoeren opdracht:", "FIELD_HEADER_FONT_NAME" : "Fontnaam:", "FIELD_HEADER_FONT_SIZE" : "Fontgrootte:", "FIELD_HEADER_ENABLE_SFTP" : "SFTP mogelijk maken:", "FIELD_HEADER_HOSTNAME" : "Servernaam:", "FIELD_HEADER_USERNAME" : "Gebruikersnaam:", "FIELD_HEADER_PASSWORD" : "Wachtwoord:", "FIELD_HEADER_PASSPHRASE" : "Wachtwoordzin:", "FIELD_HEADER_PORT" : "Poort:", "FIELD_HEADER_PRIVATE_KEY" : "Persoonlijke sleutel:", "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "Zwart op wit", "FIELD_OPTION_COLOR_SCHEME_EMPTY" : "", "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK" : "Grijs op zwart", "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "Groen op zwart", "FIELD_OPTION_COLOR_SCHEME_WHITE_BLACK" : "Wit op zwart", "FIELD_OPTION_FONT_SIZE_8" : "8", "FIELD_OPTION_FONT_SIZE_9" : "9", "FIELD_OPTION_FONT_SIZE_10" : "10", "FIELD_OPTION_FONT_SIZE_11" : "11", "FIELD_OPTION_FONT_SIZE_12" : "12", "FIELD_OPTION_FONT_SIZE_14" : "14", "FIELD_OPTION_FONT_SIZE_18" : "18", "FIELD_OPTION_FONT_SIZE_24" : "24", "FIELD_OPTION_FONT_SIZE_30" : "30", "FIELD_OPTION_FONT_SIZE_36" : "36", "FIELD_OPTION_FONT_SIZE_48" : "48", "FIELD_OPTION_FONT_SIZE_60" : "60", "FIELD_OPTION_FONT_SIZE_72" : "72", "FIELD_OPTION_FONT_SIZE_96" : "96", "FIELD_OPTION_FONT_SIZE_EMPTY" : "", "NAME" : "SSH", "SECTION_HEADER_AUTHENTICATION" : "Authenticatie", "SECTION_HEADER_DISPLAY" : "Scherm", "SECTION_HEADER_NETWORK" : "Netwerk", "SECTION_HEADER_SESSION" : "Sessie / Omgeving", "SECTION_HEADER_SFTP" : "SFTP" }, "PROTOCOL_TELNET" : { "FIELD_HEADER_COLOR_SCHEME" : "Kleuren combinatie:", "FIELD_HEADER_FONT_NAME" : "Fontnaam:", "FIELD_HEADER_FONT_SIZE" : "Fontgrootte:", "FIELD_HEADER_HOSTNAME" : "Servernaam:", "FIELD_HEADER_USERNAME" : "Gebruikersnaam:", "FIELD_HEADER_PASSWORD" : "Wachtwoord:", "FIELD_HEADER_PASSWORD_REGEX" : "Wachtwoord reguliere expressie:", "FIELD_HEADER_PORT" : "Poort:", "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "Zwart op wit", "FIELD_OPTION_COLOR_SCHEME_EMPTY" : "", "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK" : "Grijs op zwart", "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "Groen op zwart", "FIELD_OPTION_COLOR_SCHEME_WHITE_BLACK" : "Wit op zwart", "FIELD_OPTION_FONT_SIZE_8" : "8", "FIELD_OPTION_FONT_SIZE_9" : "9", "FIELD_OPTION_FONT_SIZE_10" : "10", "FIELD_OPTION_FONT_SIZE_11" : "11", "FIELD_OPTION_FONT_SIZE_12" : "12", "FIELD_OPTION_FONT_SIZE_14" : "14", "FIELD_OPTION_FONT_SIZE_18" : "18", "FIELD_OPTION_FONT_SIZE_24" : "24", "FIELD_OPTION_FONT_SIZE_30" : "30", "FIELD_OPTION_FONT_SIZE_36" : "36", "FIELD_OPTION_FONT_SIZE_48" : "48", "FIELD_OPTION_FONT_SIZE_60" : "60", "FIELD_OPTION_FONT_SIZE_72" : "72", "FIELD_OPTION_FONT_SIZE_96" : "96", "FIELD_OPTION_FONT_SIZE_EMPTY" : "", "NAME" : "Telnet", "SECTION_HEADER_AUTHENTICATION" : "Authenticatie", "SECTION_HEADER_DISPLAY" : "Scherm", "SECTION_HEADER_NETWORK" : "Netwerk" }, "PROTOCOL_VNC" : { "FIELD_HEADER_AUDIO_SERVERNAME" : "Audio server naam:", "FIELD_HEADER_CLIPBOARD_ENCODING" : "Encodering:", "FIELD_HEADER_COLOR_DEPTH" : "Kleurdiepte:", "FIELD_HEADER_CURSOR" : "Cursor:", "FIELD_HEADER_DEST_HOST" : "Externe server:", "FIELD_HEADER_DEST_PORT" : "Externe poort:", "FIELD_HEADER_ENABLE_AUDIO" : "Inschakelen geluid:", "FIELD_HEADER_ENABLE_SFTP" : "Inschakelen SFTP:", "FIELD_HEADER_HOSTNAME" : "Servernaam:", "FIELD_HEADER_PASSWORD" : "Wachtwoord:", "FIELD_HEADER_PORT" : "Poort:", "FIELD_HEADER_READ_ONLY" : "Alleen lezen:", "FIELD_HEADER_SFTP_DIRECTORY" : "Standaard upload map:", "FIELD_HEADER_SFTP_HOSTNAME" : "Servernaam:", "FIELD_HEADER_SFTP_PASSPHRASE" : "Wachtwoordzin:", "FIELD_HEADER_SFTP_PASSWORD" : "Wachtwoord:", "FIELD_HEADER_SFTP_PORT" : "Poort:", "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Persoonlijke sleutel:", "FIELD_HEADER_SFTP_USERNAME" : "Gebruikersnaam:", "FIELD_HEADER_SWAP_RED_BLUE" : "Verwissel rood/blauw componenten:", "FIELD_OPTION_COLOR_DEPTH_8" : "256 kleuren", "FIELD_OPTION_COLOR_DEPTH_16" : "Minder kleuren (16-bit)", "FIELD_OPTION_COLOR_DEPTH_24" : "Echte kleuren (24-bit)", "FIELD_OPTION_COLOR_DEPTH_32" : "Echte kleuren (32-bit)", "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "", "FIELD_OPTION_CURSOR_EMPTY" : "", "FIELD_OPTION_CURSOR_LOCAL" : "Lokaal", "FIELD_OPTION_CURSOR_REMOTE" : "Extern", "FIELD_OPTION_CLIPBOARD_ENCODING_CP1252" : "CP1252", "FIELD_OPTION_CLIPBOARD_ENCODING_EMPTY" : "", "FIELD_OPTION_CLIPBOARD_ENCODING_ISO8859_1" : "ISO 8859-1", "FIELD_OPTION_CLIPBOARD_ENCODING_UTF_8" : "UTF-8", "FIELD_OPTION_CLIPBOARD_ENCODING_UTF_16" : "UTF-16", "NAME" : "VNC", "SECTION_HEADER_AUDIO" : "Geluid", "SECTION_HEADER_AUTHENTICATION" : "Authenticatie", "SECTION_HEADER_CLIPBOARD" : "Klembord", "SECTION_HEADER_DISPLAY" : "Scherm", "SECTION_HEADER_NETWORK" : "Netwerk", "SECTION_HEADER_REPEATER" : "VNC Repeater", "SECTION_HEADER_SFTP" : "SFTP" }, "SETTINGS" : { "SECTION_HEADER_SETTINGS" : "Instellingen" }, "SETTINGS_CONNECTION_HISTORY" : { "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", "HELP_CONNECTION_HISTORY" : "De gebruikgeschiedenis van verbindingen wordt hier onder getoond en kan gesorteerd worden door op de titel van de kolom te klikken. Voer een zoekterm in en klik op \"Zoeken\", om op specifieke resultaten te zoeken. Alleen de resultaten die voldoen aan de zoekterm zullen dan getoond worden.", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_NO_HISTORY" : "Geen resultaten gevonden", "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Verbindingsnaam", "TABLE_HEADER_SESSION_DURATION" : "Tijdsduur", "TABLE_HEADER_SESSION_STARTDATE" : "Starttijd", "TABLE_HEADER_SESSION_USERNAME" : "Gebruikersnaam", "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" }, "SETTINGS_CONNECTIONS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_NEW_CONNECTION" : "Nieuwe Verbinding", "ACTION_NEW_CONNECTION_GROUP" : "Nieuwe Verbindingsgroep", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "HELP_CONNECTIONS" : "Klik of tik op een verbinding hieronder om die verbinding te beheren. Afhankelijk van uw toegangsniveau kunnen verbindingen worden toegevoegd en verwijderd en hun eigenschappen (protocol, hostname, port, etc.) worden gewijzigd. ", "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", "SECTION_HEADER_CONNECTIONS" : "Verbindingen" }, "SETTINGS_PREFERENCES" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", "FIELD_HEADER_LANGUAGE" : "Taal Keuze:", "FIELD_HEADER_PASSWORD" : "Wachtwoord:", "FIELD_HEADER_PASSWORD_OLD" : "Huidig Wachtwoord:", "FIELD_HEADER_PASSWORD_NEW" : "Nieuw Wachtwoord:", "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "Bevestig Nieuw Wachtwoord:", "FIELD_HEADER_USERNAME" : "Gebruikersnaam:", "HELP_DEFAULT_INPUT_METHOD" : "De standaard invoer methode bepaalt hoe toetsenbord gebeurtenissen ontvangen worden door Guacamole. Het veranderen van deze instelling kan nodig zijn bij gebruik van een mobiel apparaat of wanneer er via een IME getypt wordt. Deze instelling kan per verbinding worden overschreven via het Guacamole menu.", "HELP_DEFAULT_MOUSE_MODE" : "De standaard muis emulatie modus bepaalt hoe de externe muis in nieuwe verbindingen omgaat met touch. Deze instelling kan per verbinding worden overschreven via het Guacamole menu.", "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", "HELP_LANGUAGE" : "Selecteer onderstaand een andere taal om alle text in Guacamole hieraan aan te passen. De beschikbare keuzes zijn afhankelijk van welke talen er geinstalleerd zijn.", "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", "HELP_UPDATE_PASSWORD" : "Als u uw wachtwoord wilt wijzigen, voer dan uw huidige wachtwoord en uw nieuwe wachtwoord hieronder in en klik op \"Wijzig Wachtwoord\". Deze wijziging zal meteen actief zijn.", "INFO_PASSWORD_CHANGED" : "Wachtwoord gewijzigd.", "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Standaard Invoer Methode", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Standaard Muis Emulatie Modus", "SECTION_HEADER_UPDATE_PASSWORD" : "Wijzig Wachtwoord" }, "SETTINGS_USERS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_NEW_USER" : "Nieuwe Gebruiker", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "HELP_USERS" : "Klik of tik op een van de onderstaande gebruikers om die te beheren. Afhankelijk van uw toegangsniveau kunnen gebruikers worden toegevoegd, verwijderd en hun wachtwoorden gewijzigd.", "SECTION_HEADER_USERS" : "Gebruikers" }, "SETTINGS_SESSIONS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_DELETE" : "Beeindig Sessies", "DIALOG_HEADER_CONFIRM_DELETE" : "Beeindig Sessie", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", "HELP_SESSIONS" : "Alle Guacamole sessies die op dit moment actief zijn worden hier getoond. Als u een of meerdere sessies wilt beeindigen, vink die sessie(s) dan aan en klik op \"Beeindig Sessies\". Door het verbreken van een sessie verliest de gebruiker ogenblikkelijk het contact met die sessie(s).", "INFO_NO_SESSIONS" : "Geen actieve sessies", "SECTION_HEADER_SESSIONS" : "Actieve Sessies", "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Verbindingnaam", "TABLE_HEADER_SESSION_REMOTEHOST" : "Servernaam", "TABLE_HEADER_SESSION_STARTDATE" : "Actief sinds", "TABLE_HEADER_SESSION_USERNAME" : "Gebruikersnaam", "TEXT_CONFIRM_DELETE" : "Weet u zeker dat u alle geselecteerde sessies wilt beeindigen? De gebruikers van deze sessies zullen ogenblikkelijk hun verbinding met deze sessies verliezen." }, "USER_MENU" : { "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY" } } guacamole-client-0.9.9/guacamole/src/main/webapp/translations/it.json0100664 0001751 0001751 00000073106 12645510255 027521 0ustar00mjumpermjumper0000000 0000000 { "NAME" : "Italiano", "APP" : { "ACTION_ACKNOWLEDGE" : "OK", "ACTION_CANCEL" : "Annulla", "ACTION_CLONE" : "Clona", "ACTION_CONTINUE" : "Continua", "ACTION_DELETE" : "Cancella", "ACTION_DELETE_SESSIONS" : "Termina Sessione", "ACTION_LOGIN" : "Entra", "ACTION_LOGOUT" : "Esci", "ACTION_MANAGE_CONNECTIONS" : "Connessioni", "ACTION_MANAGE_PREFERENCES" : "Preferenze", "ACTION_MANAGE_SETTINGS" : "Opzioni", "ACTION_MANAGE_SESSIONS" : "Sessioni Attive", "ACTION_MANAGE_USERS" : "Utenti", "ACTION_NAVIGATE_BACK" : "Indietro", "ACTION_NAVIGATE_HOME" : "Home", "ACTION_SAVE" : "Salva", "ACTION_UPDATE_PASSWORD" : "Aggiorna Password", "DIALOG_HEADER_ERROR" : "Errore", "ERROR_PASSWORD_BLANK" : "La password non può essere vuota.", "ERROR_PASSWORD_MISMATCH" : "Le password inserite sono diverse!", "FIELD_HEADER_PASSWORD" : "Password:", "FIELD_HEADER_PASSWORD_AGAIN" : "Re-inserisci la password:", "FIELD_PLACEHOLDER_FILTER" : "Filtro", "FORMAT_DATE_TIME_PRECISE" : "dd-MM-yyyy HH:mm:ss", "INFO_ACTIVE_USER_COUNT" : "Ora utilizzato da {USERS} {USERS, plural, one{user} other{users}}.", "NAME" : "Guacamole ${project.version}" }, "CLIENT" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Pulisci i trasferimenti completati", "ACTION_DISCONNECT" : "Disconnetti", "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", "ACTION_RECONNECT" : "Riconnetti", "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", "ACTION_UPLOAD_FILES" : "Carica un file", "DIALOG_HEADER_CONNECTING" : "Connessione in corso", "DIALOG_HEADER_CONNECTION_ERROR" : "Errore di Connessione", "DIALOG_HEADER_DISCONNECTED" : "Disconnesso", "ERROR_CLIENT_201" : "La connessione è stata chiusa perchè il server è sovraccarico. Attendi qualche minuto e riprova a collegarti.", "ERROR_CLIENT_202" : "Il server Guacamole ha chiuso la connessione perchè il desktop remoto risponde troppo lentamente. Per favore riprova o contatta il tuo amministratore di sistema.", "ERROR_CLIENT_203" : "Il desktop remoto ha riscontrato un errore ed ha chiuso la connessione. Per favore riprova o contatta il tuo amministratore di sistema.", "ERROR_CLIENT_205" : "Questa connessione è stata chiusa a causa di un conflitto con un'altra connessione. Riprova tra qualche minuto.", "ERROR_CLIENT_301" : "Autenticazione fallita. Prova ad effettaure nuovamente la connessione.", "ERROR_CLIENT_303" : "Non hai i permessi per accedere a questa connessione. Se ne hai bisogno, chiedi ai tuoi amministratori di sistema di aggiungerti agli utenti abilitati.", "ERROR_CLIENT_308" : "Il server Guacamole ha chiuso la connessione perchè il tuo browser non ha risposto per troppo tempo e sembrava essersi disconnesso. Solitamente la causa è un problema di rete, ad esempio un segnale wifi instabile o una connesisone molto lenta. Controlla la tua rete e riprova.", "ERROR_CLIENT_31D" : "Il server Guacamole rifiuta l'accesso alla connessione perchè hai raggiunto il limite massimo di connessioni simultanee per singolo utente. Chiudi una o più connessioni e riprova.", "ERROR_CLIENT_DEFAULT" : "Si è verificato un errore interno sul server Guacamole, e la connessione è stata terminata. Se il problema persiste avvisa il tuo amministratore di sistema o controlla i file di log.", "ERROR_TUNNEL_201" : "Il server Guacamole ha rifiutato questa connessione perchè ci sono troppe connessioni attive. Attendi qualche minuto e riprova.", "ERROR_TUNNEL_202" : "La connessione è stata chiusa dal server is taking too long to respond.Solitamente la causa è un problema di rete, ad esempio un segnale wifi instabile o una connesisone molto lenta. Controlla la tua rete e riprova.", "ERROR_TUNNEL_203" : "Si è verificato un errore sul server e la connessione è stata chiusa. Riprova o contatta il tuo amministratore di sistema.", "ERROR_TUNNEL_204" : "La connessione richiesta non esiste. Controlla il nome della connessione e riprova. Grazie.", "ERROR_TUNNEL_205" : "Questa connessione è già in uso, non sono possibili accessi concorrenti. Riprova più tardi. Grazie.", "ERROR_TUNNEL_301" : "Non hai i permessi per accedere a questa connessione perchè non hai effettuato il login. Inserisci nome utente e password e riprova.", "ERROR_TUNNEL_303" : "Non hai i permessi per accedere a questa connessione. Se ne hai bisogno, chiedi ai tuoi amministratori di sistema di aggiungerti agli utenti abilitati.", "ERROR_TUNNEL_308" : "Il server Guacamole ha chiuso la connessione perchè il tuo browser non ha risposto per troppo tempo e sembrava essersi disconnesso. Solitamente la causa è un problema di rete, ad esempio un segnale wifi instabile o una connesisone molto lenta. Controlla la tua rete e riprova.", "ERROR_TUNNEL_31D" : "Il server Guacamole rifiuta l'accesso alla connessione perchè hai raggiunto il limite massimo di connessioni simultanee per singolo utente. Chiudi una o più connessioni e riprova. Grazie.", "ERROR_TUNNEL_DEFAULT" : "Si è verificato un errore interno sul server Guacamole, e la connessione è stata terminata. Se il problema persiste avvisa il tuo amministratore di sistema o controlla i file di log.", "ERROR_UPLOAD_100" : "Il trasferimento di file non è supportato o non è attivo. Contatta il tuo amministratore di sistema.", "ERROR_UPLOAD_201" : "Ci sono troppi file in coda per il trasferimento. Attendi che siano completati i trasferimenti in atto e riprova.", "ERROR_UPLOAD_202" : "Trasferimento annullato: il desktop remoto risponde troppo lentamente. Per favore riprova o contatta il tuo amministratore di sistema.", "ERROR_UPLOAD_203" : "Il desktop remoto ha incontrato un errore durante il trasferimento. Per favore riprova o contatta il tuo amministratore di sistema.", "ERROR_UPLOAD_204" : "La destinazione del file non esiste. Controlla che la destinazione esista e riprova.", "ERROR_UPLOAD_205" : "La destinazione del file non è bloccata. Per favore attendi che ogni processo in corso sia termianto e riprova.", "ERROR_UPLOAD_301" : "Non hai i permessi per caricare i file perchè non hai fatto il login. Inserisci nome utente e password e riprova.", "ERROR_UPLOAD_303" : "Non hai i permessi per caricare i file. Se ti serve questa funzionalità contatta il tuo amministratore di sistema.", "ERROR_UPLOAD_308" : "Il trasferimento di file si è fermato. Solitamente la causa è un problema di rete, ad esempio un segnale wifi instabile o una connesisone molto lenta. Controlla la tua rete e riprova.", "ERROR_UPLOAD_31D" : "Ci sono troppi file in coda per il trasferimento. Attendi che siano completati i trasferimenti in atto e riprova.", "ERROR_UPLOAD_DEFAULT" : "Si è verificato un errore sul server e la connessione è stata chiusa. Riprova o contatta il tuo amministratore di sistema.", "HELP_CLIPBOARD" : "Il testo copiato/tagliato appare qui. I cambiamenti effettuati al testo qui sotto saranno riportati negli appunti remoti.", "HELP_INPUT_METHOD_NONE" : "Non c'è nessun metodo di immissione. L'input da tastiera è accettato da una tastiera fisica connessa.", "HELP_INPUT_METHOD_OSK" : "Mostra e accetta input dalla tastiera su schermo. La tastiera su schermo ti permette di scrivere combinazioni di tasti altrimenti mipossibli (ad esempio Ctrl-Alt-Canc).", "HELP_INPUT_METHOD_TEXT" : "Abilita la battitura di testo ed emula gli eventi da tastiera basati sul testo battuto. Questo è necessario per tablet e smartphone che non hanno una tastiera fisica.", "HELP_MOUSE_MODE" : "Determina come si deve comportare il mouse remoto in base al touch.", "HELP_MOUSE_MODE_ABSOLUTE" : "Tap to click. Il click è sostituito dal tocco.", "HELP_MOUSE_MODE_RELATIVE" : "Trascina il dito per muovere il puntatore del mouse e fai tap al posto del click. Il click sarà effettuato dove si trova il puntatore.", "INFO_NO_FILE_TRANSFERS" : "Nessun trasferimento di file.", "NAME_INPUT_METHOD_NONE" : "Nessuno", "NAME_INPUT_METHOD_OSK" : "Tastiera su schermo", "NAME_INPUT_METHOD_TEXT" : "Inserimento Testo", "NAME_KEY_CTRL" : "Ctrl", "NAME_KEY_ALT" : "Alt", "NAME_KEY_ESC" : "Esc", "NAME_KEY_TAB" : "Tab", "NAME_MOUSE_MODE_ABSOLUTE" : "Touchscreen", "NAME_MOUSE_MODE_RELATIVE" : "Touchpad", "SECTION_HEADER_CLIPBOARD" : "Appunti", "SECTION_HEADER_DEVICES" : "Dispositivi", "SECTION_HEADER_DISPLAY" : "Schermo", "SECTION_HEADER_FILE_TRANSFERS" : "Trasferimento file", "SECTION_HEADER_INPUT_METHOD" : "Metodo di input", "SECTION_HEADER_MOUSE_MODE" : "Modalità di emulazione del mouse", "TEXT_ZOOM_AUTO_FIT" : "Automatically fit to browser window", "TEXT_CLIENT_STATUS_IDLE" : "Inattivo.", "TEXT_CLIENT_STATUS_CONNECTING" : "Connessione in corso a Guacamole...", "TEXT_CLIENT_STATUS_DISCONNECTED" : "Sei stato disconnesso.", "TEXT_CLIENT_STATUS_WAITING" : "Connesso a Guacamole. Attendi una risposta...", "TEXT_RECONNECT_COUNTDOWN" : "Riconnessione in {REMAINING} {REMAINING, plural, one{second} other{seconds}}...", "TEXT_FILE_TRANSFER_PROGRESS" : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}", "URL_OSK_LAYOUT" : "layouts/it-it-qwerty.json" }, "FORM" : { "HELP_SHOW_PASSWORD" : "Click to show password", "HELP_HIDE_PASSWORD" : "Click to hide password" }, "HOME" : { "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", "INFO_NO_RECENT_CONNECTIONS" : "Nessuna connessione recente.", "PASSWORD_CHANGED" : "Password modificata.", "SECTION_HEADER_ALL_CONNECTIONS" : "Tutte le Connessioni", "SECTION_HEADER_RECENT_CONNECTIONS" : "Connessioni Recenti" }, "LOGIN": { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "ERROR_INVALID_LOGIN" : "Nome utente e/o password errati.", "FIELD_HEADER_USERNAME" : "Username", "FIELD_HEADER_PASSWORD" : "Password" }, "MANAGE_CONNECTION" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_CLONE" : "@:APP.ACTION_CLONE", "ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_SAVE" : "@:APP.ACTION_SAVE", "DIALOG_HEADER_CONFIRM_DELETE" : "Elimina connessione", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_HEADER_LOCATION" : "Location:", "FIELD_HEADER_NAME" : "Name:", "FIELD_HEADER_PROTOCOL" : "Protocol:", "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "Attiva adesso", "INFO_CONNECTION_NOT_USED" : "Questa connessione non è mai stata usata.", "SECTION_HEADER_EDIT_CONNECTION" : "Modifica Connessione", "SECTION_HEADER_HISTORY" : "Cronologia di utilizzo", "SECTION_HEADER_PARAMETERS" : "Parametri", "TABLE_HEADER_HISTORY_USERNAME" : "Username", "TABLE_HEADER_HISTORY_START" : "Start Time", "TABLE_HEADER_HISTORY_DURATION" : "Durata", "TEXT_CONFIRM_DELETE" : "Le Connessioni non possono essere ripristinate dopo la loro eliminazione. Sei sicuro di volere eliminare questa connessione?" }, "MANAGE_CONNECTION_GROUP" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_SAVE" : "@:APP.ACTION_SAVE", "DIALOG_HEADER_CONFIRM_DELETE" : "Elimina Gruppo di Connesisoni", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_HEADER_LOCATION" : "Location:", "FIELD_HEADER_NAME" : "Nome:", "FIELD_HEADER_TYPE" : "Tipo:", "NAME_TYPE_BALANCING" : "Bilanciamento", "NAME_TYPE_ORGANIZATIONAL" : "Organizzazione", "SECTION_HEADER_EDIT_CONNECTION_GROUP" : "Modifica Gruppo di Connessioni", "TEXT_CONFIRM_DELETE" : "Un Gruppo di Connessioni non può essere ripristinato dopo l'eliminazione. Sei sicuro di volere eliminare questa gruppo?" }, "MANAGE_USER" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_SAVE" : "@:APP.ACTION_SAVE", "DIALOG_HEADER_CONFIRM_DELETE" : "Elimina utente", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", "FIELD_HEADER_ADMINISTER_SYSTEM" : "Amministartore di sistema:", "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "Cambia la tua password:", "FIELD_HEADER_CREATE_NEW_USERS" : "Crea un utente:", "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Crea una connessione:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Crea un ruppo di connessioni:", "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", "FIELD_HEADER_USERNAME" : "Username:", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "SECTION_HEADER_CONNECTIONS" : "Connessioni", "SECTION_HEADER_EDIT_USER" : "Modifica Utente", "SECTION_HEADER_PERMISSIONS" : "Permessi", "TEXT_CONFIRM_DELETE" : "L'utente non può essere ripristinato dopo l'eliminazione. Sei sicuro di volere eliminare l'utente?" }, "PROTOCOL_RDP" : { "FIELD_HEADER_COLOR_DEPTH" : "Color depth:", "FIELD_HEADER_CONSOLE" : "Administrator console:", "FIELD_HEADER_CONSOLE_AUDIO" : "Support audio in console:", "FIELD_HEADER_CLIENT_NAME" : "Client name:", "FIELD_HEADER_DISABLE_AUDIO" : "Disable audio:", "FIELD_HEADER_DISABLE_AUTH" : "Disable authentication:", "FIELD_HEADER_DOMAIN" : "Domain:", "FIELD_HEADER_DPI" : "Resolution (DPI):", "FIELD_HEADER_DRIVE_PATH" : "Drive path:", "FIELD_HEADER_ENABLE_DESKTOP_COMPOSITION" : "Enable desktop composition (Aero):", "FIELD_HEADER_ENABLE_DRIVE" : "Enable drive:", "FIELD_HEADER_ENABLE_FONT_SMOOTHING" : "Enable font smoothing (ClearType):", "FIELD_HEADER_ENABLE_FULL_WINDOW_DRAG" : "Enable full-window drag:", "FIELD_HEADER_ENABLE_MENU_ANIMATIONS" : "Enable menu animations:", "FIELD_HEADER_ENABLE_PRINTING" : "Enable printing:", "FIELD_HEADER_ENABLE_SFTP" : "Enable SFTP:", "FIELD_HEADER_ENABLE_THEMING" : "Enable theming:", "FIELD_HEADER_ENABLE_WALLPAPER" : "Enable wallpaper:", "FIELD_HEADER_HEIGHT" : "Height:", "FIELD_HEADER_HOSTNAME" : "Hostname:", "FIELD_HEADER_IGNORE_CERT" : "Ignore server certificate:", "FIELD_HEADER_INITIAL_PROGRAM" : "Initial program:", "FIELD_HEADER_PASSWORD" : "Password:", "FIELD_HEADER_PORT" : "Port:", "FIELD_HEADER_REMOTE_APP_ARGS" : "Parameters:", "FIELD_HEADER_REMOTE_APP_DIR" : "Working directory:", "FIELD_HEADER_REMOTE_APP" : "Program:", "FIELD_HEADER_SECURITY" : "Security mode:", "FIELD_HEADER_SERVER_LAYOUT" : "Keyboard layout:", "FIELD_HEADER_SFTP_HOSTNAME" : "Hostname:", "FIELD_HEADER_SFTP_PASSPHRASE" : "Passphrase:", "FIELD_HEADER_SFTP_PASSWORD" : "Password:", "FIELD_HEADER_SFTP_PORT" : "Port:", "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Private key:", "FIELD_HEADER_SFTP_USERNAME" : "Username:", "FIELD_HEADER_STATIC_CHANNELS" : "Static channel names:", "FIELD_HEADER_USERNAME" : "Username:", "FIELD_HEADER_WIDTH" : "Width:", "FIELD_OPTION_COLOR_DEPTH_16" : "Low color (16-bit)", "FIELD_OPTION_COLOR_DEPTH_24" : "True color (24-bit)", "FIELD_OPTION_COLOR_DEPTH_32" : "True color (32-bit)", "FIELD_OPTION_COLOR_DEPTH_8" : "256 color", "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "", "FIELD_OPTION_SECURITY_ANY" : "Any", "FIELD_OPTION_SECURITY_EMPTY" : "", "FIELD_OPTION_SECURITY_NLA" : "NLA (Network Level Authentication)", "FIELD_OPTION_SECURITY_RDP" : "RDP encryption", "FIELD_OPTION_SECURITY_TLS" : "TLS encryption", "FIELD_OPTION_SERVER_LAYOUT_DE_DE_QWERTZ" : "German (Qwertz)", "FIELD_OPTION_SERVER_LAYOUT_EMPTY" : "", "FIELD_OPTION_SERVER_LAYOUT_EN_US_QWERTY" : "US English (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_FAILSAFE" : "Unicode", "FIELD_OPTION_SERVER_LAYOUT_FR_FR_AZERTY" : "French (Azerty)", "FIELD_OPTION_SERVER_LAYOUT_IT_IT_QWERTY" : "Italian (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_SV_SE_QWERTY" : "Swedish (Qwerty)", "NAME" : "RDP", "SECTION_HEADER_AUTHENTICATION" : "Authentication", "SECTION_HEADER_BASIC_PARAMETERS" : "Basic Settings", "SECTION_HEADER_DEVICE_REDIRECTION" : "Device Redirection", "SECTION_HEADER_DISPLAY" : "Display", "SECTION_HEADER_NETWORK" : "Network", "SECTION_HEADER_PERFORMANCE" : "Performance", "SECTION_HEADER_REMOTEAPP" : "RemoteApp", "SECTION_HEADER_SFTP" : "SFTP" }, "PROTOCOL_SSH" : { "FIELD_HEADER_FONT_NAME" : "Font name:", "FIELD_HEADER_FONT_SIZE" : "Font size:", "FIELD_HEADER_ENABLE_SFTP" : "Enable SFTP:", "FIELD_HEADER_HOSTNAME" : "Hostname:", "FIELD_HEADER_USERNAME" : "Username:", "FIELD_HEADER_PASSWORD" : "Password:", "FIELD_HEADER_PASSPHRASE" : "Passphrase:", "FIELD_HEADER_PORT" : "Port:", "FIELD_HEADER_PRIVATE_KEY" : "Private key:", "FIELD_OPTION_FONT_SIZE_8" : "8", "FIELD_OPTION_FONT_SIZE_9" : "9", "FIELD_OPTION_FONT_SIZE_10" : "10", "FIELD_OPTION_FONT_SIZE_11" : "11", "FIELD_OPTION_FONT_SIZE_12" : "12", "FIELD_OPTION_FONT_SIZE_14" : "14", "FIELD_OPTION_FONT_SIZE_18" : "18", "FIELD_OPTION_FONT_SIZE_24" : "24", "FIELD_OPTION_FONT_SIZE_30" : "30", "FIELD_OPTION_FONT_SIZE_36" : "36", "FIELD_OPTION_FONT_SIZE_48" : "48", "FIELD_OPTION_FONT_SIZE_60" : "60", "FIELD_OPTION_FONT_SIZE_72" : "72", "FIELD_OPTION_FONT_SIZE_96" : "96", "FIELD_OPTION_FONT_SIZE_EMPTY" : "", "NAME" : "SSH", "SECTION_HEADER_AUTHENTICATION" : "Authentication", "SECTION_HEADER_DISPLAY" : "Display", "SECTION_HEADER_NETWORK" : "Network", "SECTION_HEADER_SFTP" : "SFTP" }, "PROTOCOL_TELNET" : { "FIELD_HEADER_FONT_NAME" : "Font name:", "FIELD_HEADER_FONT_SIZE" : "Font size:", "FIELD_HEADER_HOSTNAME" : "Hostname:", "FIELD_HEADER_USERNAME" : "Username:", "FIELD_HEADER_PASSWORD" : "Password:", "FIELD_HEADER_PASSWORD_REGEX" : "Password regular expression:", "FIELD_HEADER_PORT" : "Port:", "FIELD_OPTION_FONT_SIZE_8" : "8", "FIELD_OPTION_FONT_SIZE_9" : "9", "FIELD_OPTION_FONT_SIZE_10" : "10", "FIELD_OPTION_FONT_SIZE_11" : "11", "FIELD_OPTION_FONT_SIZE_12" : "12", "FIELD_OPTION_FONT_SIZE_14" : "14", "FIELD_OPTION_FONT_SIZE_18" : "18", "FIELD_OPTION_FONT_SIZE_24" : "24", "FIELD_OPTION_FONT_SIZE_30" : "30", "FIELD_OPTION_FONT_SIZE_36" : "36", "FIELD_OPTION_FONT_SIZE_48" : "48", "FIELD_OPTION_FONT_SIZE_60" : "60", "FIELD_OPTION_FONT_SIZE_72" : "72", "FIELD_OPTION_FONT_SIZE_96" : "96", "FIELD_OPTION_FONT_SIZE_EMPTY" : "", "NAME" : "Telnet", "SECTION_HEADER_AUTHENTICATION" : "Authentication", "SECTION_HEADER_DISPLAY" : "Display", "SECTION_HEADER_NETWORK" : "Network" }, "PROTOCOL_VNC" : { "FIELD_HEADER_AUDIO_SERVERNAME" : "Audio server name:", "FIELD_HEADER_COLOR_DEPTH" : "Color depth:", "FIELD_HEADER_CURSOR" : "Cursor:", "FIELD_HEADER_DEST_HOST" : "Destination host:", "FIELD_HEADER_DEST_PORT" : "Destination port:", "FIELD_HEADER_ENABLE_AUDIO" : "Enable audio:", "FIELD_HEADER_ENABLE_SFTP" : "Enable SFTP:", "FIELD_HEADER_HOSTNAME" : "Hostname:", "FIELD_HEADER_PASSWORD" : "Password:", "FIELD_HEADER_PORT" : "Port:", "FIELD_HEADER_READ_ONLY" : "Read-only:", "FIELD_HEADER_SFTP_HOSTNAME" : "Hostname:", "FIELD_HEADER_SFTP_PASSPHRASE" : "Passphrase:", "FIELD_HEADER_SFTP_PASSWORD" : "Password:", "FIELD_HEADER_SFTP_PORT" : "Port:", "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Private key:", "FIELD_HEADER_SFTP_USERNAME" : "Username:", "FIELD_HEADER_SWAP_RED_BLUE" : "Swap red/blue components:", "FIELD_OPTION_COLOR_DEPTH_8" : "256 color", "FIELD_OPTION_COLOR_DEPTH_16" : "Low color (16-bit)", "FIELD_OPTION_COLOR_DEPTH_24" : "True color (24-bit)", "FIELD_OPTION_COLOR_DEPTH_32" : "True color (32-bit)", "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "", "FIELD_OPTION_CURSOR_EMPTY" : "", "FIELD_OPTION_CURSOR_LOCAL" : "Local", "FIELD_OPTION_CURSOR_REMOTE" : "Remote", "NAME" : "VNC", "SECTION_HEADER_AUDIO" : "Audio", "SECTION_HEADER_AUTHENTICATION" : "Authentication", "SECTION_HEADER_CLIPBOARD" : "Appunti", "SECTION_HEADER_DISPLAY" : "Display", "SECTION_HEADER_NETWORK" : "Network", "SECTION_HEADER_REPEATER" : "VNC Repeater", "SECTION_HEADER_SFTP" : "SFTP" }, "SETTINGS" : { "SECTION_HEADER_SETTINGS" : "Settings" }, "SETTINGS_CONNECTIONS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_NEW_CONNECTION" : "Nuova Connessione", "ACTION_NEW_CONNECTION_GROUP" : "Nuovo Gruppo", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "HELP_CONNECTIONS" : "Fai click o tap sulla connessione qui sotto per gestire quella connessione. In base al tuo livello di accesso, le connessioni possono essere craete, eliminate, e le relative proprietà (protocol, hostname, port, etc.) possono essere cambiate.", "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", "SECTION_HEADER_CONNECTIONS" : "Connessioni" }, "SETTINGS_CONNECTION_HISTORY" : { "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Nome della connessione", "TABLE_HEADER_SESSION_STARTDATE" : "Start Time", "TABLE_HEADER_SESSION_DURATION" : "Durata", "TABLE_HEADER_SESSION_USERNAME" : "Username" }, "SETTINGS_PREFERENCES" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", "FIELD_HEADER_LANGUAGE" : "Lingua dell'interfaccia:", "FIELD_HEADER_PASSWORD" : "Password:", "FIELD_HEADER_PASSWORD_OLD" : "Password Attuale:", "FIELD_HEADER_PASSWORD_NEW" : "Nuova Password:", "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "Conferma Nuova Password:", "FIELD_HEADER_USERNAME" : "Username:", "HELP_DEFAULT_INPUT_METHOD" : "The default input method determines how keyboard events are received by Guacamole. Changing this setting may be necessary when using a mobile device, or when typing through an IME. This setting can be overridden on a per-connection basis within the Guacamole menu.", "HELP_DEFAULT_MOUSE_MODE" : "The default mouse emulation mode determines how the remote mouse will behave in new connections with respect to touches. This setting can be overridden on a per-connection basis within the Guacamole menu.", "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", "HELP_LANGUAGE" : "Select a different language below to change the language of all text within Guacamole. Available choices will depend on which languages are installed.", "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", "HELP_UPDATE_PASSWORD" : "Se desideri cambiare la tua password, inserisci la tua password attuale e sotto scriti quella che desideri come nuova password, clicca \"Modifica Password\". La modifica avrà effetto immediato.", "INFO_PASSWORD_CHANGED" : "Password Modificata.", "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Default Input Method", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Default Mouse Emulation Mode", "SECTION_HEADER_UPDATE_PASSWORD" : "Modifica Password" }, "SETTINGS_USERS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_NEW_USER" : "Nuovo utente", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "HELP_USERS" : "Click or tap on a user below to manage that user. Depending on your access level, users can be added and deleted, and their passwords can be changed.", "SECTION_HEADER_USERS" : "Utenti" }, "SETTINGS_SESSIONS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_DELETE" : "Termian Sessione", "DIALOG_HEADER_CONFIRM_DELETE" : "Termina Sessione", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", "HELP_SESSIONS" : "All currently-active Guacamole sessions are listed here. If you wish to kill one or more sessions, check the box next to those sessions and click \"Kill Sessions\". Killing a session will immediately disconnect the user from the associated connection.", "INFO_NO_SESSIONS" : "Nessuna sessione attiva", "SECTION_HEADER_SESSIONS" : "Sessioni Attive", "TABLE_HEADER_SESSION_USERNAME" : "Username", "TABLE_HEADER_SESSION_STARTDATE" : "Attivo da", "TABLE_HEADER_SESSION_REMOTEHOST" : "Remote host", "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Nome della connessione", "TEXT_CONFIRM_DELETE" : "Sei sicuro di voler termianre la sessione selezionata? L'utente che sta utilizzando questa sessione sarà immediatamente disconnesso." }, "USER_MENU" : { "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME" } } guacamole-client-0.9.9/guacamole/src/main/webapp/translations/en.json0100664 0001751 0001751 00000100474 12645510255 027506 0ustar00mjumpermjumper0000000 0000000 { "NAME" : "English", "APP" : { "ACTION_ACKNOWLEDGE" : "OK", "ACTION_CANCEL" : "Cancel", "ACTION_CLONE" : "Clone", "ACTION_CONTINUE" : "Continue", "ACTION_DELETE" : "Delete", "ACTION_DELETE_SESSIONS" : "Kill Sessions", "ACTION_LOGIN" : "Login", "ACTION_LOGOUT" : "Logout", "ACTION_MANAGE_CONNECTIONS" : "Connections", "ACTION_MANAGE_PREFERENCES" : "Preferences", "ACTION_MANAGE_SETTINGS" : "Settings", "ACTION_MANAGE_SESSIONS" : "Active Sessions", "ACTION_MANAGE_USERS" : "Users", "ACTION_NAVIGATE_BACK" : "Back", "ACTION_NAVIGATE_HOME" : "Home", "ACTION_SAVE" : "Save", "ACTION_SEARCH" : "Search", "ACTION_UPDATE_PASSWORD" : "Update Password", "ACTION_VIEW_HISTORY" : "History", "DIALOG_HEADER_ERROR" : "Error", "ERROR_PASSWORD_BLANK" : "Your password cannot be blank.", "ERROR_PASSWORD_MISMATCH" : "The provided passwords do not match.", "FIELD_HEADER_PASSWORD" : "Password:", "FIELD_HEADER_PASSWORD_AGAIN" : "Re-enter Password:", "FIELD_PLACEHOLDER_FILTER" : "Filter", "FORMAT_DATE_TIME_PRECISE" : "yyyy-MM-dd HH:mm:ss", "INFO_ACTIVE_USER_COUNT" : "Currently in use by {USERS} {USERS, plural, one{user} other{users}}.", "NAME" : "Guacamole ${project.version}", "TEXT_HISTORY_DURATION" : "{VALUE} {UNIT, select, second{{VALUE, plural, one{second} other{seconds}}} minute{{VALUE, plural, one{minute} other{minutes}}} hour{{VALUE, plural, one{hour} other{hours}}} day{{VALUE, plural, one{day} other{days}}} other{}}" }, "CLIENT" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Clear", "ACTION_DISCONNECT" : "Disconnect", "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", "ACTION_RECONNECT" : "Reconnect", "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", "ACTION_UPLOAD_FILES" : "Upload Files", "DIALOG_HEADER_CONNECTING" : "Connecting", "DIALOG_HEADER_CONNECTION_ERROR" : "Connection Error", "DIALOG_HEADER_DISCONNECTED" : "Disconnected", "ERROR_CLIENT_201" : "This connection has been closed because the server is busy. Please wait a few minutes and try again.", "ERROR_CLIENT_202" : "The Guacamole server has closed the connection because the remote desktop is taking too long to respond. Please try again or contact your system administrator.", "ERROR_CLIENT_203" : "The remote desktop server encountered an error and has closed the connection. Please try again or contact your system administrator.", "ERROR_CLIENT_205" : "This connection has been closed because it conflicts with another connection. Please try again later.", "ERROR_CLIENT_301" : "Log in failed. Please reconnect and try again.", "ERROR_CLIENT_303" : "You do not have permission to access this connection. If you require access, please ask your system administrator to add you the list of allowed users, or check your system settings.", "ERROR_CLIENT_308" : "The Guacamole server has closed the connection because there has been no response from your browser for long enough that it appeared to be disconnected. This is commonly caused by network problems, such as spotty wireless signal, or simply very slow network speeds. Please check your network and try again.", "ERROR_CLIENT_31D" : "The Guacamole server is denying access to this connection because you have exhausted the limit for simultaneous connection use by an individual user. Please close one or more connections and try again.", "ERROR_CLIENT_DEFAULT" : "An internal error has occurred within the Guacamole server, and the connection has been terminated. If the problem persists, please notify your system administrator, or check your system logs.", "ERROR_TUNNEL_201" : "The Guacamole server has rejected this connection attempt because there are too many active connections. Please wait a few minutes and try again.", "ERROR_TUNNEL_202" : "The connection has been closed because the server is taking too long to respond. This is usually caused by network problems, such as a spotty wireless signal, or slow network speeds. Please check your network connection and try again or contact your system administrator.", "ERROR_TUNNEL_203" : "The server encountered an error and has closed the connection. Please try again or contact your system administrator.", "ERROR_TUNNEL_204" : "The requested connection does not exist. Please check the connection name and try again.", "ERROR_TUNNEL_205" : "This connection is currently in use, and concurrent access to this connection is not allowed. Please try again later.", "ERROR_TUNNEL_301" : "You do not have permission to access this connection because you are not logged in. Please log in and try again.", "ERROR_TUNNEL_303" : "You do not have permission to access this connection. If you require access, please ask your system administrator to add you the list of allowed users, or check your system settings.", "ERROR_TUNNEL_308" : "The Guacamole server has closed the connection because there has been no response from your browser for long enough that it appeared to be disconnected. This is commonly caused by network problems, such as spotty wireless signal, or simply very slow network speeds. Please check your network and try again.", "ERROR_TUNNEL_31D" : "The Guacamole server is denying access to this connection because you have exhausted the limit for simultaneous connection use by an individual user. Please close one or more connections and try again.", "ERROR_TUNNEL_DEFAULT" : "An internal error has occurred within the Guacamole server, and the connection has been terminated. If the problem persists, please notify your system administrator, or check your system logs.", "ERROR_UPLOAD_100" : "File transfer is either not supported or not enabled. Please contact your system administrator, or check your system logs.", "ERROR_UPLOAD_201" : "Too many files are currently being transferred. Please wait for existing transfers to complete, and then try again.", "ERROR_UPLOAD_202" : "The file cannot be transferred because the remote desktop server is taking too long to respond. Please try again or or contact your system administrator.", "ERROR_UPLOAD_203" : "The remote desktop server encountered an error during transfer. Please try again or contact your system administrator.", "ERROR_UPLOAD_204" : "The destination for the file transfer does not exist. Please check that the destination exists and try again.", "ERROR_UPLOAD_205" : "The destination for the file transfer is currently locked. Please wait for any in-progress tasks to complete and try again.", "ERROR_UPLOAD_301" : "You do not have permission to upload this file because you are not logged in. Please log in and try again.", "ERROR_UPLOAD_303" : "You do not have permission to upload this file. If you require access, please check your system settings, or check with your system administrator.", "ERROR_UPLOAD_308" : "The file transfer has stalled. This is commonly caused by network problems, such as spotty wireless signal, or simply very slow network speeds. Please check your network and try again.", "ERROR_UPLOAD_31D" : "Too many files are currently being transferred. Please wait for existing transfers to complete, and then try again.", "ERROR_UPLOAD_DEFAULT" : "An internal error has occurred within the Guacamole server, and the connection has been terminated. If the problem persists, please notify your system administrator, or check your system logs.", "HELP_CLIPBOARD" : "Text copied/cut within Guacamole will appear here. Changes to the text below will affect the remote clipboard.", "HELP_INPUT_METHOD_NONE" : "No input method is used. Keyboard input is accepted from a connected, physical keyboard.", "HELP_INPUT_METHOD_OSK" : "Display and accept input from the built-in Guacamole on-screen keyboard. The on-screen keyboard allows typing of key combinations that may otherwise be impossible (such as Ctrl-Alt-Del).", "HELP_INPUT_METHOD_TEXT" : "Allow typing of text, and emulate keyboard events based on the typed text. This is necessary for devices such as mobile phones that lack a physical keyboard.", "HELP_MOUSE_MODE" : "Determines how the remote mouse behaves with respect to touches.", "HELP_MOUSE_MODE_ABSOLUTE" : "Tap to click. The click occurs at the location of the touch.", "HELP_MOUSE_MODE_RELATIVE" : "Drag to move the mouse pointer and tap to click. The click occurs at the location of the pointer.", "INFO_NO_FILE_TRANSFERS" : "No file transfers.", "NAME_INPUT_METHOD_NONE" : "None", "NAME_INPUT_METHOD_OSK" : "On-screen keyboard", "NAME_INPUT_METHOD_TEXT" : "Text input", "NAME_KEY_CTRL" : "Ctrl", "NAME_KEY_ALT" : "Alt", "NAME_KEY_ESC" : "Esc", "NAME_KEY_TAB" : "Tab", "NAME_MOUSE_MODE_ABSOLUTE" : "Touchscreen", "NAME_MOUSE_MODE_RELATIVE" : "Touchpad", "SECTION_HEADER_CLIPBOARD" : "Clipboard", "SECTION_HEADER_DEVICES" : "Devices", "SECTION_HEADER_DISPLAY" : "Display", "SECTION_HEADER_FILE_TRANSFERS" : "File Transfers", "SECTION_HEADER_INPUT_METHOD" : "Input method", "SECTION_HEADER_MOUSE_MODE" : "Mouse emulation mode", "TEXT_ZOOM_AUTO_FIT" : "Automatically fit to browser window", "TEXT_CLIENT_STATUS_IDLE" : "Idle.", "TEXT_CLIENT_STATUS_CONNECTING" : "Connecting to Guacamole...", "TEXT_CLIENT_STATUS_DISCONNECTED" : "You have been disconnected.", "TEXT_CLIENT_STATUS_WAITING" : "Connected to Guacamole. Waiting for response...", "TEXT_RECONNECT_COUNTDOWN" : "Reconnecting in {REMAINING} {REMAINING, plural, one{second} other{seconds}}...", "TEXT_FILE_TRANSFER_PROGRESS" : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}", "URL_OSK_LAYOUT" : "layouts/en-us-qwerty.json" }, "DATA_SOURCE_DEFAULT" : { "NAME" : "Default (XML)" }, "FORM" : { "FIELD_PLACEHOLDER_DATE" : "YYYY-MM-DD", "FIELD_PLACEHOLDER_TIME" : "HH:MM:SS", "HELP_SHOW_PASSWORD" : "Click to show password", "HELP_HIDE_PASSWORD" : "Click to hide password" }, "HOME" : { "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", "INFO_NO_RECENT_CONNECTIONS" : "No recent connections.", "PASSWORD_CHANGED" : "Password changed.", "SECTION_HEADER_ALL_CONNECTIONS" : "All Connections", "SECTION_HEADER_RECENT_CONNECTIONS" : "Recent Connections" }, "LOGIN": { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "ERROR_INVALID_LOGIN" : "Invalid Login", "FIELD_HEADER_USERNAME" : "Username", "FIELD_HEADER_PASSWORD" : "Password" }, "MANAGE_CONNECTION" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_CLONE" : "@:APP.ACTION_CLONE", "ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_SAVE" : "@:APP.ACTION_SAVE", "DIALOG_HEADER_CONFIRM_DELETE" : "Delete Connection", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_HEADER_LOCATION" : "Location:", "FIELD_HEADER_NAME" : "Name:", "FIELD_HEADER_PROTOCOL" : "Protocol:", "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "Active Now", "INFO_CONNECTION_NOT_USED" : "This connection has not yet been used.", "SECTION_HEADER_EDIT_CONNECTION" : "Edit Connection", "SECTION_HEADER_HISTORY" : "Usage History", "SECTION_HEADER_PARAMETERS" : "Parameters", "TABLE_HEADER_HISTORY_USERNAME" : "Username", "TABLE_HEADER_HISTORY_START" : "Start Time", "TABLE_HEADER_HISTORY_DURATION" : "Duration", "TEXT_CONFIRM_DELETE" : "Connections cannot be restored after they have been deleted. Are you sure you want to delete this connection?", "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" }, "MANAGE_CONNECTION_GROUP" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_SAVE" : "@:APP.ACTION_SAVE", "DIALOG_HEADER_CONFIRM_DELETE" : "Delete Connection Group", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_HEADER_LOCATION" : "Location:", "FIELD_HEADER_NAME" : "Name:", "FIELD_HEADER_TYPE" : "Type:", "NAME_TYPE_BALANCING" : "Balancing", "NAME_TYPE_ORGANIZATIONAL" : "Organizational", "SECTION_HEADER_EDIT_CONNECTION_GROUP" : "Edit Connection Group", "TEXT_CONFIRM_DELETE" : "Connection groups cannot be restored after they have been deleted. Are you sure you want to delete this connection group?" }, "MANAGE_USER" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_SAVE" : "@:APP.ACTION_SAVE", "DIALOG_HEADER_CONFIRM_DELETE" : "Delete User", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", "FIELD_HEADER_ADMINISTER_SYSTEM" : "Administer system:", "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "Change own password:", "FIELD_HEADER_CREATE_NEW_USERS" : "Create new users:", "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Create new connections:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Create new connection groups:", "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", "FIELD_HEADER_USERNAME" : "Username:", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "INFO_READ_ONLY" : "Sorry, but this user account cannot be edited.", "SECTION_HEADER_CONNECTIONS" : "Connections", "SECTION_HEADER_EDIT_USER" : "Edit User", "SECTION_HEADER_PERMISSIONS" : "Permissions", "TEXT_CONFIRM_DELETE" : "Users cannot be restored after they have been deleted. Are you sure you want to delete this user?" }, "PROTOCOL_RDP" : { "FIELD_HEADER_CLIENT_NAME" : "Client name:", "FIELD_HEADER_COLOR_DEPTH" : "Color depth:", "FIELD_HEADER_CONSOLE" : "Administrator console:", "FIELD_HEADER_CONSOLE_AUDIO" : "Support audio in console:", "FIELD_HEADER_CREATE_DRIVE_PATH" : "Automatically create drive:", "FIELD_HEADER_DISABLE_AUDIO" : "Disable audio:", "FIELD_HEADER_DISABLE_AUTH" : "Disable authentication:", "FIELD_HEADER_DOMAIN" : "Domain:", "FIELD_HEADER_DPI" : "Resolution (DPI):", "FIELD_HEADER_DRIVE_PATH" : "Drive path:", "FIELD_HEADER_ENABLE_DESKTOP_COMPOSITION" : "Enable desktop composition (Aero):", "FIELD_HEADER_ENABLE_DRIVE" : "Enable drive:", "FIELD_HEADER_ENABLE_FONT_SMOOTHING" : "Enable font smoothing (ClearType):", "FIELD_HEADER_ENABLE_FULL_WINDOW_DRAG" : "Enable full-window drag:", "FIELD_HEADER_ENABLE_MENU_ANIMATIONS" : "Enable menu animations:", "FIELD_HEADER_ENABLE_PRINTING" : "Enable printing:", "FIELD_HEADER_ENABLE_SFTP" : "Enable SFTP:", "FIELD_HEADER_ENABLE_THEMING" : "Enable theming:", "FIELD_HEADER_ENABLE_WALLPAPER" : "Enable wallpaper:", "FIELD_HEADER_HEIGHT" : "Height:", "FIELD_HEADER_HOSTNAME" : "Hostname:", "FIELD_HEADER_IGNORE_CERT" : "Ignore server certificate:", "FIELD_HEADER_INITIAL_PROGRAM" : "Initial program:", "FIELD_HEADER_PASSWORD" : "Password:", "FIELD_HEADER_PORT" : "Port:", "FIELD_HEADER_PRECONNECTION_BLOB" : "Preconnection BLOB (VM ID):", "FIELD_HEADER_PRECONNECTION_ID" : "RDP source ID:", "FIELD_HEADER_REMOTE_APP_ARGS" : "Parameters:", "FIELD_HEADER_REMOTE_APP_DIR" : "Working directory:", "FIELD_HEADER_REMOTE_APP" : "Program:", "FIELD_HEADER_SECURITY" : "Security mode:", "FIELD_HEADER_SERVER_LAYOUT" : "Keyboard layout:", "FIELD_HEADER_SFTP_DIRECTORY" : "Default upload directory:", "FIELD_HEADER_SFTP_HOSTNAME" : "Hostname:", "FIELD_HEADER_SFTP_PASSPHRASE" : "Passphrase:", "FIELD_HEADER_SFTP_PASSWORD" : "Password:", "FIELD_HEADER_SFTP_PORT" : "Port:", "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Private key:", "FIELD_HEADER_SFTP_USERNAME" : "Username:", "FIELD_HEADER_STATIC_CHANNELS" : "Static channel names:", "FIELD_HEADER_USERNAME" : "Username:", "FIELD_HEADER_WIDTH" : "Width:", "FIELD_OPTION_COLOR_DEPTH_16" : "Low color (16-bit)", "FIELD_OPTION_COLOR_DEPTH_24" : "True color (24-bit)", "FIELD_OPTION_COLOR_DEPTH_32" : "True color (32-bit)", "FIELD_OPTION_COLOR_DEPTH_8" : "256 color", "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "", "FIELD_OPTION_SECURITY_ANY" : "Any", "FIELD_OPTION_SECURITY_EMPTY" : "", "FIELD_OPTION_SECURITY_NLA" : "NLA (Network Level Authentication)", "FIELD_OPTION_SECURITY_RDP" : "RDP encryption", "FIELD_OPTION_SECURITY_TLS" : "TLS encryption", "FIELD_OPTION_SERVER_LAYOUT_DE_DE_QWERTZ" : "German (Qwertz)", "FIELD_OPTION_SERVER_LAYOUT_EMPTY" : "", "FIELD_OPTION_SERVER_LAYOUT_EN_US_QWERTY" : "US English (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_FAILSAFE" : "Unicode", "FIELD_OPTION_SERVER_LAYOUT_FR_FR_AZERTY" : "French (Azerty)", "FIELD_OPTION_SERVER_LAYOUT_IT_IT_QWERTY" : "Italian (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_SV_SE_QWERTY" : "Swedish (Qwerty)", "NAME" : "RDP", "SECTION_HEADER_AUTHENTICATION" : "Authentication", "SECTION_HEADER_BASIC_PARAMETERS" : "Basic Settings", "SECTION_HEADER_DEVICE_REDIRECTION" : "Device Redirection", "SECTION_HEADER_DISPLAY" : "Display", "SECTION_HEADER_NETWORK" : "Network", "SECTION_HEADER_PERFORMANCE" : "Performance", "SECTION_HEADER_PRECONNECTION_PDU" : "Preconnection PDU / Hyper-V", "SECTION_HEADER_REMOTEAPP" : "RemoteApp", "SECTION_HEADER_SFTP" : "SFTP" }, "PROTOCOL_SSH" : { "FIELD_HEADER_COLOR_SCHEME" : "Color scheme:", "FIELD_HEADER_COMMAND" : "Execute command:", "FIELD_HEADER_FONT_NAME" : "Font name:", "FIELD_HEADER_FONT_SIZE" : "Font size:", "FIELD_HEADER_ENABLE_SFTP" : "Enable SFTP:", "FIELD_HEADER_HOSTNAME" : "Hostname:", "FIELD_HEADER_USERNAME" : "Username:", "FIELD_HEADER_PASSWORD" : "Password:", "FIELD_HEADER_PASSPHRASE" : "Passphrase:", "FIELD_HEADER_PORT" : "Port:", "FIELD_HEADER_PRIVATE_KEY" : "Private key:", "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "Black on white", "FIELD_OPTION_COLOR_SCHEME_EMPTY" : "", "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK" : "Gray on black", "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "Green on black", "FIELD_OPTION_COLOR_SCHEME_WHITE_BLACK" : "White on black", "FIELD_OPTION_FONT_SIZE_8" : "8", "FIELD_OPTION_FONT_SIZE_9" : "9", "FIELD_OPTION_FONT_SIZE_10" : "10", "FIELD_OPTION_FONT_SIZE_11" : "11", "FIELD_OPTION_FONT_SIZE_12" : "12", "FIELD_OPTION_FONT_SIZE_14" : "14", "FIELD_OPTION_FONT_SIZE_18" : "18", "FIELD_OPTION_FONT_SIZE_24" : "24", "FIELD_OPTION_FONT_SIZE_30" : "30", "FIELD_OPTION_FONT_SIZE_36" : "36", "FIELD_OPTION_FONT_SIZE_48" : "48", "FIELD_OPTION_FONT_SIZE_60" : "60", "FIELD_OPTION_FONT_SIZE_72" : "72", "FIELD_OPTION_FONT_SIZE_96" : "96", "FIELD_OPTION_FONT_SIZE_EMPTY" : "", "NAME" : "SSH", "SECTION_HEADER_AUTHENTICATION" : "Authentication", "SECTION_HEADER_DISPLAY" : "Display", "SECTION_HEADER_NETWORK" : "Network", "SECTION_HEADER_SESSION" : "Session / Environment", "SECTION_HEADER_SFTP" : "SFTP" }, "PROTOCOL_TELNET" : { "FIELD_HEADER_COLOR_SCHEME" : "Color scheme:", "FIELD_HEADER_FONT_NAME" : "Font name:", "FIELD_HEADER_FONT_SIZE" : "Font size:", "FIELD_HEADER_HOSTNAME" : "Hostname:", "FIELD_HEADER_USERNAME" : "Username:", "FIELD_HEADER_PASSWORD" : "Password:", "FIELD_HEADER_PASSWORD_REGEX" : "Password regular expression:", "FIELD_HEADER_PORT" : "Port:", "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "Black on white", "FIELD_OPTION_COLOR_SCHEME_EMPTY" : "", "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK" : "Gray on black", "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "Green on black", "FIELD_OPTION_COLOR_SCHEME_WHITE_BLACK" : "White on black", "FIELD_OPTION_FONT_SIZE_8" : "8", "FIELD_OPTION_FONT_SIZE_9" : "9", "FIELD_OPTION_FONT_SIZE_10" : "10", "FIELD_OPTION_FONT_SIZE_11" : "11", "FIELD_OPTION_FONT_SIZE_12" : "12", "FIELD_OPTION_FONT_SIZE_14" : "14", "FIELD_OPTION_FONT_SIZE_18" : "18", "FIELD_OPTION_FONT_SIZE_24" : "24", "FIELD_OPTION_FONT_SIZE_30" : "30", "FIELD_OPTION_FONT_SIZE_36" : "36", "FIELD_OPTION_FONT_SIZE_48" : "48", "FIELD_OPTION_FONT_SIZE_60" : "60", "FIELD_OPTION_FONT_SIZE_72" : "72", "FIELD_OPTION_FONT_SIZE_96" : "96", "FIELD_OPTION_FONT_SIZE_EMPTY" : "", "NAME" : "Telnet", "SECTION_HEADER_AUTHENTICATION" : "Authentication", "SECTION_HEADER_DISPLAY" : "Display", "SECTION_HEADER_NETWORK" : "Network" }, "PROTOCOL_VNC" : { "FIELD_HEADER_AUDIO_SERVERNAME" : "Audio server name:", "FIELD_HEADER_CLIPBOARD_ENCODING" : "Encoding:", "FIELD_HEADER_COLOR_DEPTH" : "Color depth:", "FIELD_HEADER_CURSOR" : "Cursor:", "FIELD_HEADER_DEST_HOST" : "Destination host:", "FIELD_HEADER_DEST_PORT" : "Destination port:", "FIELD_HEADER_ENABLE_AUDIO" : "Enable audio:", "FIELD_HEADER_ENABLE_SFTP" : "Enable SFTP:", "FIELD_HEADER_HOSTNAME" : "Hostname:", "FIELD_HEADER_PASSWORD" : "Password:", "FIELD_HEADER_PORT" : "Port:", "FIELD_HEADER_READ_ONLY" : "Read-only:", "FIELD_HEADER_SFTP_DIRECTORY" : "Default upload directory:", "FIELD_HEADER_SFTP_HOSTNAME" : "Hostname:", "FIELD_HEADER_SFTP_PASSPHRASE" : "Passphrase:", "FIELD_HEADER_SFTP_PASSWORD" : "Password:", "FIELD_HEADER_SFTP_PORT" : "Port:", "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Private key:", "FIELD_HEADER_SFTP_USERNAME" : "Username:", "FIELD_HEADER_SWAP_RED_BLUE" : "Swap red/blue components:", "FIELD_OPTION_COLOR_DEPTH_8" : "256 color", "FIELD_OPTION_COLOR_DEPTH_16" : "Low color (16-bit)", "FIELD_OPTION_COLOR_DEPTH_24" : "True color (24-bit)", "FIELD_OPTION_COLOR_DEPTH_32" : "True color (32-bit)", "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "", "FIELD_OPTION_CURSOR_EMPTY" : "", "FIELD_OPTION_CURSOR_LOCAL" : "Local", "FIELD_OPTION_CURSOR_REMOTE" : "Remote", "FIELD_OPTION_CLIPBOARD_ENCODING_CP1252" : "CP1252", "FIELD_OPTION_CLIPBOARD_ENCODING_EMPTY" : "", "FIELD_OPTION_CLIPBOARD_ENCODING_ISO8859_1" : "ISO 8859-1", "FIELD_OPTION_CLIPBOARD_ENCODING_UTF_8" : "UTF-8", "FIELD_OPTION_CLIPBOARD_ENCODING_UTF_16" : "UTF-16", "NAME" : "VNC", "SECTION_HEADER_AUDIO" : "Audio", "SECTION_HEADER_AUTHENTICATION" : "Authentication", "SECTION_HEADER_CLIPBOARD" : "Clipboard", "SECTION_HEADER_DISPLAY" : "Display", "SECTION_HEADER_NETWORK" : "Network", "SECTION_HEADER_REPEATER" : "VNC Repeater", "SECTION_HEADER_SFTP" : "SFTP" }, "SETTINGS" : { "SECTION_HEADER_SETTINGS" : "Settings" }, "SETTINGS_CONNECTION_HISTORY" : { "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", "HELP_CONNECTION_HISTORY" : "History records for past connections are listed here and can be sorted by clicking the column headers. To search for specific records, enter a filter string and click \"Search\". Only records which match the provided filter string will be listed.", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_NO_HISTORY" : "No matching records", "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Connection name", "TABLE_HEADER_SESSION_DURATION" : "Duration", "TABLE_HEADER_SESSION_STARTDATE" : "Start time", "TABLE_HEADER_SESSION_USERNAME" : "Username", "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" }, "SETTINGS_CONNECTIONS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_NEW_CONNECTION" : "New Connection", "ACTION_NEW_CONNECTION_GROUP" : "New Group", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "HELP_CONNECTIONS" : "Click or tap on a connection below to manage that connection. Depending on your access level, connections can be added and deleted, and their properties (protocol, hostname, port, etc.) can be changed.", "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", "SECTION_HEADER_CONNECTIONS" : "Connections" }, "SETTINGS_PREFERENCES" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", "FIELD_HEADER_LANGUAGE" : "Display language:", "FIELD_HEADER_PASSWORD" : "Password:", "FIELD_HEADER_PASSWORD_OLD" : "Current Password:", "FIELD_HEADER_PASSWORD_NEW" : "New Password:", "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "Confirm New Password:", "FIELD_HEADER_USERNAME" : "Username:", "HELP_DEFAULT_INPUT_METHOD" : "The default input method determines how keyboard events are received by Guacamole. Changing this setting may be necessary when using a mobile device, or when typing through an IME. This setting can be overridden on a per-connection basis within the Guacamole menu.", "HELP_DEFAULT_MOUSE_MODE" : "The default mouse emulation mode determines how the remote mouse will behave in new connections with respect to touches. This setting can be overridden on a per-connection basis within the Guacamole menu.", "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", "HELP_LANGUAGE" : "Select a different language below to change the language of all text within Guacamole. Available choices will depend on which languages are installed.", "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", "HELP_UPDATE_PASSWORD" : "If you wish to change your password, enter your current password and the desired new password below, and click \"Update Password\". The change will take effect immediately.", "INFO_PASSWORD_CHANGED" : "Password changed.", "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Default Input Method", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Default Mouse Emulation Mode", "SECTION_HEADER_UPDATE_PASSWORD" : "Change Password" }, "SETTINGS_USERS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_NEW_USER" : "New User", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "HELP_USERS" : "Click or tap on a user below to manage that user. Depending on your access level, users can be added and deleted, and their passwords can be changed.", "SECTION_HEADER_USERS" : "Users" }, "SETTINGS_SESSIONS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_DELETE" : "Kill Sessions", "DIALOG_HEADER_CONFIRM_DELETE" : "Kill Sessions", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", "HELP_SESSIONS" : "All currently-active Guacamole sessions are listed here. If you wish to kill one or more sessions, check the box next to those sessions and click \"Kill Sessions\". Killing a session will immediately disconnect the user from the associated connection.", "INFO_NO_SESSIONS" : "No active sessions", "SECTION_HEADER_SESSIONS" : "Active Sessions", "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Connection name", "TABLE_HEADER_SESSION_REMOTEHOST" : "Remote host", "TABLE_HEADER_SESSION_STARTDATE" : "Active since", "TABLE_HEADER_SESSION_USERNAME" : "Username", "TEXT_CONFIRM_DELETE" : "Are you sure you want to kill all selected sessions? The users using these sessions will be immediately disconnected." }, "USER_MENU" : { "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY" } } guacamole-client-0.9.9/guacamole/src/main/webapp/translations/ru.json0100664 0001751 0001751 00000110602 12645510255 027524 0ustar00mjumpermjumper0000000 0000000 { "NAME" : "РуÑÑкий", "APP" : { "ACTION_ACKNOWLEDGE" : "ОК", "ACTION_CANCEL" : "Отмена", "ACTION_CLONE" : "Скопировать", "ACTION_DELETE" : "Удалить", "ACTION_DELETE_SESSIONS" : "Завершить ÑеÑÑии", "ACTION_LOGIN" : "Вход", "ACTION_LOGOUT" : "Выход", "ACTION_MANAGE_CONNECTIONS" : "ПодключениÑ", "ACTION_MANAGE_PREFERENCES" : "ÐаÑтройки", "ACTION_MANAGE_SETTINGS" : "Опции", "ACTION_MANAGE_SESSIONS" : "Ðктивные ÑеÑÑии", "ACTION_MANAGE_USERS" : "Пользователи", "ACTION_NAVIGATE_BACK" : "Ðазад", "ACTION_NAVIGATE_HOME" : "ГлавнаÑ", "ACTION_SAVE" : "Сохранить", "ACTION_UPDATE_PASSWORD" : "Обновить пароль", "DIALOG_HEADER_ERROR" : "Ошибка", "ERROR_PASSWORD_BLANK" : "Пароль не может быть пуÑтым.", "ERROR_PASSWORD_MISMATCH" : "Указанные пароли не Ñовпадают.", "FIELD_HEADER_PASSWORD" : "Пароль:", "FIELD_HEADER_PASSWORD_AGAIN" : "Повтор паролÑ:", "FIELD_PLACEHOLDER_FILTER" : "Фильтр", "FORMAT_DATE_TIME_PRECISE" : "yyyy-MM-dd HH:mm:ss", "INFO_ACTIVE_USER_COUNT" : "Подключено пользователей {USERS}.", "NAME" : "Guacamole ${project.version}", "TEXT_HISTORY_DURATION" : "{VALUE} {UNIT, select, second{{VALUE, plural, one{Ñекунда} other{Ñек}}} minute{{VALUE, plural, one{минута} other{мин}}} hour{{VALUE, plural, one{чаÑ} other{ч}}} day{{VALUE, plural, one{день} other{дн}}} other{}}" }, "CLIENT" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "ОчиÑтить", "ACTION_DISCONNECT" : "ОтключитьÑÑ", "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", "ACTION_RECONNECT" : "ПереподключитьÑÑ", "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", "ACTION_UPLOAD_FILES" : "Загрузка файлов", "DIALOG_HEADER_CONNECTING" : "Подключение", "DIALOG_HEADER_CONNECTION_ERROR" : "Ошибка подключениÑ", "DIALOG_HEADER_DISCONNECTED" : "Отключено", "ERROR_CLIENT_201" : "Соединение было закрыто, так как Ñервер перегружен. ПожалуйÑта, попробуйте повторить попытку позднее.", "ERROR_CLIENT_202" : "Сервер закрыл Ñоединение, так как удаленные рабочий Ñтол не отвечает. ПожалуйÑта, попробуйте повторить попытку позднее или обратитеÑÑŒ к админиÑтратору..", "ERROR_CLIENT_203" : "Ðа Ñервере удаленные рабочих Ñтолов произошла ошибка, и Ñоединение было закрыто. ПожалуйÑта, попробуйте повторить попытку позднее или обратитеÑÑŒ к админиÑтратору.", "ERROR_CLIENT_205" : "Соединение было закрыто, так как оно конфликтовало Ñ Ð´Ñ€ÑƒÐ³Ð¸Ð¼ Ñоединением. ПожалуйÑта, попробуйте повторить попытку позднее.", "ERROR_CLIENT_301" : "Ðе удалоÑÑŒ выполнить вход. ПожалуйÑта, переподключитеÑÑŒ и повторите попытку.", "ERROR_CLIENT_303" : "У Ð²Ð°Ñ Ð½ÐµÑ‚ Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ð´Ð¾Ñтупа к Ñтому Ñоединению. Ð”Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð´Ð¾Ñтупа, пожалуйÑта, обратитеÑÑŒ к админиÑтратору.", "ERROR_CLIENT_308" : "Сервер Guacamole закрыл Ñоединение, так как браузер не ответил в течение отведенного времени. Такое бывает в Ñлучае Ð²Ð¾Ð·Ð½Ð¸ÐºÐ½Ð¾Ð²ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ Ñ Ð´Ð¾Ñтупом в Ñеть, ÑвÑзанных Ñо Ñлабым беÑпроводным Ñигналом или низкими ÑкороÑÑ‚Ñми передачи данных. ПожалуйÑта, проверьте ваше Ñетевое подключение и повторите попытку Ñнова.", "ERROR_CLIENT_31D" : "Сервер Guacamole отклонил доÑтуп к Ñоединению, так как вы превыÑили макÑимальное количеÑтво одновременных подключений. ПожалуйÑта, закройте ненужные ÑÐ¾ÐµÐ´Ð¸Ð½ÐµÐ½Ð¸Ñ Ð¸ повторите попытку.", "ERROR_CLIENT_DEFAULT" : "Соединение было прервано из-за внутренней ошибки Ñервера. ПожалуйÑта, попробуйте повторить попытку позднее или обратитеÑÑŒ к админиÑтратору.", "ERROR_TUNNEL_201" : "Сервер запретил Ñоединение, так как открыто Ñлишком много Ñоединений. ПожалуйÑта, попробуйте повторить попытку позднее.", "ERROR_TUNNEL_202" : "Сервер закрыл Ñоединение, так как удаленные рабочий Ñтол не отвечает. ПожалуйÑта, попробуйте повторить попытку позднее или обратитеÑÑŒ к админиÑтратору", "ERROR_TUNNEL_203" : "Ðа Ñервере удаленные рабочих Ñтолов произошла ошибка, и Ñоединение было закрыто. ПожалуйÑта, попробуйте повторить попытку позднее или обратитеÑÑŒ к админиÑтратору.", "ERROR_TUNNEL_204" : "Соединение не ÑущеÑтвует. Проверьте, пожалуйÑта, название ÑоединениÑ.", "ERROR_TUNNEL_205" : "Соединение в наÑтоÑщий момент занÑто, и доÑтуп к Ñтому Ñоединению не разрешен. ожалуйÑта, попробуйте повторить попытку позднее.", "ERROR_TUNNEL_301" : "У Ð²Ð°Ñ Ð½ÐµÑ‚ Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ð´Ð¾Ñтупа к Ñтому Ñоединению, так как вы не выполнили вход. ПожалуйÑта, выполните вход и повторите попытку.", "ERROR_TUNNEL_303" : "У Ð²Ð°Ñ Ð½ÐµÑ‚ Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ð´Ð¾Ñтупа к Ñтому Ñоединению. Ð”Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð´Ð¾Ñтупа, пожалуйÑта, обратитеÑÑŒ к админиÑтратору.", "ERROR_TUNNEL_308" : "Сервер Guacamole закрыл Ñоединение, так как браузер не ответил в течение отведенного времени. Такое бывает в Ñлучае Ð²Ð¾Ð·Ð½Ð¸ÐºÐ½Ð¾Ð²ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ Ñ Ð´Ð¾Ñтупом в Ñеть, ÑвÑзанных Ñо Ñлабым беÑпроводным Ñигналом или низкими ÑкороÑÑ‚Ñми передачи данных. ПожалуйÑта, проверьте ваше Ñетевое подключение и повторите попытку Ñнова.", "ERROR_TUNNEL_31D" : "Сервер Guacamole отклонил доÑтуп к Ñоединению, так как вы превыÑили макÑимальное количеÑтво одновременных подключений. ПожалуйÑта, закройте ненужные ÑÐ¾ÐµÐ´Ð¸Ð½ÐµÐ½Ð¸Ñ Ð¸ повторите попытку.", "ERROR_TUNNEL_DEFAULT" : "Соединение было прервано из-за внутренней ошибки Ñервера. ПожалуйÑта, попробуйте повторить попытку позднее или обратитеÑÑŒ к админиÑтратору.", "ERROR_UPLOAD_100" : "Передача файлов либо не поддерживаетÑÑ, либо не включена. ПожалуйÑта, обратитеÑÑŒ к админиÑтратору.", "ERROR_UPLOAD_201" : "Слишком много задач передачи файлов активно. Подождите Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ Ñ‚ÐµÐºÑƒÑ‰Ð¸Ñ… передач и повторите попытку Ñнова.", "ERROR_UPLOAD_202" : "Файл не может быть передан, так как удаленный рабочий Ñтол не отвечает. ПожалуйÑта, попробуйте повторить попытку позднее или обратитеÑÑŒ к админиÑтратору.", "ERROR_UPLOAD_203" : "Произошла ошибка при передаче файла. ПожалуйÑта, попробуйте повторить попытку позднее или обратитеÑÑŒ к админиÑтратору.", "ERROR_UPLOAD_204" : "Ðазначение Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ´Ð°Ð²Ð°ÐµÐ¼Ð¾Ð³Ð¾ файла не ÑущеÑтвует. ПожалуйÑта, проверьте назначение и повторите попытку Ñнова.", "ERROR_UPLOAD_205" : "Ðазнечение Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ´Ð°Ð²Ð°ÐµÐ¼Ð¾Ð³Ð¾ файла заблокировано. ПожалуйÑта, дождитеÑÑŒ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ Ð´Ñ€ÑƒÐ³Ð¸Ñ… передач и повторите попытку Ñнова.", "ERROR_UPLOAD_301" : "У Ð²Ð°Ñ Ð½ÐµÑ‚ Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñ Ð½Ð° загрузку файла, так как вы не выполнили вход. ПожалуйÑта, выполните вход и повторите попытку передачи файла.", "ERROR_UPLOAD_303" : "У Ð²Ð°Ñ Ð½ÐµÑ‚ Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñ Ð½Ð° загрузку файла. Ð”Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð²ÐµÑ€ÑŒÑ‚Ðµ неÑтройки или обратитеÑÑŒ к админиÑтратору.", "ERROR_UPLOAD_308" : "Передача файла завиÑла. Такое бывает в Ñлучае Ð²Ð¾Ð·Ð½Ð¸ÐºÐ½Ð¾Ð²ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ Ñ Ð´Ð¾Ñтупом в Ñеть, ÑвÑзанных Ñо Ñлабым беÑпроводным Ñигналом или низкими ÑкороÑÑ‚Ñми передачи данных. ПожалуйÑта, проверьте ваше Ñетевое подключение и повторите попытку Ñнова.", "ERROR_UPLOAD_31D" : "Слишком много файлов передаетÑÑ Ð² наÑтоÑщий момент. Подождите Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ Ñ‚ÐµÐºÑƒÑ‰Ð¸Ñ… передач и повторите попытку Ñнова.", "ERROR_UPLOAD_DEFAULT" : "Соединение было прервано из-за внутренней ошибки Ñервера. ПожалуйÑта, попробуйте повторить попытку позднее или обратитеÑÑŒ к админиÑтратору.", "HELP_CLIPBOARD" : "ТекÑÑ‚, Ñкопированный или вырезанный внутри ÑеанÑа, поÑвитÑÑ Ð² Ñтом поле. Изменение текÑта также отразитьÑÑ Ð½Ð° буфере обмена удаленного рабочего Ñтола.", "HELP_INPUT_METHOD_NONE" : "Ðе выбран метод ввода. Ввод разрешен Ð´Ð»Ñ Ñ„Ð¸Ð·Ð¸Ñ‡ÐµÑкой клавиатуры.", "HELP_INPUT_METHOD_OSK" : "Отображать и принимать ввод Ñо вÑтроенной Ñкранной клавиатуры. Ð­ÐºÑ€Ð°Ð½Ð½Ð°Ñ ÐºÐ»Ð°Ð²Ð¸Ð°Ñ‚ÑƒÑ€Ð° позволÑет вводить любые комбинации, недоÑтупные в других режимах (например Alt-Ctrl-Del).", "HELP_INPUT_METHOD_TEXT" : "Разрешить ввод текÑта и Ñмулировать ÑÐ¾Ð±Ñ‹Ñ‚Ð¸Ñ ÐºÐ»Ð°Ð²Ð¸Ð°Ñ‚ÑƒÑ€Ñ‹ в завиÑимоÑти от нажатых клавиш. Это необходимо Ð´Ð»Ñ ÑƒÑтройÑтв без физичеÑкой клавиатуры (Ñмартфоны, планшеты).", "HELP_MOUSE_MODE" : "ОпределÑет поведение курÑора мыши при прикоÑновении.", "HELP_MOUSE_MODE_ABSOLUTE" : "ПрикоÑнитеÑÑŒ, чтобы Ñделать клик. Клик проиÑходит в точке прикоÑÐ½Ð¾Ð²ÐµÐ½Ð¸Ñ Ðº Ñкрану.", "HELP_MOUSE_MODE_RELATIVE" : "ПотÑните, чтобы перемещать курÑор. ПрикоÑнитеÑÑŒ, чтобы Ñделать клик. Клик проиÑходит в точке Ð½Ð°Ñ…Ð¾Ð¶Ð´ÐµÐ½Ð¸Ñ ÐºÑƒÑ€Ñора.", "INFO_NO_FILE_TRANSFERS" : "Ðет загрузок.", "NAME_INPUT_METHOD_NONE" : "Ðет", "NAME_INPUT_METHOD_OSK" : "Ð­ÐºÑ€Ð°Ð½Ð½Ð°Ñ ÐºÐ»Ð°Ð²Ð¸Ð°Ñ‚ÑƒÑ€Ð°", "NAME_INPUT_METHOD_TEXT" : "Ввод текÑта", "NAME_KEY_CTRL" : "Ctrl", "NAME_KEY_ALT" : "Alt", "NAME_KEY_ESC" : "Esc", "NAME_KEY_TAB" : "Tab", "NAME_MOUSE_MODE_ABSOLUTE" : "Touchscreen", "NAME_MOUSE_MODE_RELATIVE" : "Touchpad", "SECTION_HEADER_CLIPBOARD" : "Буфер обмена", "SECTION_HEADER_DISPLAY" : "Экран", "SECTION_HEADER_FILE_TRANSFERS" : "Загрузки файлов", "SECTION_HEADER_INPUT_METHOD" : "Метод ввода", "SECTION_HEADER_MOUSE_MODE" : "Режим ÑмулÑции мыши", "TEXT_ZOOM_AUTO_FIT" : "ÐвтоматичеÑки умещать в окне", "TEXT_CLIENT_STATUS_IDLE" : "БездейÑтвие.", "TEXT_CLIENT_STATUS_CONNECTING" : "Подключение к Guacamole...", "TEXT_CLIENT_STATUS_DISCONNECTED" : "Ð’Ñ‹ были уÑпешно подключены.", "TEXT_CLIENT_STATUS_WAITING" : "Подключено к Guacamole. Ожидание ответа...", "TEXT_RECONNECT_COUNTDOWN" : "Переподключение через {REMAINING} Ñек...", "TEXT_FILE_TRANSFER_PROGRESS" : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}", "URL_OSK_LAYOUT" : "layouts/ru-ru-qwerty.json" }, "FORM" : { "HELP_SHOW_PASSWORD" : "Ðажмите, чтобы поÑмотреть пароль", "HELP_HIDE_PASSWORD" : "Ðажмите, чтобы ÑпрÑтать пароль" }, "HOME" : { "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", "INFO_NO_RECENT_CONNECTIONS" : "Ðет недавних подключениÑ.", "PASSWORD_CHANGED" : "Пароль был уÑпешно изменен.", "SECTION_HEADER_ALL_CONNECTIONS" : "Ð’Ñе подключениÑ", "SECTION_HEADER_RECENT_CONNECTIONS" : "Ðедавние подключениÑ" }, "LOGIN": { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "ERROR_INVALID_LOGIN" : "Ðеверные данные Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ð°", "FIELD_HEADER_USERNAME" : "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ", "FIELD_HEADER_PASSWORD" : "Пароль" }, "MANAGE_CONNECTION" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_CLONE" : "@:APP.ACTION_CLONE", "ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_SAVE" : "@:APP.ACTION_SAVE", "DIALOG_HEADER_CONFIRM_DELETE" : "Удалить подключение", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_HEADER_LOCATION" : "Размещение:", "FIELD_HEADER_NAME" : "Ðазвание:", "FIELD_HEADER_PROTOCOL" : "Протокол:", "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "Ðктивно", "INFO_CONNECTION_NOT_USED" : "Это подключение еще не иÑпользовалоÑÑŒ.", "SECTION_HEADER_EDIT_CONNECTION" : "Редактировать подключение", "SECTION_HEADER_HISTORY" : "ИÑÑ‚Ð¾Ñ€Ð¸Ñ Ð¸ÑпользованиÑ", "SECTION_HEADER_PARAMETERS" : "ÐаÑтройки", "TABLE_HEADER_HISTORY_USERNAME" : "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ", "TABLE_HEADER_HISTORY_START" : "Ð’Ñ€ÐµÐ¼Ñ Ð½Ð°Ñ‡Ð°Ð»Ð°", "TABLE_HEADER_HISTORY_DURATION" : "ПродолжительноÑть", "TEXT_CONFIRM_DELETE" : "ÐŸÐ¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Ð½Ðµ могут быть воÑÑтановлены поÑле удалениÑ. Ð’Ñ‹ уверены, что хотите удалить подключение?", "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" }, "MANAGE_CONNECTION_GROUP" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_SAVE" : "@:APP.ACTION_SAVE", "DIALOG_HEADER_CONFIRM_DELETE" : "Удалить группу подключений", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_HEADER_LOCATION" : "Размещение:", "FIELD_HEADER_NAME" : "Ðазвание:", "FIELD_HEADER_TYPE" : "Тип:", "NAME_TYPE_BALANCING" : "БаланÑировщик", "NAME_TYPE_ORGANIZATIONAL" : "Корпоративный", "SECTION_HEADER_EDIT_CONNECTION_GROUP" : "Редактировать группу подключений", "TEXT_CONFIRM_DELETE" : "Группы подключений не могут быть воÑÑтановлены поÑле удалениÑ. Ð’Ñ‹ уверены, что хотите удалить группу подключений?" }, "MANAGE_USER" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_SAVE" : "@:APP.ACTION_SAVE", "DIALOG_HEADER_CONFIRM_DELETE" : "Удалить пользователÑ", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", "FIELD_HEADER_ADMINISTER_SYSTEM" : "ÐдминиÑтрирование ÑиÑтемы:", "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "Изменить ÑобÑтвенный пароль:", "FIELD_HEADER_CREATE_NEW_USERS" : "Создать нового пользователÑ:", "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Создать новое подключение:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Создать новую группу подключений:", "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", "FIELD_HEADER_USERNAME" : "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ:", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "SECTION_HEADER_CONNECTIONS" : "ПодключениÑ", "SECTION_HEADER_EDIT_USER" : "Редактировать пользователÑ", "SECTION_HEADER_PERMISSIONS" : "РазрешениÑ", "TEXT_CONFIRM_DELETE" : "Пользователи не могут быть воÑÑтановлены поÑле удалениÑ. Ð’Ñ‹ уверены, что хотите удалить пользователÑ?" }, "PROTOCOL_RDP" : { "FIELD_HEADER_CLIENT_NAME" : "Ð˜Ð¼Ñ ÐºÐ»Ð¸ÐµÐ½Ñ‚Ð°:", "FIELD_HEADER_COLOR_DEPTH" : "Глубина цвета:", "FIELD_HEADER_CONSOLE_AUDIO" : "Поддержка звука:", "FIELD_HEADER_CONSOLE" : "КонÑоль админиÑтратора:", "FIELD_HEADER_DISABLE_AUDIO" : "Отключить звук:", "FIELD_HEADER_DISABLE_AUTH" : "Отключить аутентификацию:", "FIELD_HEADER_DOMAIN" : "Домен:", "FIELD_HEADER_DPI" : "Разрешение Ñкрана (DPI):", "FIELD_HEADER_DRIVE_PATH" : "Путь до диÑка:", "FIELD_HEADER_ENABLE_DRIVE" : "Включить диÑк:", "FIELD_HEADER_ENABLE_PRINTING" : "Включить печать:", "FIELD_HEADER_ENABLE_SFTP" : "Включить SFTP:", "FIELD_HEADER_HEIGHT" : "Ð’Ñ‹Ñота:", "FIELD_HEADER_HOSTNAME" : "Ðазвание Ñервера:", "FIELD_HEADER_IGNORE_CERT" : "Игнорировать Ñертификат Ñервера:", "FIELD_HEADER_INITIAL_PROGRAM" : "ЗапуÑк программ при подключении:", "FIELD_HEADER_PASSWORD" : "Пароль:", "FIELD_HEADER_PORT" : "Порт:", "FIELD_HEADER_REMOTE_APP_ARGS" : "Параметры RemoteApp:", "FIELD_HEADER_REMOTE_APP_DIR" : "Рабочий каталог RemoteApp:", "FIELD_HEADER_REMOTE_APP" : "Программы RemoteApp:", "FIELD_HEADER_SECURITY" : "Режим безопаÑноÑти:", "FIELD_HEADER_SERVER_LAYOUT" : "РаÑкладка клавиатуры:", "FIELD_HEADER_SFTP_HOSTNAME" : "Ðазвание Ñервера:", "FIELD_HEADER_SFTP_PASSPHRASE" : "Ð¡ÐµÐºÑ€ÐµÑ‚Ð½Ð°Ñ Ñ„Ñ€Ð°Ð·Ð°:", "FIELD_HEADER_SFTP_PASSWORD" : "Пароль:", "FIELD_HEADER_SFTP_PORT" : "Порт:", "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Приватный ключ:", "FIELD_HEADER_SFTP_USERNAME" : "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ:", "FIELD_HEADER_STATIC_CHANNELS" : "Статичное название канала:", "FIELD_HEADER_USERNAME" : "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ:", "FIELD_HEADER_WIDTH" : "Ширина:", "FIELD_OPTION_COLOR_DEPTH_16" : "Low color (16-бит)", "FIELD_OPTION_COLOR_DEPTH_24" : "True color (24-бит)", "FIELD_OPTION_COLOR_DEPTH_32" : "True color (32-бит)", "FIELD_OPTION_COLOR_DEPTH_8" : "256 цветов", "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "", "FIELD_OPTION_SECURITY_ANY" : "Любой", "FIELD_OPTION_SECURITY_EMPTY" : "", "FIELD_OPTION_SECURITY_NLA" : "NLA (Network Level Authentication)", "FIELD_OPTION_SECURITY_RDP" : "RDP шифрование", "FIELD_OPTION_SECURITY_TLS" : "TLS шифрование", "FIELD_OPTION_SERVER_LAYOUT_DE_DE_QWERTZ" : "German (Qwertz)", "FIELD_OPTION_SERVER_LAYOUT_EMPTY" : "", "FIELD_OPTION_SERVER_LAYOUT_EN_US_QWERTY" : "US English (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_FAILSAFE" : "Unicode", "FIELD_OPTION_SERVER_LAYOUT_FR_FR_AZERTY" : "French (Azerty)", "FIELD_OPTION_SERVER_LAYOUT_IT_IT_QWERTY" : "Italian (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_SV_SE_QWERTY" : "Swedish (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_RU_RU_QWERTY" : "Russian (Qwerty)", "NAME" : "RDP" }, "PROTOCOL_SSH" : { "FIELD_HEADER_FONT_NAME" : "Шрифт:", "FIELD_HEADER_FONT_SIZE" : "Размер шрифта:", "FIELD_HEADER_ENABLE_SFTP" : "Включить SFTP:", "FIELD_HEADER_HOSTNAME" : "Ðазвание Ñервера:", "FIELD_HEADER_USERNAME" : "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ:", "FIELD_HEADER_PASSWORD" : "Пароль:", "FIELD_HEADER_PASSPHRASE" : "Ð¡ÐµÐºÑ€ÐµÑ‚Ð½Ð°Ñ Ñ„Ñ€Ð°Ð·Ð°:", "FIELD_HEADER_PORT" : "Порт:", "FIELD_HEADER_PRIVATE_KEY" : "Приватный ключ:", "FIELD_OPTION_FONT_SIZE_8" : "8", "FIELD_OPTION_FONT_SIZE_9" : "9", "FIELD_OPTION_FONT_SIZE_10" : "10", "FIELD_OPTION_FONT_SIZE_11" : "11", "FIELD_OPTION_FONT_SIZE_12" : "12", "FIELD_OPTION_FONT_SIZE_14" : "14", "FIELD_OPTION_FONT_SIZE_18" : "18", "FIELD_OPTION_FONT_SIZE_24" : "24", "FIELD_OPTION_FONT_SIZE_30" : "30", "FIELD_OPTION_FONT_SIZE_36" : "36", "FIELD_OPTION_FONT_SIZE_48" : "48", "FIELD_OPTION_FONT_SIZE_60" : "60", "FIELD_OPTION_FONT_SIZE_72" : "72", "FIELD_OPTION_FONT_SIZE_96" : "96", "FIELD_OPTION_FONT_SIZE_EMPTY" : "", "NAME" : "SSH" }, "PROTOCOL_TELNET" : { "FIELD_HEADER_FONT_NAME" : "Шрифт:", "FIELD_HEADER_FONT_SIZE" : "Размер шрифта:", "FIELD_HEADER_HOSTNAME" : "Ðазвание Ñервера:", "FIELD_HEADER_USERNAME" : "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ:", "FIELD_HEADER_PASSWORD" : "Пароль:", "FIELD_HEADER_PASSWORD_REGEX" : "РегулÑрное выражение Ð´Ð»Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ:", "FIELD_HEADER_PORT" : "Порт:", "FIELD_OPTION_FONT_SIZE_8" : "8", "FIELD_OPTION_FONT_SIZE_9" : "9", "FIELD_OPTION_FONT_SIZE_10" : "10", "FIELD_OPTION_FONT_SIZE_11" : "11", "FIELD_OPTION_FONT_SIZE_12" : "12", "FIELD_OPTION_FONT_SIZE_14" : "14", "FIELD_OPTION_FONT_SIZE_18" : "18", "FIELD_OPTION_FONT_SIZE_24" : "24", "FIELD_OPTION_FONT_SIZE_30" : "30", "FIELD_OPTION_FONT_SIZE_36" : "36", "FIELD_OPTION_FONT_SIZE_48" : "48", "FIELD_OPTION_FONT_SIZE_60" : "60", "FIELD_OPTION_FONT_SIZE_72" : "72", "FIELD_OPTION_FONT_SIZE_96" : "96", "FIELD_OPTION_FONT_SIZE_EMPTY" : "", "NAME" : "Telnet" }, "PROTOCOL_VNC" : { "FIELD_HEADER_AUDIO_SERVERNAME" : "Ðазвание аудио-Ñервера:", "FIELD_HEADER_COLOR_DEPTH" : "Глубина цвета:", "FIELD_HEADER_CURSOR" : "КурÑор:", "FIELD_HEADER_DEST_HOST" : "Repeater destination host:", "FIELD_HEADER_DEST_PORT" : "Repeater destination port:", "FIELD_HEADER_ENABLE_AUDIO" : "Включить звук:", "FIELD_HEADER_ENABLE_SFTP" : "Включить SFTP:", "FIELD_HEADER_HOSTNAME" : "Ðазвание Ñервера:", "FIELD_HEADER_PASSWORD" : "Пароль:", "FIELD_HEADER_PORT" : "Порт:", "FIELD_HEADER_READ_ONLY" : "Только проÑмотр:", "FIELD_HEADER_SFTP_HOSTNAME" : "Ðазвание Ñервера:", "FIELD_HEADER_SFTP_PASSPHRASE" : "Ð¡ÐµÐºÑ€ÐµÑ‚Ð½Ð°Ñ Ñ„Ñ€Ð°Ð·Ð°:", "FIELD_HEADER_SFTP_PASSWORD" : "Пароль:", "FIELD_HEADER_SFTP_PORT" : "Порт:", "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Приватный ключ:", "FIELD_HEADER_SFTP_USERNAME" : "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ:", "FIELD_HEADER_SWAP_RED_BLUE" : "ПоменÑть Ñиний и краÑный компоненты:", "FIELD_OPTION_COLOR_DEPTH_8" : "256 цветов", "FIELD_OPTION_COLOR_DEPTH_16" : "Low color (16-бит)", "FIELD_OPTION_COLOR_DEPTH_24" : "True color (24-бит)", "FIELD_OPTION_COLOR_DEPTH_32" : "True color (32-бит)", "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "", "FIELD_OPTION_CURSOR_EMPTY" : "", "FIELD_OPTION_CURSOR_LOCAL" : "Локальный", "FIELD_OPTION_CURSOR_REMOTE" : "Удаленный", "NAME" : "VNC", "SECTION_HEADER_CLIPBOARD" : "Буфер обмена" }, "SETTINGS" : { "SECTION_HEADER_SETTINGS" : "ÐаÑтройки" }, "SETTINGS_CONNECTIONS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_NEW_CONNECTION" : "Ðовое подключение", "ACTION_NEW_CONNECTION_GROUP" : "ÐÐ¾Ð²Ð°Ñ Ð³Ñ€ÑƒÐ¿Ð¿Ð°", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "HELP_CONNECTIONS" : "Ðажмите на подключение, чтобы управлÑть им. Ð’ завиÑимоÑти от прав доÑтупа возможно добавление и удаление подключений, а также изменение их ÑвойÑтв (протокол, название Ñервера, порт и пр.).", "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", "SECTION_HEADER_CONNECTIONS" : "ПодключениÑ" }, "SETTINGS_CONNECTION_HISTORY" : { "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Ðазвание подключениÑ", "TABLE_HEADER_SESSION_DURATION" : "ПродолжительноÑть", "TABLE_HEADER_SESSION_STARTDATE" : "Ð’Ñ€ÐµÐ¼Ñ Ð½Ð°Ñ‡Ð°Ð»Ð°", "TABLE_HEADER_SESSION_USERNAME" : "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ", "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" }, "SETTINGS_PREFERENCES" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", "FIELD_HEADER_LANGUAGE" : "Язык:", "FIELD_HEADER_PASSWORD" : "Пароль:", "FIELD_HEADER_PASSWORD_OLD" : "Текущий пароль:", "FIELD_HEADER_PASSWORD_NEW" : "Ðовый пароль:", "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "Подтверждение паролÑ:", "FIELD_HEADER_USERNAME" : "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ:", "HELP_DEFAULT_INPUT_METHOD" : "Режим ввода по умолчанию определÑет, каким образом Ð½Ð°Ð¶Ð°Ñ‚Ð¸Ñ Ð½Ð° клавиатуру будут передаватьÑÑ Guacamole. Изменение данной наÑтройки может быть полезным при работе Ñ Ð¼Ð¾Ð±Ð¸Ð»ÑŒÐ½Ñ‹Ñ… уÑтройÑтв или при вводе через IME. Ð”Ð°Ð½Ð½Ð°Ñ Ð½Ð°Ñтройка может быть Ñделана Ð´Ð»Ñ ÐºÐ°Ð¶Ð´Ð¾Ð³Ð¾ Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Ñ‡ÐµÑ€ÐµÐ· оÑновное меню Guacamole.", "HELP_DEFAULT_MOUSE_MODE" : "Режим ÑмулÑции мыши по умолчанию определÑет, каким образом мышь на удаленном Ñервере будет реагировать на прикоÑÐ½Ð¾Ð²ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ð½Ð¾Ð²Ñ‹Ñ… подключений. Ð”Ð°Ð½Ð½Ð°Ñ Ð½Ð°Ñтройка может быть переопределена Ð´Ð»Ñ ÐºÐ°Ð¶Ð´Ð¾Ð³Ð¾ Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Ñ‡ÐµÑ€ÐµÐ· оÑновное меню Guacamole.", "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", "HELP_LANGUAGE" : "Выберите другой Ñзык из ÑпиÑка ниже, чтобы иÑпользовать его в Guacamole. ДоÑтупноÑть опций завиÑит от уÑтановленных в ÑиÑтеме Ñзыков.", "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", "HELP_UPDATE_PASSWORD" : "ЕÑли вы хотите изменить пароль, укажите ваш текущий пароль и дважды новый пароль. Затем нажмите на \"Изменить пароль\". Ð˜Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð²ÑтупÑÑ‚ в Ñилу моментально.", "INFO_PASSWORD_CHANGED" : "Пароль уÑпешно изменен.", "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Метод ввода по умолчанию", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Режим ÑмулÑции мыши по умолчанию", "SECTION_HEADER_UPDATE_PASSWORD" : "Изменить пароль" }, "SETTINGS_USERS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_NEW_USER" : "Ðовый пользователь", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "HELP_USERS" : "Ðажмите на пользователÑ, чтобы управлÑть им. Ð’ завиÑимоÑти от прав доÑтупа возможно добавление и удаление пользователей, а также изменение паролей.", "SECTION_HEADER_USERS" : "Пользователи" }, "SETTINGS_SESSIONS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_DELETE" : "Завершить ÑеÑÑии", "DIALOG_HEADER_CONFIRM_DELETE" : "Завершение ÑеÑÑий", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", "HELP_SESSIONS" : "Ð’Ñе активные в наÑтоÑщий момент ÑеÑÑии Guacamole предÑтавлены здеÑÑŒ. ЕÑли вы хотите завершить одну или неÑколько ÑеÑÑий, выберите нужные ÑеÑÑии и нажмите на \"Завершить ÑеÑÑии\". Принудительное завершение ÑеÑÑий приведет к немедленному отключению пользователей, которые ими пользуютÑÑ.", "INFO_NO_SESSIONS" : "Ðет активных ÑеÑÑий", "SECTION_HEADER_SESSIONS" : "Ðктивные ÑеÑÑии", "TABLE_HEADER_SESSION_USERNAME" : "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ", "TABLE_HEADER_SESSION_STARTDATE" : "Ðктивен Ñ", "TABLE_HEADER_SESSION_REMOTEHOST" : "Удаленный Ñервер", "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Ðазвание подключениÑ", "TEXT_CONFIRM_DELETE" : "Ð’Ñ‹ уверены, что хотите завершить вÑе выбранные ÑеÑÑии? Пользователи, работающие в Ñтих ÑеÑÑиÑÑ…, будут немедленно отключены." }, "USER_MENU" : { "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME" } } guacamole-client-0.9.9/guacamole/src/main/webapp/translations/de.json0100664 0001751 0001751 00000102415 12645510255 027471 0ustar00mjumpermjumper0000000 0000000 { "NAME" : "Deutsch", "APP" : { "ACTION_ACKNOWLEDGE" : "OK", "ACTION_CANCEL" : "Abbruch", "ACTION_CLONE" : "Kopieren", "ACTION_CONTINUE" : "Weiter", "ACTION_DELETE" : "Löschen", "ACTION_DELETE_SESSIONS" : "Beende Sitzung", "ACTION_LOGIN" : "Anmelden", "ACTION_LOGOUT" : "Abmelden", "ACTION_MANAGE_CONNECTIONS" : "Verbindungen", "ACTION_MANAGE_PREFERENCES" : "Einstellungen", "ACTION_MANAGE_SETTINGS" : "Einstellungen", "ACTION_MANAGE_SESSIONS" : "Aktive Sitzungen", "ACTION_MANAGE_USERS" : "Benutzer", "ACTION_NAVIGATE_BACK" : "Zurück", "ACTION_NAVIGATE_HOME" : "Startseite", "ACTION_SAVE" : "Speichern", "ACTION_SEARCH" : "Suche", "ACTION_UPDATE_PASSWORD" : "Aktualisiere Passwort", "ACTION_VIEW_HISTORY" : "Verlauf", "DIALOG_HEADER_ERROR" : "Fehler", "ERROR_PASSWORD_BLANK" : "Bitte ein Passwort vergeben.", "ERROR_PASSWORD_MISMATCH" : "Die Passwörter stimmen nicht überein.", "FIELD_HEADER_PASSWORD" : "Passwort:", "FIELD_HEADER_PASSWORD_AGAIN" : "Wiederhole Passwort:", "FIELD_PLACEHOLDER_FILTER" : "Filter", "FORMAT_DATE_TIME_PRECISE" : "dd-MM-yyyy HH:mm:ss", "INFO_ACTIVE_USER_COUNT" : "In Benutzung durch {USERS} Benutzer.", "NAME" : "Guacamole ${project.version}", "TEXT_HISTORY_DURATION" : "{VALUE} {UNIT, select, second{{VALUE, plural, one{Sekunde} other{Sekunden}}} minute{{VALUE, plural, one{Minute} other{Minuten}}} hour{{VALUE, plural, one{Stunde} other{Stunden}}} day{{VALUE, plural, one{Tag} other{Tage}}} other{}}" }, "CLIENT" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Entferne abgeschlossene Übertragungen", "ACTION_DISCONNECT" : "Trennen", "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", "ACTION_RECONNECT" : "Neu Verbinden", "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", "ACTION_UPLOAD_FILES" : "Dateien hochladen", "DIALOG_HEADER_CONNECTING" : "Verbindung", "DIALOG_HEADER_CONNECTION_ERROR" : "Verbindungsfehler", "DIALOG_HEADER_DISCONNECTED" : "Verbindung getrennt", "ERROR_CLIENT_201" : "Aufgrund hoher Serverauslastung wurde diese Verbindung zurückgesetzt. Versuche es in wenigen Minuten erneut.", "ERROR_CLIENT_202" : "Der Verbindungsaufbau wurde durch den Guacamole Server abgebrochen da der Entfernte Computer nicht reagiert. Versuche es noch einmal oder kontaktiere den Systemadministrator.", "ERROR_CLIENT_203" : "Der entfernte Computer hat einen Fehler hervorgerufen und die Verbindung geschlossen. Versuche es noch einmal oder kontaktiere den Systemadministrator.", "ERROR_CLIENT_205" : "Diese Verbindung kollidiert mit einer bestehenen Verbindung. Versuche es später erneut.", "ERROR_CLIENT_301" : "Anmeldung Fehlgeschlagen. Bitte schließen Sie und versuchen Sie es erneut.", "ERROR_CLIENT_303" : "Sie haben keine Berechtigung auf diese Verbindung zuzugreifen. Wenn Sie diese Berechtigung benötigen, bitten Sie den Systemadministrator diese Berechtigung hinzuzufügen oder prüfen Sie Ihre Systemeinstellugen.", "ERROR_CLIENT_308" : "Die Verbindung wurde durch den Guacamole Server geschlossen da keine aktive Interkommukaion mit dem Browser besteht. Dies wird gewöhnlich durch Netzwerkprobleme verursacht, wie eine schlechte drahtlose Verbindung oder eine sehr langsame Netzwerkverbindung. Bitte überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es erneut.", "ERROR_CLIENT_31D" : "Der Zugang zu dieser Verbindung wurde durch den Guacamole Server verweigert, da die maximale Anzahl der gleichzeitigen Zugiffe für diese Verbindung für einen einzelnen Benutzer erreicht wurde.", "ERROR_CLIENT_DEFAULT" : "Die Verbindung wurde aufgrund eines interen Fehlers im Guacamole Server beendet. Sollte dieses Problem weiterhin bestehen informieren Sie den Systemadministrator oder überprüfen Sie die Protokolle.", "ERROR_TUNNEL_201" : "Der Verbindungsversuch wurde aufgrund zu vieler bestehenden Verbindungen durch den Guacamole Server zurückgewiesen. Versuche es in wenigen Minuten erneut.", "ERROR_TUNNEL_202" : "Die Verbindung zum Server wurde aufgrund hoher Latenz geschlossen. Dies wird gewöhnlich durch Netzwerkprobleme verursacht, wie eine schlechte drahtlose Verbindung oder eine sehr langsame Netzwerkverbindung. Bitte überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es erneut.", "ERROR_TUNNEL_203" : "Die Verbindung wurde aufgrund eines interen Fehlers beendet. Versuche es noch einmal oder kontaktiere den Systemadministrator.", "ERROR_TUNNEL_204" : "Die angeforderte Verbindung exisiert nicht. Bitte überprüfe den Verbindungsnamen und versuche es erneut.", "ERROR_TUNNEL_205" : "Diese Verbindung ist in Verwendung, ein konkurrierender Zugriff ist nicht gestattet. Versuche es später erneut.", "ERROR_TUNNEL_301" : "Sie haben keine Zugriffsberechtigung für diese Verbindung. Bitte melden Sie sich an und versuchen es erneut.", "ERROR_TUNNEL_303" : "Sie haben keine Zugriffsberechtigung für diese Verbindung. Wenn Sie diese Berechtigung benötigen, bitten Sie den Systemadministrator diese Berechtigung hinzuzufügen oder prüfen Sie Ihre Systemeinstellugen.", "ERROR_TUNNEL_308" : "Die Verbindung wurde durch den Guacamole Server geschlossen da keine aktive Interkommunikation mit dem Browser besteht. Dies wird gewöhnlich durch Netzwerkprobleme verursacht, wie eine schlechte drahtlose Verbindung oder eine sehr langsame Netzwerkverbindung. Bitte überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es erneut.", "ERROR_TUNNEL_31D" : "Der Zugang zu dieser Verbindung wurde durch den Guacamole Server verweigert, da die maximale Anzahl der gleichzeitigen Zugiffe für einen einzelnen Benutzer erreicht wurde. Bitte schliesse eine oder mehrere Verbindungen und versuche es erneut.", "ERROR_TUNNEL_DEFAULT" : "Die Verbindung wurde aufgrund eines interen Fehlers im Guacamole Server beendet. Sollte dieses Problem weiterhin bestehen informieren Sie den Systemadministrator oder überprüfen Sie die Protokolle.", "ERROR_UPLOAD_100" : "Dateiübertragungen werden entweder nicht unterstützt oder sind nicht aktiviert. Bitte kontaktiere den Systemadministrator oder überprüfe die Protokolle.", "ERROR_UPLOAD_201" : "Die maximale Anzahl gleichzeitiger Dateiübertragungen erreicht. Bitte warte bis laufende Dateiübertagungen abgeschlossen sind und versuche es erneut.", "ERROR_UPLOAD_202" : "Die Dateiübertragung konnte nicht gestartet werden da der Entfernet Computer nicht reagiert. Bitte versuche es erneut oder kontaktiere den Systemadministrator.", "ERROR_UPLOAD_203" : "Der entfernte Computer hat bei der Übertragungen einen Fehler verursacht. Bitte versuche es erneut oder kontaktiere den Systemadministrator.", "ERROR_UPLOAD_204" : "Das Übertragungsziel existiert nicht. Bitte überprüfe ob der Zielort exisitert und versuche es erneut.", "ERROR_UPLOAD_205" : "Das Übertragungsziel ist zur Zeit gesperrt. Bitte warte bis alle laufenden Prozesse beendet wurden und versuche es erneut.", "ERROR_UPLOAD_301" : "Es besteht ohne Anmeldung keine Berechtigung zur Dateiübertragung. Bitte anmelden und erneut versuchen.", "ERROR_UPLOAD_303" : "Es besteht keine Berechtingung zur Dateiübertragung. Wenn Sie diese Berechtigung benötigen überprüfen Sie die Systemeinstellungen oder überprüfen Sie diese gemeinsam mit dem Systemadministrator.", "ERROR_UPLOAD_308" : "Die Dateiübertragung weist keinen Fortschritt auf. Dies wird gewöhnlich durch Netzwerkprobleme verursacht, wie eine schlechte drahtlose Verbindung oder eine sehr langsame Netzwerkverbindung. Bitte überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es erneut.", "ERROR_UPLOAD_31D" : "Die maximale Anzahl gleichzeiter Dateiübertragungen erreicht. Bitte warte bis laufende Dateiübertagungen abgeschlossen sind und versuche es erneut.", "ERROR_UPLOAD_DEFAULT" : "Die Verbindung wurde aufgrund eines interen Fehlers im Guacamole Server beendet. Sollte dieses Problem weiterhin bestehen informieren Sie den Systemadministrator oder überprüfen Sie die Protokolle.", "HELP_CLIPBOARD" : "Kopierter oder ausgeschnittener Text aus Guacamole wird hier angezeigt. Änderungen am Text werden direkt auf die entfernte Zwischenablage angewandt.", "HELP_INPUT_METHOD_NONE" : "Keine Eingabemethode in Verwendung. Tastatureingaben werden von der Hardwaretastatur akzeptiert.", "HELP_INPUT_METHOD_OSK" : "Bildschirmeingaben und die eingebettete Guacamole Bildschrimtastatur werden akzeptiert. Die Bildschirmtastatur gestattet Tastenkombinationen die ansonsten unmöglich sind (z.B.: Strg-Alt-Del).", "HELP_INPUT_METHOD_TEXT" : "Gestattet Eingaben von Text und emuliert Tastaturkombinationen basierend auf den eingegebenen Text. Dies wird benötigt für Geräte ohne Hardwaretastatur.", "HELP_MOUSE_MODE" : "Beeinflusst, wie sich die entfernte Maus bei Touchpadberührungen verhält.", "HELP_MOUSE_MODE_ABSOLUTE" : "Tippen Sie auf die Zielposition, der Klick erfolgt am Ort der Berührung des Touchscreen's.", "HELP_MOUSE_MODE_RELATIVE" : "Den Mauszeiger zur Zielposition bewegen und klicken. Der Klick erfolgt an der Position des Mauszeigers.", "INFO_NO_FILE_TRANSFERS" : "Keine Dateiübertragungen.", "NAME_INPUT_METHOD_NONE" : "Keine", "NAME_INPUT_METHOD_OSK" : "Bildschirmtastatur", "NAME_INPUT_METHOD_TEXT" : "Texteingabe", "NAME_KEY_CTRL" : "Strg", "NAME_KEY_ALT" : "Alt", "NAME_KEY_ESC" : "Esc", "NAME_KEY_TAB" : "Tab", "NAME_MOUSE_MODE_ABSOLUTE" : "Touchscreen", "NAME_MOUSE_MODE_RELATIVE" : "Touchpad", "SECTION_HEADER_CLIPBOARD" : "Zwischenablage", "SECTION_HEADER_DEVICES" : "Geräte", "SECTION_HEADER_DISPLAY" : "Anzeige", "SECTION_HEADER_FILE_TRANSFERS" : "Dateiübertragung", "SECTION_HEADER_INPUT_METHOD" : "Eingabemethode", "SECTION_HEADER_MOUSE_MODE" : "Mausemulationsmodus", "TEXT_ZOOM_AUTO_FIT" : "Autoanpassung Fenstergröße", "TEXT_CLIENT_STATUS_IDLE" : "Inaktiv.", "TEXT_CLIENT_STATUS_CONNECTING" : "Verbindungsaufbau zu Guacamole...", "TEXT_CLIENT_STATUS_DISCONNECTED" : "Verbindung wurde getrennt.", "TEXT_CLIENT_STATUS_WAITING" : "Verbindungsaufbau zu Guacamole. Bitte warten...", "TEXT_RECONNECT_COUNTDOWN" : "Neuverbindung in {REMAINING} {REMAINING, plural, one{Sekunde} other{Sekunden}}...", "TEXT_FILE_TRANSFER_PROGRESS" : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}", "URL_OSK_LAYOUT" : "layouts/de-de-qwertz.json" }, "DATA_SOURCE_DEFAULT" : { "NAME" : "Standard (XML)" }, "FORM" : { "FIELD_PLACEHOLDER_DATE" : "YYYY-MM-DD", "FIELD_PLACEHOLDER_TIME" : "HH:MM:SS", "HELP_SHOW_PASSWORD" : "Passwort anzeigen", "HELP_HIDE_PASSWORD" : "Passwort ausblenden" }, "HOME" : { "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", "INFO_NO_RECENT_CONNECTIONS" : "Keine aktiven Verbindungen.", "PASSWORD_CHANGED" : "Passwort geändert.", "SECTION_HEADER_ALL_CONNECTIONS" : "Alle Verbindungen", "SECTION_HEADER_RECENT_CONNECTIONS" : "Letzte Verbindungen" }, "LOGIN": { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "ERROR_INVALID_LOGIN" : "Anmeldungsfehler", "FIELD_HEADER_USERNAME" : "Benutzername", "FIELD_HEADER_PASSWORD" : "Passwort" }, "MANAGE_CONNECTION" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_CLONE" : "@:APP.ACTION_CLONE", "ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_SAVE" : "@:APP.ACTION_SAVE", "DIALOG_HEADER_CONFIRM_DELETE" : "Lösche Verbindung", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_HEADER_LOCATION" : "Standort:", "FIELD_HEADER_NAME" : "Name:", "FIELD_HEADER_PROTOCOL" : "Protokol:", "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "Aktivieren", "INFO_CONNECTION_NOT_USED" : "Diese Verbindung wurde bisher nicht verwendet.", "SECTION_HEADER_EDIT_CONNECTION" : "Bearbeite Verbindung", "SECTION_HEADER_HISTORY" : "Verlauf", "SECTION_HEADER_PARAMETERS" : "Parameter", "TABLE_HEADER_HISTORY_USERNAME" : "Benutzername", "TABLE_HEADER_HISTORY_START" : "Begin", "TABLE_HEADER_HISTORY_DURATION" : "Dauer", "TEXT_CONFIRM_DELETE" : "Dieser Löschvorgang ist unumkehrbar. Soll diese Verbindung wirklich gelöscht werden?", "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" }, "MANAGE_CONNECTION_GROUP" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_SAVE" : "@:APP.ACTION_SAVE", "DIALOG_HEADER_CONFIRM_DELETE" : "Lösche Verbindungsgruppe", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_HEADER_LOCATION" : "Standort:", "FIELD_HEADER_NAME" : "Name:", "FIELD_HEADER_TYPE" : "Typ:", "NAME_TYPE_BALANCING" : "Balancing", "NAME_TYPE_ORGANIZATIONAL" : "Organisation", "SECTION_HEADER_EDIT_CONNECTION_GROUP" : "Ändere Verbindungsgruppe", "TEXT_CONFIRM_DELETE" : "Dieser Löschvorgang ist unumkehrbar. Soll diese Verbindungsgruppe wirklich gelöscht werden?" }, "MANAGE_USER" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_DELETE" : "@:APP.ACTION_DELETE", "ACTION_SAVE" : "@:APP.ACTION_SAVE", "DIALOG_HEADER_CONFIRM_DELETE" : "Lösche Benutzer", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", "FIELD_HEADER_ADMINISTER_SYSTEM" : "Administration:", "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "Ändere eigenes Passwort:", "FIELD_HEADER_CREATE_NEW_USERS" : "Erstelle neue Benutzer:", "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Erstelle neue Verbindung:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Erstelle neue Verbindungsgruppe:", "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", "FIELD_HEADER_USERNAME" : "Benutzername:", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "INFO_READ_ONLY" : "Dieses Benutzerkonto kann nicht bearbeitet werden.", "SECTION_HEADER_CONNECTIONS" : "Verbindungen", "SECTION_HEADER_EDIT_USER" : "Ändere Benutzer", "SECTION_HEADER_PERMISSIONS" : "Berechtigungen", "TEXT_CONFIRM_DELETE" : "Dieser Löschvorgang ist unumkehrbar. Soll dieser Benutzer wirklich gelöscht werden?" }, "PROTOCOL_RDP" : { "FIELD_HEADER_CLIENT_NAME" : "Client-Name:", "FIELD_HEADER_COLOR_DEPTH" : "Farbtiefe:", "FIELD_HEADER_CONSOLE" : "Mit Konsole verbinden (Windows 2003 / 2003 R2):", "FIELD_HEADER_CONSOLE_AUDIO" : "Audiounterstützung Konsole:", "FIELD_HEADER_CREATE_DRIVE_PATH" : "Erstellen Sie automatisch Laufwerk:", "FIELD_HEADER_DISABLE_AUDIO" : "Deaktivere Audio:", "FIELD_HEADER_DISABLE_AUTH" : "Deaktivere Authentifizierung:", "FIELD_HEADER_DOMAIN" : "Domäne:", "FIELD_HEADER_DPI" : "Auflösung (DPI):", "FIELD_HEADER_DRIVE_PATH" : "Laufwerkspfad:", "FIELD_HEADER_ENABLE_DESKTOP_COMPOSITION" : "Aktiviere Desktop Gestaltung (Aero):", "FIELD_HEADER_ENABLE_DRIVE" : "Aktiviere Laufwerk:", "FIELD_HEADER_ENABLE_FONT_SMOOTHING" : "Aktiviere Schriftartglättung (ClearType):", "FIELD_HEADER_ENABLE_FULL_WINDOW_DRAG" : "Aktiviere Fensterziehen:", "FIELD_HEADER_ENABLE_MENU_ANIMATIONS" : "Aktiviere Menüanimationen:", "FIELD_HEADER_ENABLE_PRINTING" : "Aktiviere Drucken:", "FIELD_HEADER_ENABLE_SFTP" : "Aktiviere SFTP:", "FIELD_HEADER_ENABLE_THEMING" : "Aktiviere Theming:", "FIELD_HEADER_ENABLE_WALLPAPER" : "Aktiviere Desktophintergrund:", "FIELD_HEADER_HEIGHT" : "Höhe:", "FIELD_HEADER_HOSTNAME" : "Hostname:", "FIELD_HEADER_IGNORE_CERT" : "Server Zertifikat ignorieren:", "FIELD_HEADER_INITIAL_PROGRAM" : "Startprogramm:", "FIELD_HEADER_PASSWORD" : "Passwort:", "FIELD_HEADER_PORT" : "Port:", "FIELD_HEADER_REMOTE_APP_ARGS" : "Parameter:", "FIELD_HEADER_REMOTE_APP_DIR" : "Arbeitsverzeichnis:", "FIELD_HEADER_REMOTE_APP" : "Programm:", "FIELD_HEADER_SECURITY" : "Sicherheitsverfahren:", "FIELD_HEADER_SERVER_LAYOUT" : "Tastatur Layout:", "FIELD_HEADER_SFTP_DIRECTORY" : "Standard-Upload-Verzeichnis:", "FIELD_HEADER_SFTP_HOSTNAME" : "Hostname:", "FIELD_HEADER_SFTP_PASSPHRASE" : "Passphrase:", "FIELD_HEADER_SFTP_PASSWORD" : "Passwort:", "FIELD_HEADER_SFTP_PORT" : "Port:", "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Privater Schlüssel:", "FIELD_HEADER_SFTP_USERNAME" : "Benutzername:", "FIELD_HEADER_STATIC_CHANNELS" : "Statischer Kanalname:", "FIELD_HEADER_USERNAME" : "Benutzername:", "FIELD_HEADER_WIDTH" : "Breite:", "FIELD_OPTION_COLOR_DEPTH_16" : "Hohe Farbtiefe (16-bit)", "FIELD_OPTION_COLOR_DEPTH_24" : "Echtfarben (24-bit)", "FIELD_OPTION_COLOR_DEPTH_32" : "Echtfarben (32-bit)", "FIELD_OPTION_COLOR_DEPTH_8" : "256 Farben", "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "", "FIELD_OPTION_SECURITY_ANY" : "Jede", "FIELD_OPTION_SECURITY_EMPTY" : "", "FIELD_OPTION_SECURITY_NLA" : "NLA (Netzwerkebene Authentifizierung)", "FIELD_OPTION_SECURITY_RDP" : "RDP Verschlüsselung", "FIELD_OPTION_SECURITY_TLS" : "TLS Verschlüsselung", "FIELD_OPTION_SERVER_LAYOUT_DE_DE_QWERTZ" : "Deutsch (Qwertz)", "FIELD_OPTION_SERVER_LAYOUT_EMPTY" : "", "FIELD_OPTION_SERVER_LAYOUT_EN_US_QWERTY" : "US Englisch (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_FAILSAFE" : "Unicode", "FIELD_OPTION_SERVER_LAYOUT_FR_FR_AZERTY" : "Französisch (Azerty)", "FIELD_OPTION_SERVER_LAYOUT_IT_IT_QWERTY" : "Italienisch (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_SV_SE_QWERTY" : "Schwedisch (Qwerty)", "NAME" : "RDP", "SECTION_HEADER_AUTHENTICATION" : "Authentifizierung", "SECTION_HEADER_BASIC_PARAMETERS" : "Basiseinstellungen", "SECTION_HEADER_DEVICE_REDIRECTION" : "Geräteumleitung", "SECTION_HEADER_DISPLAY" : "Bildschirm", "SECTION_HEADER_NETWORK" : "Netzwerk", "SECTION_HEADER_PERFORMANCE" : "Geschwindigkeit", "SECTION_HEADER_REMOTEAPP" : "Entferntes Programm", "SECTION_HEADER_SFTP" : "SFTP" }, "PROTOCOL_SSH" : { "FIELD_HEADER_COLOR_SCHEME" : "Farbschema:", "FIELD_HEADER_COMMAND" : "Befehl ausführen:", "FIELD_HEADER_FONT_NAME" : "Schriftart Name:", "FIELD_HEADER_FONT_SIZE" : "Schriftart Größe:", "FIELD_HEADER_ENABLE_SFTP" : "Aktiviere SFTP:", "FIELD_HEADER_HOSTNAME" : "Hostname:", "FIELD_HEADER_USERNAME" : "Benutzername:", "FIELD_HEADER_PASSWORD" : "Passwort:", "FIELD_HEADER_PASSPHRASE" : "Passphrase:", "FIELD_HEADER_PORT" : "Port:", "FIELD_HEADER_PRIVATE_KEY" : "Privater Schlüssel:", "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "Schwarz auf Weiß", "FIELD_OPTION_COLOR_SCHEME_EMPTY" : "", "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK" : "Grau auf Schwarz", "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "Grün auf Schwarz", "FIELD_OPTION_COLOR_SCHEME_WHITE_BLACK" : "Weiß auf Schwarz", "FIELD_OPTION_FONT_SIZE_8" : "8", "FIELD_OPTION_FONT_SIZE_9" : "9", "FIELD_OPTION_FONT_SIZE_10" : "10", "FIELD_OPTION_FONT_SIZE_11" : "11", "FIELD_OPTION_FONT_SIZE_12" : "12", "FIELD_OPTION_FONT_SIZE_14" : "14", "FIELD_OPTION_FONT_SIZE_18" : "18", "FIELD_OPTION_FONT_SIZE_24" : "24", "FIELD_OPTION_FONT_SIZE_30" : "30", "FIELD_OPTION_FONT_SIZE_36" : "36", "FIELD_OPTION_FONT_SIZE_48" : "48", "FIELD_OPTION_FONT_SIZE_60" : "60", "FIELD_OPTION_FONT_SIZE_72" : "72", "FIELD_OPTION_FONT_SIZE_96" : "96", "FIELD_OPTION_FONT_SIZE_EMPTY" : "", "NAME" : "SSH", "SECTION_HEADER_AUTHENTICATION" : "Authentifizierung", "SECTION_HEADER_DISPLAY" : "Bildschirm", "SECTION_HEADER_NETWORK" : "Netzwerk", "SECTION_HEADER_SESSION" : "Sitzung / Umgebung", "SECTION_HEADER_SFTP" : "SFTP" }, "PROTOCOL_TELNET" : { "FIELD_HEADER_COLOR_SCHEME" : "Farbschema:", "FIELD_HEADER_FONT_NAME" : "Schriftart Name:", "FIELD_HEADER_FONT_SIZE" : "Schriftart Größe:", "FIELD_HEADER_HOSTNAME" : "Hostname:", "FIELD_HEADER_USERNAME" : "Benutzername:", "FIELD_HEADER_PASSWORD" : "Passwort:", "FIELD_HEADER_PASSWORD_REGEX" : "Reguläre Passwortersetzungen:", "FIELD_HEADER_PORT" : "Port:", "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "Schwarz auf Weiß", "FIELD_OPTION_COLOR_SCHEME_EMPTY" : "", "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK" : "Grau auf Schwarz", "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "Grün auf Schwarz", "FIELD_OPTION_COLOR_SCHEME_WHITE_BLACK" : "Weiß auf Schwarz", "FIELD_OPTION_FONT_SIZE_8" : "8", "FIELD_OPTION_FONT_SIZE_9" : "9", "FIELD_OPTION_FONT_SIZE_10" : "10", "FIELD_OPTION_FONT_SIZE_11" : "11", "FIELD_OPTION_FONT_SIZE_12" : "12", "FIELD_OPTION_FONT_SIZE_14" : "14", "FIELD_OPTION_FONT_SIZE_18" : "18", "FIELD_OPTION_FONT_SIZE_24" : "24", "FIELD_OPTION_FONT_SIZE_30" : "30", "FIELD_OPTION_FONT_SIZE_36" : "36", "FIELD_OPTION_FONT_SIZE_48" : "48", "FIELD_OPTION_FONT_SIZE_60" : "60", "FIELD_OPTION_FONT_SIZE_72" : "72", "FIELD_OPTION_FONT_SIZE_96" : "96", "FIELD_OPTION_FONT_SIZE_EMPTY" : "", "NAME" : "Telnet", "SECTION_HEADER_AUTHENTICATION" : "Authentifizierung", "SECTION_HEADER_DISPLAY" : "Bildschirm", "SECTION_HEADER_NETWORK" : "Netzwerk" }, "PROTOCOL_VNC" : { "FIELD_HEADER_AUDIO_SERVERNAME" : "Audioservername:", "FIELD_HEADER_CLIPBOARD_ENCODING" : "Codierung:", "FIELD_HEADER_COLOR_DEPTH" : "Farbtiefe:", "FIELD_HEADER_CURSOR" : "Cursor:", "FIELD_HEADER_DEST_HOST" : "Ziel Host:", "FIELD_HEADER_DEST_PORT" : "Ziel Port:", "FIELD_HEADER_ENABLE_AUDIO" : "Aktiviere Audio:", "FIELD_HEADER_ENABLE_SFTP" : "Aktiviere SFTP:", "FIELD_HEADER_HOSTNAME" : "Hostname:", "FIELD_HEADER_PASSWORD" : "Passwort:", "FIELD_HEADER_PORT" : "Port:", "FIELD_HEADER_READ_ONLY" : "Nur-Lesen:", "FIELD_HEADER_SFTP_DIRECTORY" : "Standard-Upload-Verzeichnis:", "FIELD_HEADER_SFTP_HOSTNAME" : "Hostname:", "FIELD_HEADER_SFTP_PASSPHRASE" : "Passphrase:", "FIELD_HEADER_SFTP_PASSWORD" : "Passwort:", "FIELD_HEADER_SFTP_PORT" : "Port:", "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Privater Schlüssel:", "FIELD_HEADER_SFTP_USERNAME" : "Benutzername:", "FIELD_HEADER_SWAP_RED_BLUE" : "Vertausche rot/blau Komponenten:", "FIELD_OPTION_COLOR_DEPTH_8" : "256 Farben", "FIELD_OPTION_COLOR_DEPTH_16" : "Hohe Farbtiefe (16-bit)", "FIELD_OPTION_COLOR_DEPTH_24" : "Echfarben (24-bit)", "FIELD_OPTION_COLOR_DEPTH_32" : "Echfarben (32-bit)", "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "", "FIELD_OPTION_CURSOR_EMPTY" : "", "FIELD_OPTION_CURSOR_LOCAL" : "Lokal", "FIELD_OPTION_CURSOR_REMOTE" : "Entfernt", "FIELD_OPTION_CLIPBOARD_ENCODING_CP1252" : "CP1252", "FIELD_OPTION_CLIPBOARD_ENCODING_EMPTY" : "", "FIELD_OPTION_CLIPBOARD_ENCODING_ISO8859_1" : "ISO 8859-1", "FIELD_OPTION_CLIPBOARD_ENCODING_UTF_8" : "UTF-8", "FIELD_OPTION_CLIPBOARD_ENCODING_UTF_16" : "UTF-16", "NAME" : "VNC", "SECTION_HEADER_AUDIO" : "Audio", "SECTION_HEADER_AUTHENTICATION" : "Authentifizierung", "SECTION_HEADER_CLIPBOARD" : "Zwischenablage", "SECTION_HEADER_DISPLAY" : "Bildschirm", "SECTION_HEADER_NETWORK" : "Netzwerk", "SECTION_HEADER_REPEATER" : "VNC Repeater", "SECTION_HEADER_SFTP" : "SFTP" }, "SETTINGS" : { "SECTION_HEADER_SETTINGS" : "Einstellungen" }, "SETTINGS_CONNECTIONS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_NEW_CONNECTION" : "Neue Verbindung", "ACTION_NEW_CONNECTION_GROUP" : "Neue Verbindungsgruppe", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "HELP_CONNECTIONS" : "Klicke oder Tippe auf eine Verbindung um diese zu verwalten. Abhänig von Ihrer Zugriffsebene können Verbindungen hinzugefügt, gelöscht oder Parameter (Protokol, Hostname, Port, etc.) geändert werden.", "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", "SECTION_HEADER_CONNECTIONS" : "Verbindungen" }, "SETTINGS_CONNECTION_HISTORY" : { "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", "HELP_CONNECTION_HISTORY" : "Die letzten Verbindungen werden hier historisch aufgelistet und können durch Klicken auf die Spaltenüberschriften sortiert werden. Zum Aufsuchen von bestimmten Datensätzen, geben Sie eine Filterzeichenfolge ein und klicken Sie auf \"Suchen\". Nur Datensätze, die die vorgesehenen Filterzeichenfolge entsprechen, werden aufgelistet.", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_NO_HISTORY" : "Keine passenden Datensätze", "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Verbindungsname", "TABLE_HEADER_SESSION_DURATION" : "Dauer", "TABLE_HEADER_SESSION_STARTDATE" : "Startzeit", "TABLE_HEADER_SESSION_USERNAME" : "Benutzername", "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" }, "SETTINGS_PREFERENCES" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", "FIELD_HEADER_LANGUAGE" : "Anzeigesprache:", "FIELD_HEADER_PASSWORD" : "Passwort:", "FIELD_HEADER_PASSWORD_OLD" : "Aktuelles Passwort:", "FIELD_HEADER_PASSWORD_NEW" : "Neues Passwort:", "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "Passwortbestättigung:", "FIELD_HEADER_USERNAME" : "Benutzername:", "HELP_DEFAULT_INPUT_METHOD" : "Die Standardeingabemethode bestimmt wie Tastaturereignisse an Guacamole weitergeleitet werden. Eine Änderung dieser Einstellung kann erforderlich sein, wenn ein mobiles Gerät verwendet wird oder bei der Eingabe durch einen IME. Dieses Verhalten kann im Menü innerhalb der Guacamole Verbindung geändert werden.", "HELP_DEFAULT_MOUSE_MODE" : "Der Standard Mausemulationsmodus bestimmt wie sich die entfernte Maus bei Touchpad Berührungen verhält. Dieses Verhalten kann im Menü innerhalb der Guacamole Verbindung geändert werden.", "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", "HELP_LANGUAGE" : "Um die Spracheinstellungen von den Guacamole zu ändern, wählen Sie eine der verfügbaren Sprachen.", "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", "HELP_UPDATE_PASSWORD" : "Wenn Sie das Passwort ändern wollen, geben Sie das aktuelle und das gewünschte Passwort ein und klicken Sie auf \"Ändere Passwort\". Die Änderung wird sofort wirksam.", "INFO_PASSWORD_CHANGED" : "Passwort geändert.", "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Standard Eingabemethode", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Standard Mausemulationsmodus", "SECTION_HEADER_UPDATE_PASSWORD" : "Ändere Passwort" }, "SETTINGS_USERS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_NEW_USER" : "Neuer Benutzer", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "HELP_USERS" : "Klicke oder Tippe auf einen Benutzer um diesen zu verwalten. Abhänig von Ihrer Zugriffsebene können Benutzer hinzugefügt, gelöscht bzw. dessen Passwort geändert werden.", "SECTION_HEADER_USERS" : "Benutzer" }, "SETTINGS_SESSIONS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_DELETE" : "Beende Sitzung", "DIALOG_HEADER_CONFIRM_DELETE" : "Beende Sitzung", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", "HELP_SESSIONS" : "Alle aktiven Guacamole Sitzungen werden hier aufgelistet. Wenn Sie eine oder mehrere Sitzungen beenden wollen, wählen Sie diese Sitzung durch Aktivierung der nebenstehende Box und klicken auf \"Beende Sitzung\". Beendung einer Sitzung trennt den Benutzer von dessen Verbindung unverzüglich.", "INFO_NO_SESSIONS" : "Keine aktiven Sitzungen", "SECTION_HEADER_SESSIONS" : "Aktive Sitzungen", "TABLE_HEADER_SESSION_USERNAME" : "Benutzername", "TABLE_HEADER_SESSION_STARTDATE" : "Aktive seit", "TABLE_HEADER_SESSION_REMOTEHOST" : "Entfernter Host", "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Verbindungsname", "TEXT_CONFIRM_DELETE" : "Sind Sie sicher, dass alle ausgwählten Sitzungen beendet werden sollen? Die Benutzer dieser Sitzungen werden unverzüglich getrennt." }, "USER_MENU" : { "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME" } } guacamole-client-0.9.9/guacamole/src/main/webapp/WEB-INF/web.xml0100664 0001751 0001751 00000004344 12612226251 026007 0ustar00mjumpermjumper0000000 0000000 index.html guiceFilter com.google.inject.servlet.GuiceFilter guiceFilter /* org.glyptodon.guacamole.net.basic.BasicServletContextListener mp3 audio/mpeg ogg audio/ogg guacamole-client-0.9.9/guacamole/src/main/webapp/index.html0100664 0001751 0001751 00000005637 12645510255 025472 0ustar00mjumpermjumper0000000 0000000
guacamole-client-0.9.9/guacamole/src/main/webapp/lib/filesaver/filesaver.js0100664 0001751 0001751 00000014565 12612226251 030533 0ustar00mjumpermjumper0000000 0000000 /* FileSaver.js * A saveAs() FileSaver implementation. * 2013-01-23 * * By Eli Grey, http://eligrey.com * License: X11/MIT * See LICENSE.md */ /*global self */ /*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, plusplus: true */ /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ var saveAs = saveAs || (navigator.msSaveBlob && navigator.msSaveBlob.bind(navigator)) || (function(view) { "use strict"; var doc = view.document // only get URL when necessary in case BlobBuilder.js hasn't overridden it yet , get_URL = function() { return view.URL || view.webkitURL || view; } , URL = view.URL || view.webkitURL || view , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") , can_use_save_link = "download" in save_link , click = function(node) { var event = doc.createEvent("MouseEvents"); event.initMouseEvent( "click", true, false, view, 0, 0, 0, 0, 0 , false, false, false, false, 0, null ); node.dispatchEvent(event); } , webkit_req_fs = view.webkitRequestFileSystem , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem , throw_outside = function (ex) { (view.setImmediate || view.setTimeout)(function() { throw ex; }, 0); } , force_saveable_type = "application/octet-stream" , fs_min_size = 0 , deletion_queue = [] , process_deletion_queue = function() { var i = deletion_queue.length; while (i--) { var file = deletion_queue[i]; if (typeof file === "string") { // file is an object URL URL.revokeObjectURL(file); } else { // file is a File file.remove(); } } deletion_queue.length = 0; // clear queue } , dispatch = function(filesaver, event_types, event) { event_types = [].concat(event_types); var i = event_types.length; while (i--) { var listener = filesaver["on" + event_types[i]]; if (typeof listener === "function") { try { listener.call(filesaver, event || filesaver); } catch (ex) { throw_outside(ex); } } } } , FileSaver = function(blob, name) { // First try a.download, then web filesystem, then object URLs var filesaver = this , type = blob.type , blob_changed = false , object_url , target_view , get_object_url = function() { var object_url = get_URL().createObjectURL(blob); deletion_queue.push(object_url); return object_url; } , dispatch_all = function() { dispatch(filesaver, "writestart progress write writeend".split(" ")); } // on any filesys errors revert to saving with object URLs , fs_error = function() { // don't create more object URLs than needed if (blob_changed || !object_url) { object_url = get_object_url(blob); } if (target_view) { target_view.location.href = object_url; } else { window.open(object_url, "_blank"); } filesaver.readyState = filesaver.DONE; dispatch_all(); } , abortable = function(func) { return function() { if (filesaver.readyState !== filesaver.DONE) { return func.apply(this, arguments); } }; } , create_if_not_found = {create: true, exclusive: false} , slice ; filesaver.readyState = filesaver.INIT; if (!name) { name = "download"; } if (can_use_save_link) { object_url = get_object_url(blob); save_link.href = object_url; save_link.download = name; click(save_link); filesaver.readyState = filesaver.DONE; dispatch_all(); return; } // Object and web filesystem URLs have a problem saving in Google Chrome when // viewed in a tab, so I force save with application/octet-stream // http://code.google.com/p/chromium/issues/detail?id=91158 if (view.chrome && type && type !== force_saveable_type) { slice = blob.slice || blob.webkitSlice; blob = slice.call(blob, 0, blob.size, force_saveable_type); blob_changed = true; } // Since I can't be sure that the guessed media type will trigger a download // in WebKit, I append .download to the filename. // https://bugs.webkit.org/show_bug.cgi?id=65440 if (webkit_req_fs && name !== "download") { name += ".download"; } if (type === force_saveable_type || webkit_req_fs) { target_view = view; } if (!req_fs) { fs_error(); return; } fs_min_size += blob.size; req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) { fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) { var save = function() { dir.getFile(name, create_if_not_found, abortable(function(file) { file.createWriter(abortable(function(writer) { writer.onwriteend = function(event) { target_view.location.href = file.toURL(); deletion_queue.push(file); filesaver.readyState = filesaver.DONE; dispatch(filesaver, "writeend", event); }; writer.onerror = function() { var error = writer.error; if (error.code !== error.ABORT_ERR) { fs_error(); } }; "writestart progress write abort".split(" ").forEach(function(event) { writer["on" + event] = filesaver["on" + event]; }); writer.write(blob); filesaver.abort = function() { writer.abort(); filesaver.readyState = filesaver.DONE; }; filesaver.readyState = filesaver.WRITING; }), fs_error); }), fs_error); }; dir.getFile(name, {create: false}, abortable(function(file) { // delete file if it already exists file.remove(); save(); }), abortable(function(ex) { if (ex.code === ex.NOT_FOUND_ERR) { save(); } else { fs_error(); } })); }), fs_error); }), fs_error); } , FS_proto = FileSaver.prototype , saveAs = function(blob, name) { return new FileSaver(blob, name); } ; FS_proto.abort = function() { var filesaver = this; filesaver.readyState = filesaver.DONE; dispatch(filesaver, "abort"); }; FS_proto.readyState = FS_proto.INIT = 0; FS_proto.WRITING = 1; FS_proto.DONE = 2; FS_proto.error = FS_proto.onwritestart = FS_proto.onprogress = FS_proto.onwrite = FS_proto.onabort = FS_proto.onerror = FS_proto.onwriteend = null; view.addEventListener("unload", process_deletion_queue, false); return saveAs; }(self)); guacamole-client-0.9.9/guacamole/src/main/webapp/lib/filesaver/LICENSE.md0100664 0001751 0001751 00000002227 12612226251 027611 0ustar00mjumpermjumper0000000 0000000 This software is licensed under the MIT/X11 license. MIT/X11 license --------------- Copyright © 2011 [Eli Grey][1]. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. [1]: http://eligrey.comguacamole-client-0.9.9/guacamole/src/main/webapp/lib/angular-module-shim/angular-module-shim.js0100664 0001751 0001751 00000001406 12612226251 034305 0ustar00mjumpermjumper0000000 0000000 (function(angular) { 'use strict'; if ( !angular ) { throw new Error('angular-module-shim: Missing Angular'); } var origFn = angular.module; var hash = {}; angular.module = function(name,requires,configFn) { var requires = requires || []; var registered = hash[name]; var module; if ( registered ) { module = origFn(name); module.requires.push.apply(module.requires,requires); // Register the config function if it exists. if (configFn) { module.config(configFn); } } else { hash[name] = true; module = origFn(name,requires,configFn); } return module; }; })(window.angular); guacamole-client-0.9.9/guacamole/src/main/webapp/lib/angular-module-shim/LICENSE0100664 0001751 0001751 00000002066 12612226251 031105 0ustar00mjumpermjumper0000000 0000000 The MIT License (MIT) Copyright (c) 2014 Jed Richards Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/pl.js0100664 0001751 0001751 00000000632 12612226251 031270 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.pl = function (n) { if (n == 1) { return 'one'; } if ((n % 10) >= 2 && (n % 10) <= 4 && ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { return 'few'; } if ((n % 10) === 0 || n != 1 && (n % 10) == 1 || ((n % 10) >= 5 && (n % 10) <= 9 || (n % 100) >= 12 && (n % 100) <= 14) && n == Math.floor(n)) { return 'many'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/lv.js0100664 0001751 0001751 00000000246 12612226251 031277 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.lv = function (n) { if (n === 0) { return 'zero'; } if ((n % 10) == 1 && (n % 100) != 11) { return 'one'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/am.js0100664 0001751 0001751 00000000156 12612226251 031253 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.am = function(n) { if (n === 0 || n == 1) { return 'one'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/nl.js0100664 0001751 0001751 00000000151 12612226251 031262 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.nl = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/ja.js0100664 0001751 0001751 00000000100 12612226251 031235 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.ja = function ( n ) { return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/id.js0100664 0001751 0001751 00000000075 12612226251 031252 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.id = function(n) { return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/sw.js0100664 0001751 0001751 00000000151 12612226251 031302 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.sw = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/el.js0100664 0001751 0001751 00000000151 12612226251 031251 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.el = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/th.js0100664 0001751 0001751 00000000100 12612226251 031256 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.th = function ( n ) { return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/is.js0100664 0001751 0001751 00000000151 12612226251 031264 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.is = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/es.js0100664 0001751 0001751 00000000151 12612226251 031260 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.es = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/mo.js0100664 0001751 0001751 00000000326 12612226251 031270 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.mo = function (n) { if (n == 1) { return 'one'; } if (n === 0 || n != 1 && (n % 100) >= 1 && (n % 100) <= 19 && n == Math.floor(n)) { return 'few'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/eu.js0100664 0001751 0001751 00000000151 12612226251 031262 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.eu = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/zh.js0100664 0001751 0001751 00000000100 12612226251 031264 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.zh = function ( n ) { return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/sr.js0100664 0001751 0001751 00000000625 12612226251 031303 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.sr = function (n) { if ((n % 10) == 1 && (n % 100) != 11) { return 'one'; } if ((n % 10) >= 2 && (n % 10) <= 4 && ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { return 'few'; } if ((n % 10) === 0 || ((n % 10) >= 5 && (n % 10) <= 9) || ((n % 100) >= 11 && (n % 100) <= 14) && n == Math.floor(n)) { return 'many'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/ar.js0100664 0001751 0001751 00000000541 12612226251 031256 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.ar = function(n) { if (n === 0) { return 'zero'; } if (n == 1) { return 'one'; } if (n == 2) { return 'two'; } if ((n % 100) >= 3 && (n % 100) <= 10 && n == Math.floor(n)) { return 'few'; } if ((n % 100) >= 11 && (n % 100) <= 99 && n == Math.floor(n)) { return 'many'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/da.js0100664 0001751 0001751 00000000151 12612226251 031235 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.da = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/te.js0100664 0001751 0001751 00000000151 12612226251 031261 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.te = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/ms.js0100664 0001751 0001751 00000000100 12612226251 031262 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.ms = function ( n ) { return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/sk.js0100664 0001751 0001751 00000000236 12612226251 031272 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.sk = function (n) { if (n == 1) { return 'one'; } if (n == 2 || n == 3 || n == 4) { return 'few'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/tr.js0100664 0001751 0001751 00000000075 12612226251 031303 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.tr = function(n) { return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/or.js0100664 0001751 0001751 00000000151 12612226251 031271 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.or = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/lag.js0100664 0001751 0001751 00000000225 12612226251 031416 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.lag = function (n) { if (n === 0) { return 'zero'; } if (n > 0 && n < 2) { return 'one'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/ca.js0100664 0001751 0001751 00000000151 12612226251 031234 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.ca = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/gsw.js0100664 0001751 0001751 00000000152 12612226251 031452 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.gsw = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/et.js0100664 0001751 0001751 00000000151 12612226251 031261 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.et = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/gu.js0100664 0001751 0001751 00000000151 12612226251 031264 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.gu = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/no.js0100664 0001751 0001751 00000000151 12612226251 031265 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.no = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/vi.js0100664 0001751 0001751 00000000100 12612226251 031261 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.vi = function ( n ) { return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/sl.js0100664 0001751 0001751 00000000332 12612226251 031270 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.sl = function (n) { if ((n % 100) == 1) { return 'one'; } if ((n % 100) == 2) { return 'two'; } if ((n % 100) == 3 || (n % 100) == 4) { return 'few'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/ro.js0100664 0001751 0001751 00000000326 12612226251 031275 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.ro = function (n) { if (n == 1) { return 'one'; } if (n === 0 || n != 1 && (n % 100) >= 1 && (n % 100) <= 19 && n == Math.floor(n)) { return 'few'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/in.js0100664 0001751 0001751 00000000100 12612226251 031251 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale["in"] = function(n) { return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/hi.js0100664 0001751 0001751 00000000156 12612226251 031256 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.hi = function(n) { if (n === 0 || n == 1) { return 'one'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/fr.js0100664 0001751 0001751 00000000155 12612226251 031264 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.fr = function (n) { if (n >= 0 && n < 2) { return 'one'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/ga.js0100664 0001751 0001751 00000000212 12612226251 031236 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.ga = function (n) { if (n == 1) { return 'one'; } if (n == 2) { return 'two'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/pt.js0100664 0001751 0001751 00000000151 12612226251 031274 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.pt = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/iw.js0100664 0001751 0001751 00000000151 12612226251 031270 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.iw = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/tl.js0100664 0001751 0001751 00000000156 12612226251 031275 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.tl = function(n) { if (n === 0 || n == 1) { return 'one'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/sv.js0100664 0001751 0001751 00000000151 12612226251 031301 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.sv = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/he.js0100664 0001751 0001751 00000000151 12612226251 031245 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.he = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/bn.js0100664 0001751 0001751 00000000151 12612226251 031250 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.bn = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/br.js0100664 0001751 0001751 00000000377 12612226251 031266 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.br = function (n) { if (n === 0) { return 'zero'; } if (n == 1) { return 'one'; } if (n == 2) { return 'two'; } if (n == 3) { return 'few'; } if (n == 6) { return 'many'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/af.js0100664 0001751 0001751 00000000151 12612226251 031237 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.af = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/mk.js0100664 0001751 0001751 00000000166 12612226251 031266 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.mk = function (n) { if ((n % 10) == 1 && n != 11) { return 'one'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/gl.js0100664 0001751 0001751 00000000151 12612226251 031253 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.gl = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/ru.js0100664 0001751 0001751 00000000625 12612226251 031305 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.ru = function (n) { if ((n % 10) == 1 && (n % 100) != 11) { return 'one'; } if ((n % 10) >= 2 && (n % 10) <= 4 && ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { return 'few'; } if ((n % 10) === 0 || ((n % 10) >= 5 && (n % 10) <= 9) || ((n % 100) >= 11 && (n % 100) <= 14) && n == Math.floor(n)) { return 'many'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/sq.js0100664 0001751 0001751 00000000151 12612226251 031274 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.sq = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/fa.js0100664 0001751 0001751 00000000100 12612226251 031231 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.fa = function ( n ) { return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/cy.js0100664 0001751 0001751 00000000377 12612226251 031276 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.cy = function (n) { if (n === 0) { return 'zero'; } if (n == 1) { return 'one'; } if (n == 2) { return 'two'; } if (n == 3) { return 'few'; } if (n == 6) { return 'many'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/uk.js0100664 0001751 0001751 00000000625 12612226251 031276 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.uk = function (n) { if ((n % 10) == 1 && (n % 100) != 11) { return 'one'; } if ((n % 10) >= 2 && (n % 10) <= 4 && ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { return 'few'; } if ((n % 10) === 0 || ((n % 10) >= 5 && (n % 10) <= 9) || ((n % 100) >= 11 && (n % 100) <= 14) && n == Math.floor(n)) { return 'many'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/shi.js0100664 0001751 0001751 00000000265 12612226251 031442 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.shi = function(n) { if (n >= 0 && n <= 1) { return 'one'; } if (n >= 2 && n <= 10 && n == Math.floor(n)) { return 'few'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/kn.js0100664 0001751 0001751 00000000100 12612226251 031253 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.kn = function ( n ) { return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/de.js0100664 0001751 0001751 00000000151 12612226251 031241 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.de = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/fil.js0100664 0001751 0001751 00000000157 12612226251 031431 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.fil = function(n) { if (n === 0 || n == 1) { return 'one'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/ln.js0100664 0001751 0001751 00000000156 12612226251 031267 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.ln = function(n) { if (n === 0 || n == 1) { return 'one'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/mt.js0100664 0001751 0001751 00000000440 12612226251 031272 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.mt = function (n) { if (n == 1) { return 'one'; } if (n === 0 || ((n % 100) >= 2 && (n % 100) <= 4 && n == Math.floor(n))) { return 'few'; } if ((n % 100) >= 11 && (n % 100) <= 19 && n == Math.floor(n)) { return 'many'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/hr.js0100664 0001751 0001751 00000000625 12612226251 031270 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.hr = function (n) { if ((n % 10) == 1 && (n % 100) != 11) { return 'one'; } if ((n % 10) >= 2 && (n % 10) <= 4 && ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { return 'few'; } if ((n % 10) === 0 || ((n % 10) >= 5 && (n % 10) <= 9) || ((n % 100) >= 11 && (n % 100) <= 14) && n == Math.floor(n)) { return 'many'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/mr.js0100664 0001751 0001751 00000000151 12612226251 031267 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.mr = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/bg.js0100664 0001751 0001751 00000000151 12612226251 031241 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.bg = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/it.js0100664 0001751 0001751 00000000151 12612226251 031265 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.it = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/hu.js0100664 0001751 0001751 00000000075 12612226251 031272 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.hu = function(n) { return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/ur.js0100664 0001751 0001751 00000000151 12612226251 031277 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.ur = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/ko.js0100664 0001751 0001751 00000000100 12612226251 031254 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.ko = function ( n ) { return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/cs.js0100664 0001751 0001751 00000000236 12612226251 031262 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.cs = function (n) { if (n == 1) { return 'one'; } if (n == 2 || n == 3 || n == 4) { return 'few'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/ml.js0100664 0001751 0001751 00000000151 12612226251 031261 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.ml = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/ta.js0100664 0001751 0001751 00000000151 12612226251 031255 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.ta = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/lt.js0100664 0001751 0001751 00000000421 12612226251 031270 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.lt = function (n) { if ((n % 10) == 1 && ((n % 100) < 11 || (n % 100) > 19)) { return 'one'; } if ((n % 10) >= 2 && (n % 10) <= 9 && ((n % 100) < 11 || (n % 100) > 19) && n == Math.floor(n)) { return 'few'; } return 'other'; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/en.js0100664 0001751 0001751 00000000151 12612226251 031253 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.en = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/locale/fi.js0100664 0001751 0001751 00000000151 12612226251 031247 0ustar00mjumpermjumper0000000 0000000 MessageFormat.locale.fi = function ( n ) { if ( n === 1 ) { return "one"; } return "other"; }; guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/LICENSE0100664 0001751 0001751 00000000762 12612226251 030071 0ustar00mjumpermjumper0000000 0000000 DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE Version 2, December 2004 Copyright (C) 2004 Sam Hocevar Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. You just DO WHAT THE FUCK YOU WANT TO. guacamole-client-0.9.9/guacamole/src/main/webapp/lib/messageformat/messageformat.js0100664 0001751 0001751 00000140500 12612226251 032252 0ustar00mjumpermjumper0000000 0000000 /** * messageformat.js * * ICU PluralFormat + SelectFormat for JavaScript * * @author Alex Sexton - @SlexAxton * @version 0.1.7 * @license WTFPL * @contributor_license Dojo CLA */ (function ( root ) { // Create the contructor function function MessageFormat ( locale, pluralFunc ) { var fallbackLocale; if ( locale && pluralFunc ) { MessageFormat.locale[ locale ] = pluralFunc; } // Defaults fallbackLocale = locale = locale || "en"; pluralFunc = pluralFunc || MessageFormat.locale[ fallbackLocale = MessageFormat.Utils.getFallbackLocale( locale ) ]; if ( ! pluralFunc ) { throw new Error( "Plural Function not found for locale: " + locale ); } // Own Properties this.pluralFunc = pluralFunc; this.locale = locale; this.fallbackLocale = fallbackLocale; } // Set up the locales object. Add in english by default MessageFormat.locale = { "en" : function ( n ) { if ( n === 1 ) { return "one"; } return "other"; } }; // Build out our basic SafeString type // more or less stolen from Handlebars by @wycats MessageFormat.SafeString = function( string ) { this.string = string; }; MessageFormat.SafeString.prototype.toString = function () { return this.string.toString(); }; MessageFormat.Utils = { numSub : function ( string, key, depth ) { // make sure that it's not an escaped octothorpe return string.replace( /^#|[^\\]#/g, function (m) { var prefix = m && m.length === 2 ? m.charAt(0) : ''; return prefix + '" + (function(){ var x = ' + key+';\nif( isNaN(x) ){\nthrow new Error("MessageFormat: `"+lastkey_'+depth+'+"` isnt a number.");\n}\nreturn x;\n})() + "'; }); }, escapeExpression : function (string) { var escape = { "\n": "\\n", "\"": '\\"' }, badChars = /[\n"]/g, possible = /[\n"]/, escapeChar = function(chr) { return escape[chr] || "&"; }; // Don't escape SafeStrings, since they're already safe if ( string instanceof MessageFormat.SafeString ) { return string.toString(); } else if ( string === null || string === false ) { return ""; } if ( ! possible.test( string ) ) { return string; } return string.replace( badChars, escapeChar ); }, getFallbackLocale: function( locale ) { var tagSeparator = locale.indexOf("-") >= 0 ? "-" : "_"; // Lets just be friends, fallback through the language tags while ( ! MessageFormat.locale.hasOwnProperty( locale ) ) { locale = locale.substring(0, locale.lastIndexOf( tagSeparator )); if (locale.length === 0) { return null; } } return locale; } }; // This is generated and pulled in for browsers. var mparser = (function(){ /* * Generated by PEG.js 0.7.0. * * http://pegjs.majda.cz/ */ function quote(s) { /* * ECMA-262, 5th ed., 7.8.4: All characters may appear literally in a * string literal except for the closing quote character, backslash, * carriage return, line separator, paragraph separator, and line feed. * Any character may appear in the form of an escape sequence. * * For portability, we also escape escape all control and non-ASCII * characters. Note that "\0" and "\v" escape sequences are not used * because JSHint does not like the first and IE the second. */ return '"' + s .replace(/\\/g, '\\\\') // backslash .replace(/"/g, '\\"') // closing quote character .replace(/\x08/g, '\\b') // backspace .replace(/\t/g, '\\t') // horizontal tab .replace(/\n/g, '\\n') // line feed .replace(/\f/g, '\\f') // form feed .replace(/\r/g, '\\r') // carriage return .replace(/[\x00-\x07\x0B\x0E-\x1F\x80-\uFFFF]/g, escape) + '"'; } var result = { /* * Parses the input with a generated parser. If the parsing is successfull, * returns a value explicitly or implicitly specified by the grammar from * which the parser was generated (see |PEG.buildParser|). If the parsing is * unsuccessful, throws |PEG.parser.SyntaxError| describing the error. */ parse: function(input, startRule) { var parseFunctions = { "start": parse_start, "messageFormatPattern": parse_messageFormatPattern, "messageFormatPatternRight": parse_messageFormatPatternRight, "messageFormatElement": parse_messageFormatElement, "elementFormat": parse_elementFormat, "pluralStyle": parse_pluralStyle, "selectStyle": parse_selectStyle, "pluralFormatPattern": parse_pluralFormatPattern, "offsetPattern": parse_offsetPattern, "selectFormatPattern": parse_selectFormatPattern, "pluralForms": parse_pluralForms, "stringKey": parse_stringKey, "string": parse_string, "id": parse_id, "chars": parse_chars, "char": parse_char, "digits": parse_digits, "hexDigit": parse_hexDigit, "_": parse__, "whitespace": parse_whitespace }; if (startRule !== undefined) { if (parseFunctions[startRule] === undefined) { throw new Error("Invalid rule name: " + quote(startRule) + "."); } } else { startRule = "start"; } var pos = 0; var reportFailures = 0; var rightmostFailuresPos = 0; var rightmostFailuresExpected = []; function padLeft(input, padding, length) { var result = input; var padLength = length - input.length; for (var i = 0; i < padLength; i++) { result = padding + result; } return result; } function escape(ch) { var charCode = ch.charCodeAt(0); var escapeChar; var length; if (charCode <= 0xFF) { escapeChar = 'x'; length = 2; } else { escapeChar = 'u'; length = 4; } return '\\' + escapeChar + padLeft(charCode.toString(16).toUpperCase(), '0', length); } function matchFailed(failure) { if (pos < rightmostFailuresPos) { return; } if (pos > rightmostFailuresPos) { rightmostFailuresPos = pos; rightmostFailuresExpected = []; } rightmostFailuresExpected.push(failure); } function parse_start() { var result0; var pos0; pos0 = pos; result0 = parse_messageFormatPattern(); if (result0 !== null) { result0 = (function(offset, messageFormatPattern) { return { type: "program", program: messageFormatPattern }; })(pos0, result0); } if (result0 === null) { pos = pos0; } return result0; } function parse_messageFormatPattern() { var result0, result1, result2; var pos0, pos1; pos0 = pos; pos1 = pos; result0 = parse_string(); if (result0 !== null) { result1 = []; result2 = parse_messageFormatPatternRight(); while (result2 !== null) { result1.push(result2); result2 = parse_messageFormatPatternRight(); } if (result1 !== null) { result0 = [result0, result1]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, s1, inner) { var st = []; if ( s1 && s1.val ) { st.push( s1 ); } for( var i in inner ){ if ( inner.hasOwnProperty( i ) ) { st.push( inner[ i ] ); } } return { type: 'messageFormatPattern', statements: st }; })(pos0, result0[0], result0[1]); } if (result0 === null) { pos = pos0; } return result0; } function parse_messageFormatPatternRight() { var result0, result1, result2, result3, result4, result5; var pos0, pos1; pos0 = pos; pos1 = pos; if (input.charCodeAt(pos) === 123) { result0 = "{"; pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("\"{\""); } } if (result0 !== null) { result1 = parse__(); if (result1 !== null) { result2 = parse_messageFormatElement(); if (result2 !== null) { result3 = parse__(); if (result3 !== null) { if (input.charCodeAt(pos) === 125) { result4 = "}"; pos++; } else { result4 = null; if (reportFailures === 0) { matchFailed("\"}\""); } } if (result4 !== null) { result5 = parse_string(); if (result5 !== null) { result0 = [result0, result1, result2, result3, result4, result5]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, mfe, s1) { var res = []; if ( mfe ) { res.push(mfe); } if ( s1 && s1.val ) { res.push( s1 ); } return { type: "messageFormatPatternRight", statements : res }; })(pos0, result0[2], result0[5]); } if (result0 === null) { pos = pos0; } return result0; } function parse_messageFormatElement() { var result0, result1, result2; var pos0, pos1, pos2; pos0 = pos; pos1 = pos; result0 = parse_id(); if (result0 !== null) { pos2 = pos; if (input.charCodeAt(pos) === 44) { result1 = ","; pos++; } else { result1 = null; if (reportFailures === 0) { matchFailed("\",\""); } } if (result1 !== null) { result2 = parse_elementFormat(); if (result2 !== null) { result1 = [result1, result2]; } else { result1 = null; pos = pos2; } } else { result1 = null; pos = pos2; } result1 = result1 !== null ? result1 : ""; if (result1 !== null) { result0 = [result0, result1]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, argIdx, efmt) { var res = { type: "messageFormatElement", argumentIndex: argIdx }; if ( efmt && efmt.length ) { res.elementFormat = efmt[1]; } else { res.output = true; } return res; })(pos0, result0[0], result0[1]); } if (result0 === null) { pos = pos0; } return result0; } function parse_elementFormat() { var result0, result1, result2, result3, result4, result5, result6; var pos0, pos1; pos0 = pos; pos1 = pos; result0 = parse__(); if (result0 !== null) { if (input.substr(pos, 6) === "plural") { result1 = "plural"; pos += 6; } else { result1 = null; if (reportFailures === 0) { matchFailed("\"plural\""); } } if (result1 !== null) { result2 = parse__(); if (result2 !== null) { if (input.charCodeAt(pos) === 44) { result3 = ","; pos++; } else { result3 = null; if (reportFailures === 0) { matchFailed("\",\""); } } if (result3 !== null) { result4 = parse__(); if (result4 !== null) { result5 = parse_pluralStyle(); if (result5 !== null) { result6 = parse__(); if (result6 !== null) { result0 = [result0, result1, result2, result3, result4, result5, result6]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, t, s) { return { type : "elementFormat", key : t, val : s.val }; })(pos0, result0[1], result0[5]); } if (result0 === null) { pos = pos0; } if (result0 === null) { pos0 = pos; pos1 = pos; result0 = parse__(); if (result0 !== null) { if (input.substr(pos, 6) === "select") { result1 = "select"; pos += 6; } else { result1 = null; if (reportFailures === 0) { matchFailed("\"select\""); } } if (result1 !== null) { result2 = parse__(); if (result2 !== null) { if (input.charCodeAt(pos) === 44) { result3 = ","; pos++; } else { result3 = null; if (reportFailures === 0) { matchFailed("\",\""); } } if (result3 !== null) { result4 = parse__(); if (result4 !== null) { result5 = parse_selectStyle(); if (result5 !== null) { result6 = parse__(); if (result6 !== null) { result0 = [result0, result1, result2, result3, result4, result5, result6]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, t, s) { return { type : "elementFormat", key : t, val : s.val }; })(pos0, result0[1], result0[5]); } if (result0 === null) { pos = pos0; } } return result0; } function parse_pluralStyle() { var result0; var pos0; pos0 = pos; result0 = parse_pluralFormatPattern(); if (result0 !== null) { result0 = (function(offset, pfp) { return { type: "pluralStyle", val: pfp }; })(pos0, result0); } if (result0 === null) { pos = pos0; } return result0; } function parse_selectStyle() { var result0; var pos0; pos0 = pos; result0 = parse_selectFormatPattern(); if (result0 !== null) { result0 = (function(offset, sfp) { return { type: "selectStyle", val: sfp }; })(pos0, result0); } if (result0 === null) { pos = pos0; } return result0; } function parse_pluralFormatPattern() { var result0, result1, result2; var pos0, pos1; pos0 = pos; pos1 = pos; result0 = parse_offsetPattern(); result0 = result0 !== null ? result0 : ""; if (result0 !== null) { result1 = []; result2 = parse_pluralForms(); while (result2 !== null) { result1.push(result2); result2 = parse_pluralForms(); } if (result1 !== null) { result0 = [result0, result1]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, op, pf) { var res = { type: "pluralFormatPattern", pluralForms: pf }; if ( op ) { res.offset = op; } else { res.offset = 0; } return res; })(pos0, result0[0], result0[1]); } if (result0 === null) { pos = pos0; } return result0; } function parse_offsetPattern() { var result0, result1, result2, result3, result4, result5, result6; var pos0, pos1; pos0 = pos; pos1 = pos; result0 = parse__(); if (result0 !== null) { if (input.substr(pos, 6) === "offset") { result1 = "offset"; pos += 6; } else { result1 = null; if (reportFailures === 0) { matchFailed("\"offset\""); } } if (result1 !== null) { result2 = parse__(); if (result2 !== null) { if (input.charCodeAt(pos) === 58) { result3 = ":"; pos++; } else { result3 = null; if (reportFailures === 0) { matchFailed("\":\""); } } if (result3 !== null) { result4 = parse__(); if (result4 !== null) { result5 = parse_digits(); if (result5 !== null) { result6 = parse__(); if (result6 !== null) { result0 = [result0, result1, result2, result3, result4, result5, result6]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, d) { return d; })(pos0, result0[5]); } if (result0 === null) { pos = pos0; } return result0; } function parse_selectFormatPattern() { var result0, result1; var pos0; pos0 = pos; result0 = []; result1 = parse_pluralForms(); while (result1 !== null) { result0.push(result1); result1 = parse_pluralForms(); } if (result0 !== null) { result0 = (function(offset, pf) { return { type: "selectFormatPattern", pluralForms: pf }; })(pos0, result0); } if (result0 === null) { pos = pos0; } return result0; } function parse_pluralForms() { var result0, result1, result2, result3, result4, result5, result6, result7; var pos0, pos1; pos0 = pos; pos1 = pos; result0 = parse__(); if (result0 !== null) { result1 = parse_stringKey(); if (result1 !== null) { result2 = parse__(); if (result2 !== null) { if (input.charCodeAt(pos) === 123) { result3 = "{"; pos++; } else { result3 = null; if (reportFailures === 0) { matchFailed("\"{\""); } } if (result3 !== null) { result4 = parse__(); if (result4 !== null) { result5 = parse_messageFormatPattern(); if (result5 !== null) { result6 = parse__(); if (result6 !== null) { if (input.charCodeAt(pos) === 125) { result7 = "}"; pos++; } else { result7 = null; if (reportFailures === 0) { matchFailed("\"}\""); } } if (result7 !== null) { result0 = [result0, result1, result2, result3, result4, result5, result6, result7]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, k, mfp) { return { type: "pluralForms", key: k, val: mfp }; })(pos0, result0[1], result0[5]); } if (result0 === null) { pos = pos0; } return result0; } function parse_stringKey() { var result0, result1; var pos0, pos1; pos0 = pos; result0 = parse_id(); if (result0 !== null) { result0 = (function(offset, i) { return i; })(pos0, result0); } if (result0 === null) { pos = pos0; } if (result0 === null) { pos0 = pos; pos1 = pos; if (input.charCodeAt(pos) === 61) { result0 = "="; pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("\"=\""); } } if (result0 !== null) { result1 = parse_digits(); if (result1 !== null) { result0 = [result0, result1]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, d) { return d; })(pos0, result0[1]); } if (result0 === null) { pos = pos0; } } return result0; } function parse_string() { var result0, result1, result2, result3, result4; var pos0, pos1, pos2; pos0 = pos; pos1 = pos; result0 = parse__(); if (result0 !== null) { result1 = []; pos2 = pos; result2 = parse__(); if (result2 !== null) { result3 = parse_chars(); if (result3 !== null) { result4 = parse__(); if (result4 !== null) { result2 = [result2, result3, result4]; } else { result2 = null; pos = pos2; } } else { result2 = null; pos = pos2; } } else { result2 = null; pos = pos2; } while (result2 !== null) { result1.push(result2); pos2 = pos; result2 = parse__(); if (result2 !== null) { result3 = parse_chars(); if (result3 !== null) { result4 = parse__(); if (result4 !== null) { result2 = [result2, result3, result4]; } else { result2 = null; pos = pos2; } } else { result2 = null; pos = pos2; } } else { result2 = null; pos = pos2; } } if (result1 !== null) { result0 = [result0, result1]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, ws, s) { var tmp = []; for( var i = 0; i < s.length; ++i ) { for( var j = 0; j < s[ i ].length; ++j ) { tmp.push(s[i][j]); } } return { type: "string", val: ws + tmp.join('') }; })(pos0, result0[0], result0[1]); } if (result0 === null) { pos = pos0; } return result0; } function parse_id() { var result0, result1, result2, result3; var pos0, pos1; pos0 = pos; pos1 = pos; result0 = parse__(); if (result0 !== null) { if (/^[a-zA-Z$_]/.test(input.charAt(pos))) { result1 = input.charAt(pos); pos++; } else { result1 = null; if (reportFailures === 0) { matchFailed("[a-zA-Z$_]"); } } if (result1 !== null) { result2 = []; if (/^[^ \t\n\r,.+={}]/.test(input.charAt(pos))) { result3 = input.charAt(pos); pos++; } else { result3 = null; if (reportFailures === 0) { matchFailed("[^ \\t\\n\\r,.+={}]"); } } while (result3 !== null) { result2.push(result3); if (/^[^ \t\n\r,.+={}]/.test(input.charAt(pos))) { result3 = input.charAt(pos); pos++; } else { result3 = null; if (reportFailures === 0) { matchFailed("[^ \\t\\n\\r,.+={}]"); } } } if (result2 !== null) { result3 = parse__(); if (result3 !== null) { result0 = [result0, result1, result2, result3]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, s1, s2) { return s1 + (s2 ? s2.join('') : ''); })(pos0, result0[1], result0[2]); } if (result0 === null) { pos = pos0; } return result0; } function parse_chars() { var result0, result1; var pos0; pos0 = pos; result1 = parse_char(); if (result1 !== null) { result0 = []; while (result1 !== null) { result0.push(result1); result1 = parse_char(); } } else { result0 = null; } if (result0 !== null) { result0 = (function(offset, chars) { return chars.join(''); })(pos0, result0); } if (result0 === null) { pos = pos0; } return result0; } function parse_char() { var result0, result1, result2, result3, result4; var pos0, pos1; pos0 = pos; if (/^[^{}\\\0-\x1F \t\n\r]/.test(input.charAt(pos))) { result0 = input.charAt(pos); pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("[^{}\\\\\\0-\\x1F \\t\\n\\r]"); } } if (result0 !== null) { result0 = (function(offset, x) { return x; })(pos0, result0); } if (result0 === null) { pos = pos0; } if (result0 === null) { pos0 = pos; if (input.substr(pos, 2) === "\\#") { result0 = "\\#"; pos += 2; } else { result0 = null; if (reportFailures === 0) { matchFailed("\"\\\\#\""); } } if (result0 !== null) { result0 = (function(offset) { return "\\#"; })(pos0); } if (result0 === null) { pos = pos0; } if (result0 === null) { pos0 = pos; if (input.substr(pos, 2) === "\\{") { result0 = "\\{"; pos += 2; } else { result0 = null; if (reportFailures === 0) { matchFailed("\"\\\\{\""); } } if (result0 !== null) { result0 = (function(offset) { return "\u007B"; })(pos0); } if (result0 === null) { pos = pos0; } if (result0 === null) { pos0 = pos; if (input.substr(pos, 2) === "\\}") { result0 = "\\}"; pos += 2; } else { result0 = null; if (reportFailures === 0) { matchFailed("\"\\\\}\""); } } if (result0 !== null) { result0 = (function(offset) { return "\u007D"; })(pos0); } if (result0 === null) { pos = pos0; } if (result0 === null) { pos0 = pos; pos1 = pos; if (input.substr(pos, 2) === "\\u") { result0 = "\\u"; pos += 2; } else { result0 = null; if (reportFailures === 0) { matchFailed("\"\\\\u\""); } } if (result0 !== null) { result1 = parse_hexDigit(); if (result1 !== null) { result2 = parse_hexDigit(); if (result2 !== null) { result3 = parse_hexDigit(); if (result3 !== null) { result4 = parse_hexDigit(); if (result4 !== null) { result0 = [result0, result1, result2, result3, result4]; } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } } else { result0 = null; pos = pos1; } if (result0 !== null) { result0 = (function(offset, h1, h2, h3, h4) { return String.fromCharCode(parseInt("0x" + h1 + h2 + h3 + h4)); })(pos0, result0[1], result0[2], result0[3], result0[4]); } if (result0 === null) { pos = pos0; } } } } } return result0; } function parse_digits() { var result0, result1; var pos0; pos0 = pos; if (/^[0-9]/.test(input.charAt(pos))) { result1 = input.charAt(pos); pos++; } else { result1 = null; if (reportFailures === 0) { matchFailed("[0-9]"); } } if (result1 !== null) { result0 = []; while (result1 !== null) { result0.push(result1); if (/^[0-9]/.test(input.charAt(pos))) { result1 = input.charAt(pos); pos++; } else { result1 = null; if (reportFailures === 0) { matchFailed("[0-9]"); } } } } else { result0 = null; } if (result0 !== null) { result0 = (function(offset, ds) { return parseInt((ds.join('')), 10); })(pos0, result0); } if (result0 === null) { pos = pos0; } return result0; } function parse_hexDigit() { var result0; if (/^[0-9a-fA-F]/.test(input.charAt(pos))) { result0 = input.charAt(pos); pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("[0-9a-fA-F]"); } } return result0; } function parse__() { var result0, result1; var pos0; reportFailures++; pos0 = pos; result0 = []; result1 = parse_whitespace(); while (result1 !== null) { result0.push(result1); result1 = parse_whitespace(); } if (result0 !== null) { result0 = (function(offset, w) { return w.join(''); })(pos0, result0); } if (result0 === null) { pos = pos0; } reportFailures--; if (reportFailures === 0 && result0 === null) { matchFailed("whitespace"); } return result0; } function parse_whitespace() { var result0; if (/^[ \t\n\r]/.test(input.charAt(pos))) { result0 = input.charAt(pos); pos++; } else { result0 = null; if (reportFailures === 0) { matchFailed("[ \\t\\n\\r]"); } } return result0; } function cleanupExpected(expected) { expected.sort(); var lastExpected = null; var cleanExpected = []; for (var i = 0; i < expected.length; i++) { if (expected[i] !== lastExpected) { cleanExpected.push(expected[i]); lastExpected = expected[i]; } } return cleanExpected; } function computeErrorPosition() { /* * The first idea was to use |String.split| to break the input up to the * error position along newlines and derive the line and column from * there. However IE's |split| implementation is so broken that it was * enough to prevent it. */ var line = 1; var column = 1; var seenCR = false; for (var i = 0; i < Math.max(pos, rightmostFailuresPos); i++) { var ch = input.charAt(i); if (ch === "\n") { if (!seenCR) { line++; } column = 1; seenCR = false; } else if (ch === "\r" || ch === "\u2028" || ch === "\u2029") { line++; column = 1; seenCR = true; } else { column++; seenCR = false; } } return { line: line, column: column }; } var result = parseFunctions[startRule](); /* * The parser is now in one of the following three states: * * 1. The parser successfully parsed the whole input. * * - |result !== null| * - |pos === input.length| * - |rightmostFailuresExpected| may or may not contain something * * 2. The parser successfully parsed only a part of the input. * * - |result !== null| * - |pos < input.length| * - |rightmostFailuresExpected| may or may not contain something * * 3. The parser did not successfully parse any part of the input. * * - |result === null| * - |pos === 0| * - |rightmostFailuresExpected| contains at least one failure * * All code following this comment (including called functions) must * handle these states. */ if (result === null || pos !== input.length) { var offset = Math.max(pos, rightmostFailuresPos); var found = offset < input.length ? input.charAt(offset) : null; var errorPosition = computeErrorPosition(); throw new this.SyntaxError( cleanupExpected(rightmostFailuresExpected), found, offset, errorPosition.line, errorPosition.column ); } return result; }, /* Returns the parser source code. */ toSource: function() { return this._source; } }; /* Thrown when a parser encounters a syntax error. */ result.SyntaxError = function(expected, found, offset, line, column) { function buildMessage(expected, found) { var expectedHumanized, foundHumanized; switch (expected.length) { case 0: expectedHumanized = "end of input"; break; case 1: expectedHumanized = expected[0]; break; default: expectedHumanized = expected.slice(0, expected.length - 1).join(", ") + " or " + expected[expected.length - 1]; } foundHumanized = found ? quote(found) : "end of input"; return "Expected " + expectedHumanized + " but " + foundHumanized + " found."; } this.name = "SyntaxError"; this.expected = expected; this.found = found; this.message = buildMessage(expected, found); this.offset = offset; this.line = line; this.column = column; }; result.SyntaxError.prototype = Error.prototype; return result; })(); MessageFormat.prototype.parse = function () { // Bind to itself so error handling works return mparser.parse.apply( mparser, arguments ); }; MessageFormat.prototype.precompile = function ( ast ) { var self = this, needOther = false, fp = { begin: 'function(d){\nvar r = "";\n', end : "return r;\n}" }; function interpMFP ( ast, data ) { // Set some default data data = data || {}; var s = '', i, tmp, lastkeyname; switch ( ast.type ) { case 'program': return interpMFP( ast.program ); case 'messageFormatPattern': for ( i = 0; i < ast.statements.length; ++i ) { s += interpMFP( ast.statements[i], data ); } return fp.begin + s + fp.end; case 'messageFormatPatternRight': for ( i = 0; i < ast.statements.length; ++i ) { s += interpMFP( ast.statements[i], data ); } return s; case 'messageFormatElement': data.pf_count = data.pf_count || 0; s += 'if(!d){\nthrow new Error("MessageFormat: No data passed to function.");\n}\n'; if ( ast.output ) { s += 'r += d["' + ast.argumentIndex + '"];\n'; } else { lastkeyname = 'lastkey_'+(data.pf_count+1); s += 'var '+lastkeyname+' = "'+ast.argumentIndex+'";\n'; s += 'var k_'+(data.pf_count+1)+'=d['+lastkeyname+'];\n'; s += interpMFP( ast.elementFormat, data ); } return s; case 'elementFormat': if ( ast.key === 'select' ) { s += interpMFP( ast.val, data ); s += 'r += (pf_' + data.pf_count + '[ k_' + (data.pf_count+1) + ' ] || pf_'+data.pf_count+'[ "other" ])( d );\n'; } else if ( ast.key === 'plural' ) { s += interpMFP( ast.val, data ); s += 'if ( pf_'+(data.pf_count)+'[ k_'+(data.pf_count+1)+' + "" ] ) {\n'; s += 'r += pf_'+data.pf_count+'[ k_'+(data.pf_count+1)+' + "" ]( d ); \n'; s += '}\nelse {\n'; s += 'r += (pf_' + data.pf_count + '[ MessageFormat.locale["' + self.fallbackLocale + '"]( k_'+(data.pf_count+1)+' - off_'+(data.pf_count)+' ) ] || pf_'+data.pf_count+'[ "other" ] )( d );\n'; s += '}\n'; } return s; /* // Unreachable cases. case 'pluralStyle': case 'selectStyle':*/ case 'pluralFormatPattern': data.pf_count = data.pf_count || 0; s += 'var off_'+data.pf_count+' = '+ast.offset+';\n'; s += 'var pf_' + data.pf_count + ' = { \n'; needOther = true; // We're going to simultaneously check to make sure we hit the required 'other' option. for ( i = 0; i < ast.pluralForms.length; ++i ) { if ( ast.pluralForms[ i ].key === 'other' ) { needOther = false; } if ( tmp ) { s += ',\n'; } else{ tmp = 1; } s += '"' + ast.pluralForms[ i ].key + '" : ' + interpMFP( ast.pluralForms[ i ].val, (function(){ var res = JSON.parse(JSON.stringify(data)); res.pf_count++; return res; })() ); } s += '\n};\n'; if ( needOther ) { throw new Error("No 'other' form found in pluralFormatPattern " + data.pf_count); } return s; case 'selectFormatPattern': data.pf_count = data.pf_count || 0; s += 'var off_'+data.pf_count+' = 0;\n'; s += 'var pf_' + data.pf_count + ' = { \n'; needOther = true; for ( i = 0; i < ast.pluralForms.length; ++i ) { if ( ast.pluralForms[ i ].key === 'other' ) { needOther = false; } if ( tmp ) { s += ',\n'; } else{ tmp = 1; } s += '"' + ast.pluralForms[ i ].key + '" : ' + interpMFP( ast.pluralForms[ i ].val, (function(){ var res = JSON.parse( JSON.stringify( data ) ); res.pf_count++; return res; })() ); } s += '\n};\n'; if ( needOther ) { throw new Error("No 'other' form found in selectFormatPattern " + data.pf_count); } return s; /* // Unreachable case 'pluralForms': */ case 'string': return 'r += "' + MessageFormat.Utils.numSub( MessageFormat.Utils.escapeExpression( ast.val ), 'k_' + data.pf_count + ' - off_' + ( data.pf_count - 1 ), data.pf_count ) + '";\n'; default: throw new Error( 'Bad AST type: ' + ast.type ); } } return interpMFP( ast ); }; MessageFormat.prototype.compile = function ( message ) { return (new Function( 'MessageFormat', 'return ' + this.precompile( this.parse( message ) ) ))(MessageFormat); }; if (typeof exports !== 'undefined') { if (typeof module !== 'undefined' && module.exports) { exports = module.exports = MessageFormat; } exports.MessageFormat = MessageFormat; } else if (typeof define === 'function' && define.amd) { define(function() { return MessageFormat; }); } else { root['MessageFormat'] = MessageFormat; } })( this ); guacamole-client-0.9.9/guacamole/src/main/webapp/lib/blob/blob.js0100664 0001751 0001751 00000013212 12612226251 026413 0ustar00mjumpermjumper0000000 0000000 /* Blob.js * A Blob implementation. * 2014-07-24 * * By Eli Grey, http://eligrey.com * By Devin Samarin, https://github.com/dsamarin * License: X11/MIT * See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md */ /*global self, unescape */ /*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, plusplus: true */ /*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */ (function (view) { "use strict"; view.URL = view.URL || view.webkitURL; if (view.Blob && view.URL) { try { new Blob; return; } catch (e) {} } // Internally we use a BlobBuilder implementation to base Blob off of // in order to support older browsers that only have BlobBuilder var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) { var get_class = function(object) { return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1]; } , FakeBlobBuilder = function BlobBuilder() { this.data = []; } , FakeBlob = function Blob(data, type, encoding) { this.data = data; this.size = data.length; this.type = type; this.encoding = encoding; } , FBB_proto = FakeBlobBuilder.prototype , FB_proto = FakeBlob.prototype , FileReaderSync = view.FileReaderSync , FileException = function(type) { this.code = this[this.name = type]; } , file_ex_codes = ( "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR " + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR" ).split(" ") , file_ex_code = file_ex_codes.length , real_URL = view.URL || view.webkitURL || view , real_create_object_URL = real_URL.createObjectURL , real_revoke_object_URL = real_URL.revokeObjectURL , URL = real_URL , btoa = view.btoa , atob = view.atob , ArrayBuffer = view.ArrayBuffer , Uint8Array = view.Uint8Array , origin = /^[\w-]+:\/*\[?[\w\.:-]+\]?(?::[0-9]+)?/ ; FakeBlob.fake = FB_proto.fake = true; while (file_ex_code--) { FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1; } // Polyfill URL if (!real_URL.createObjectURL) { URL = view.URL = function(uri) { var uri_info = document.createElementNS("http://www.w3.org/1999/xhtml", "a") , uri_origin ; uri_info.href = uri; if (!("origin" in uri_info)) { if (uri_info.protocol.toLowerCase() === "data:") { uri_info.origin = null; } else { uri_origin = uri.match(origin); uri_info.origin = uri_origin && uri_origin[1]; } } return uri_info; }; } URL.createObjectURL = function(blob) { var type = blob.type , data_URI_header ; if (type === null) { type = "application/octet-stream"; } if (blob instanceof FakeBlob) { data_URI_header = "data:" + type; if (blob.encoding === "base64") { return data_URI_header + ";base64," + blob.data; } else if (blob.encoding === "URI") { return data_URI_header + "," + decodeURIComponent(blob.data); } if (btoa) { return data_URI_header + ";base64," + btoa(blob.data); } else { return data_URI_header + "," + encodeURIComponent(blob.data); } } else if (real_create_object_URL) { return real_create_object_URL.call(real_URL, blob); } }; URL.revokeObjectURL = function(object_URL) { if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) { real_revoke_object_URL.call(real_URL, object_URL); } }; FBB_proto.append = function(data/*, endings*/) { var bb = this.data; // decode data to a binary string if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) { var str = "" , buf = new Uint8Array(data) , i = 0 , buf_len = buf.length ; for (; i < buf_len; i++) { str += String.fromCharCode(buf[i]); } bb.push(str); } else if (get_class(data) === "Blob" || get_class(data) === "File") { if (FileReaderSync) { var fr = new FileReaderSync; bb.push(fr.readAsBinaryString(data)); } else { // async FileReader won't work as BlobBuilder is sync throw new FileException("NOT_READABLE_ERR"); } } else if (data instanceof FakeBlob) { if (data.encoding === "base64" && atob) { bb.push(atob(data.data)); } else if (data.encoding === "URI") { bb.push(decodeURIComponent(data.data)); } else if (data.encoding === "raw") { bb.push(data.data); } } else { if (typeof data !== "string") { data += ""; // convert unsupported types to strings } // decode UTF-16 to binary string bb.push(unescape(encodeURIComponent(data))); } }; FBB_proto.getBlob = function(type) { if (!arguments.length) { type = null; } return new FakeBlob(this.data.join(""), type, "raw"); }; FBB_proto.toString = function() { return "[object BlobBuilder]"; }; FB_proto.slice = function(start, end, type) { var args = arguments.length; if (args < 3) { type = null; } return new FakeBlob( this.data.slice(start, args > 1 ? end : this.data.length) , type , this.encoding ); }; FB_proto.toString = function() { return "[object Blob]"; }; FB_proto.close = function() { this.size = 0; delete this.data; }; return FakeBlobBuilder; }(view)); view.Blob = function(blobParts, options) { var type = options ? (options.type || "") : ""; var builder = new BlobBuilder(); if (blobParts) { for (var i = 0, len = blobParts.length; i < len; i++) { builder.append(blobParts[i]); } } return builder.getBlob(type); }; }(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this)); guacamole-client-0.9.9/guacamole/src/main/webapp/lib/blob/LICENSE.md0100664 0001751 0001751 00000002101 12612226251 026536 0ustar00mjumpermjumper0000000 0000000 Copyright © 2014 [Eli Grey][1]. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. [1]: http://eligrey.com guacamole-client-0.9.9/guacamole/src/main/webapp/lib/angular-translate/angular-translate.js0100664 0001751 0001751 00000306273 12612226251 033643 0ustar00mjumpermjumper0000000 0000000 /*! * angular-translate - v2.7.2 - 2015-06-01 * http://github.com/angular-translate/angular-translate * Copyright (c) 2015 ; Licensed MIT */ (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module unless amdModuleId is set define([], function () { return (factory()); }); } else if (typeof exports === 'object') { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. module.exports = factory(); } else { factory(); } }(this, function () { /** * @ngdoc overview * @name pascalprecht.translate * * @description * The main module which holds everything together. */ angular.module('pascalprecht.translate', ['ng']) .run(runTranslate); function runTranslate($translate) { 'use strict'; var key = $translate.storageKey(), storage = $translate.storage(); var fallbackFromIncorrectStorageValue = function () { var preferred = $translate.preferredLanguage(); if (angular.isString(preferred)) { $translate.use(preferred); // $translate.use() will also remember the language. // So, we don't need to call storage.put() here. } else { storage.put(key, $translate.use()); } }; fallbackFromIncorrectStorageValue.displayName = 'fallbackFromIncorrectStorageValue'; if (storage) { if (!storage.get(key)) { fallbackFromIncorrectStorageValue(); } else { $translate.use(storage.get(key))['catch'](fallbackFromIncorrectStorageValue); } } else if (angular.isString($translate.preferredLanguage())) { $translate.use($translate.preferredLanguage()); } } runTranslate.$inject = ['$translate']; runTranslate.displayName = 'runTranslate'; /** * @ngdoc object * @name pascalprecht.translate.$translateSanitizationProvider * * @description * * Configurations for $translateSanitization */ angular.module('pascalprecht.translate').provider('$translateSanitization', $translateSanitizationProvider); function $translateSanitizationProvider () { 'use strict'; var $sanitize, currentStrategy = null, // TODO change to either 'sanitize', 'escape' or ['sanitize', 'escapeParameters'] in 3.0. hasConfiguredStrategy = false, hasShownNoStrategyConfiguredWarning = false, strategies; /** * Definition of a sanitization strategy function * @callback StrategyFunction * @param {string|object} value - value to be sanitized (either a string or an interpolated value map) * @param {string} mode - either 'text' for a string (translation) or 'params' for the interpolated params * @return {string|object} */ /** * @ngdoc property * @name strategies * @propertyOf pascalprecht.translate.$translateSanitizationProvider * * @description * Following strategies are built-in: *
*
sanitize
*
Sanitizes HTML in the translation text using $sanitize
*
escape
*
Escapes HTML in the translation
*
sanitizeParameters
*
Sanitizes HTML in the values of the interpolation parameters using $sanitize
*
escapeParameters
*
Escapes HTML in the values of the interpolation parameters
*
escaped
*
Support legacy strategy name 'escaped' for backwards compatibility (will be removed in 3.0)
*
* */ strategies = { sanitize: function (value, mode) { if (mode === 'text') { value = htmlSanitizeValue(value); } return value; }, escape: function (value, mode) { if (mode === 'text') { value = htmlEscapeValue(value); } return value; }, sanitizeParameters: function (value, mode) { if (mode === 'params') { value = mapInterpolationParameters(value, htmlSanitizeValue); } return value; }, escapeParameters: function (value, mode) { if (mode === 'params') { value = mapInterpolationParameters(value, htmlEscapeValue); } return value; } }; // Support legacy strategy name 'escaped' for backwards compatibility. // TODO should be removed in 3.0 strategies.escaped = strategies.escapeParameters; /** * @ngdoc function * @name pascalprecht.translate.$translateSanitizationProvider#addStrategy * @methodOf pascalprecht.translate.$translateSanitizationProvider * * @description * Adds a sanitization strategy to the list of known strategies. * * @param {string} strategyName - unique key for a strategy * @param {StrategyFunction} strategyFunction - strategy function * @returns {object} this */ this.addStrategy = function (strategyName, strategyFunction) { strategies[strategyName] = strategyFunction; return this; }; /** * @ngdoc function * @name pascalprecht.translate.$translateSanitizationProvider#removeStrategy * @methodOf pascalprecht.translate.$translateSanitizationProvider * * @description * Removes a sanitization strategy from the list of known strategies. * * @param {string} strategyName - unique key for a strategy * @returns {object} this */ this.removeStrategy = function (strategyName) { delete strategies[strategyName]; return this; }; /** * @ngdoc function * @name pascalprecht.translate.$translateSanitizationProvider#useStrategy * @methodOf pascalprecht.translate.$translateSanitizationProvider * * @description * Selects a sanitization strategy. When an array is provided the strategies will be executed in order. * * @param {string|StrategyFunction|array} strategy The sanitization strategy / strategies which should be used. Either a name of an existing strategy, a custom strategy function, or an array consisting of multiple names and / or custom functions. * @returns {object} this */ this.useStrategy = function (strategy) { hasConfiguredStrategy = true; currentStrategy = strategy; return this; }; /** * @ngdoc object * @name pascalprecht.translate.$translateSanitization * @requires $injector * @requires $log * * @description * Sanitizes interpolation parameters and translated texts. * */ this.$get = ['$injector', '$log', function ($injector, $log) { var applyStrategies = function (value, mode, selectedStrategies) { angular.forEach(selectedStrategies, function (selectedStrategy) { if (angular.isFunction(selectedStrategy)) { value = selectedStrategy(value, mode); } else if (angular.isFunction(strategies[selectedStrategy])) { value = strategies[selectedStrategy](value, mode); } else { throw new Error('pascalprecht.translate.$translateSanitization: Unknown sanitization strategy: \'' + selectedStrategy + '\''); } }); return value; }; // TODO: should be removed in 3.0 var showNoStrategyConfiguredWarning = function () { if (!hasConfiguredStrategy && !hasShownNoStrategyConfiguredWarning) { $log.warn('pascalprecht.translate.$translateSanitization: No sanitization strategy has been configured. This can have serious security implications. See http://angular-translate.github.io/docs/#/guide/19_security for details.'); hasShownNoStrategyConfiguredWarning = true; } }; if ($injector.has('$sanitize')) { $sanitize = $injector.get('$sanitize'); } return { /** * @ngdoc function * @name pascalprecht.translate.$translateSanitization#useStrategy * @methodOf pascalprecht.translate.$translateSanitization * * @description * Selects a sanitization strategy. When an array is provided the strategies will be executed in order. * * @param {string|StrategyFunction|array} strategy The sanitization strategy / strategies which should be used. Either a name of an existing strategy, a custom strategy function, or an array consisting of multiple names and / or custom functions. */ useStrategy: (function (self) { return function (strategy) { self.useStrategy(strategy); }; })(this), /** * @ngdoc function * @name pascalprecht.translate.$translateSanitization#sanitize * @methodOf pascalprecht.translate.$translateSanitization * * @description * Sanitizes a value. * * @param {string|object} value The value which should be sanitized. * @param {string} mode The current sanitization mode, either 'params' or 'text'. * @param {string|StrategyFunction|array} [strategy] Optional custom strategy which should be used instead of the currently selected strategy. * @returns {string|object} sanitized value */ sanitize: function (value, mode, strategy) { if (!currentStrategy) { showNoStrategyConfiguredWarning(); } if (arguments.length < 3) { strategy = currentStrategy; } if (!strategy) { return value; } var selectedStrategies = angular.isArray(strategy) ? strategy : [strategy]; return applyStrategies(value, mode, selectedStrategies); } }; }]; var htmlEscapeValue = function (value) { var element = angular.element('
'); element.text(value); // not chainable, see #1044 return element.html(); }; var htmlSanitizeValue = function (value) { if (!$sanitize) { throw new Error('pascalprecht.translate.$translateSanitization: Error cannot find $sanitize service. Either include the ngSanitize module (https://docs.angularjs.org/api/ngSanitize) or use a sanitization strategy which does not depend on $sanitize, such as \'escape\'.'); } return $sanitize(value); }; var mapInterpolationParameters = function (value, iteratee) { if (angular.isObject(value)) { var result = angular.isArray(value) ? [] : {}; angular.forEach(value, function (propertyValue, propertyKey) { result[propertyKey] = mapInterpolationParameters(propertyValue, iteratee); }); return result; } else if (angular.isNumber(value)) { return value; } else { return iteratee(value); } }; } /** * @ngdoc object * @name pascalprecht.translate.$translateProvider * @description * * $translateProvider allows developers to register translation-tables, asynchronous loaders * and similar to configure translation behavior directly inside of a module. * */ angular.module('pascalprecht.translate') .constant('pascalprechtTranslateOverrider', {}) .provider('$translate', $translate); function $translate($STORAGE_KEY, $windowProvider, $translateSanitizationProvider, pascalprechtTranslateOverrider) { 'use strict'; var $translationTable = {}, $preferredLanguage, $availableLanguageKeys = [], $languageKeyAliases, $fallbackLanguage, $fallbackWasString, $uses, $nextLang, $storageFactory, $storageKey = $STORAGE_KEY, $storagePrefix, $missingTranslationHandlerFactory, $interpolationFactory, $interpolatorFactories = [], $loaderFactory, $cloakClassName = 'translate-cloak', $loaderOptions, $notFoundIndicatorLeft, $notFoundIndicatorRight, $postCompilingEnabled = false, $forceAsyncReloadEnabled = false, NESTED_OBJECT_DELIMITER = '.', loaderCache, directivePriority = 0, statefulFilter = true, uniformLanguageTagResolver = 'default', languageTagResolver = { 'default': function (tag) { return (tag || '').split('-').join('_'); }, java: function (tag) { var temp = (tag || '').split('-').join('_'); var parts = temp.split('_'); return parts.length > 1 ? (parts[0].toLowerCase() + '_' + parts[1].toUpperCase()) : temp; }, bcp47: function (tag) { var temp = (tag || '').split('_').join('-'); var parts = temp.split('-'); return parts.length > 1 ? (parts[0].toLowerCase() + '-' + parts[1].toUpperCase()) : temp; } }; var version = '2.7.2'; // tries to determine the browsers language var getFirstBrowserLanguage = function () { // internal purpose only if (angular.isFunction(pascalprechtTranslateOverrider.getLocale)) { return pascalprechtTranslateOverrider.getLocale(); } var nav = $windowProvider.$get().navigator, browserLanguagePropertyKeys = ['language', 'browserLanguage', 'systemLanguage', 'userLanguage'], i, language; // support for HTML 5.1 "navigator.languages" if (angular.isArray(nav.languages)) { for (i = 0; i < nav.languages.length; i++) { language = nav.languages[i]; if (language && language.length) { return language; } } } // support for other well known properties in browsers for (i = 0; i < browserLanguagePropertyKeys.length; i++) { language = nav[browserLanguagePropertyKeys[i]]; if (language && language.length) { return language; } } return null; }; getFirstBrowserLanguage.displayName = 'angular-translate/service: getFirstBrowserLanguage'; // tries to determine the browsers locale var getLocale = function () { var locale = getFirstBrowserLanguage() || ''; if (languageTagResolver[uniformLanguageTagResolver]) { locale = languageTagResolver[uniformLanguageTagResolver](locale); } return locale; }; getLocale.displayName = 'angular-translate/service: getLocale'; /** * @name indexOf * @private * * @description * indexOf polyfill. Kinda sorta. * * @param {array} array Array to search in. * @param {string} searchElement Element to search for. * * @returns {int} Index of search element. */ var indexOf = function(array, searchElement) { for (var i = 0, len = array.length; i < len; i++) { if (array[i] === searchElement) { return i; } } return -1; }; /** * @name trim * @private * * @description * trim polyfill * * @returns {string} The string stripped of whitespace from both ends */ var trim = function() { return this.toString().replace(/^\s+|\s+$/g, ''); }; var negotiateLocale = function (preferred) { var avail = [], locale = angular.lowercase(preferred), i = 0, n = $availableLanguageKeys.length; for (; i < n; i++) { avail.push(angular.lowercase($availableLanguageKeys[i])); } if (indexOf(avail, locale) > -1) { return preferred; } if ($languageKeyAliases) { var alias; for (var langKeyAlias in $languageKeyAliases) { var hasWildcardKey = false; var hasExactKey = Object.prototype.hasOwnProperty.call($languageKeyAliases, langKeyAlias) && angular.lowercase(langKeyAlias) === angular.lowercase(preferred); if (langKeyAlias.slice(-1) === '*') { hasWildcardKey = langKeyAlias.slice(0, -1) === preferred.slice(0, langKeyAlias.length-1); } if (hasExactKey || hasWildcardKey) { alias = $languageKeyAliases[langKeyAlias]; if (indexOf(avail, angular.lowercase(alias)) > -1) { return alias; } } } } if (preferred) { var parts = preferred.split('_'); if (parts.length > 1 && indexOf(avail, angular.lowercase(parts[0])) > -1) { return parts[0]; } } // If everything fails, just return the preferred, unchanged. return preferred; }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#translations * @methodOf pascalprecht.translate.$translateProvider * * @description * Registers a new translation table for specific language key. * * To register a translation table for specific language, pass a defined language * key as first parameter. * *
   *  // register translation table for language: 'de_DE'
   *  $translateProvider.translations('de_DE', {
   *    'GREETING': 'Hallo Welt!'
   *  });
   *
   *  // register another one
   *  $translateProvider.translations('en_US', {
   *    'GREETING': 'Hello world!'
   *  });
   * 
* * When registering multiple translation tables for for the same language key, * the actual translation table gets extended. This allows you to define module * specific translation which only get added, once a specific module is loaded in * your app. * * Invoking this method with no arguments returns the translation table which was * registered with no language key. Invoking it with a language key returns the * related translation table. * * @param {string} key A language key. * @param {object} translationTable A plain old JavaScript object that represents a translation table. * */ var translations = function (langKey, translationTable) { if (!langKey && !translationTable) { return $translationTable; } if (langKey && !translationTable) { if (angular.isString(langKey)) { return $translationTable[langKey]; } } else { if (!angular.isObject($translationTable[langKey])) { $translationTable[langKey] = {}; } angular.extend($translationTable[langKey], flatObject(translationTable)); } return this; }; this.translations = translations; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#cloakClassName * @methodOf pascalprecht.translate.$translateProvider * * @description * * Let's you change the class name for `translate-cloak` directive. * Default class name is `translate-cloak`. * * @param {string} name translate-cloak class name */ this.cloakClassName = function (name) { if (!name) { return $cloakClassName; } $cloakClassName = name; return this; }; /** * @name flatObject * @private * * @description * Flats an object. This function is used to flatten given translation data with * namespaces, so they are later accessible via dot notation. */ var flatObject = function (data, path, result, prevKey) { var key, keyWithPath, keyWithShortPath, val; if (!path) { path = []; } if (!result) { result = {}; } for (key in data) { if (!Object.prototype.hasOwnProperty.call(data, key)) { continue; } val = data[key]; if (angular.isObject(val)) { flatObject(val, path.concat(key), result, key); } else { keyWithPath = path.length ? ('' + path.join(NESTED_OBJECT_DELIMITER) + NESTED_OBJECT_DELIMITER + key) : key; if(path.length && key === prevKey){ // Create shortcut path (foo.bar == foo.bar.bar) keyWithShortPath = '' + path.join(NESTED_OBJECT_DELIMITER); // Link it to original path result[keyWithShortPath] = '@:' + keyWithPath; } result[keyWithPath] = val; } } return result; }; flatObject.displayName = 'flatObject'; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#addInterpolation * @methodOf pascalprecht.translate.$translateProvider * * @description * Adds interpolation services to angular-translate, so it can manage them. * * @param {object} factory Interpolation service factory */ this.addInterpolation = function (factory) { $interpolatorFactories.push(factory); return this; }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#useMessageFormatInterpolation * @methodOf pascalprecht.translate.$translateProvider * * @description * Tells angular-translate to use interpolation functionality of messageformat.js. * This is useful when having high level pluralization and gender selection. */ this.useMessageFormatInterpolation = function () { return this.useInterpolation('$translateMessageFormatInterpolation'); }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#useInterpolation * @methodOf pascalprecht.translate.$translateProvider * * @description * Tells angular-translate which interpolation style to use as default, application-wide. * Simply pass a factory/service name. The interpolation service has to implement * the correct interface. * * @param {string} factory Interpolation service name. */ this.useInterpolation = function (factory) { $interpolationFactory = factory; return this; }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#useSanitizeStrategy * @methodOf pascalprecht.translate.$translateProvider * * @description * Simply sets a sanitation strategy type. * * @param {string} value Strategy type. */ this.useSanitizeValueStrategy = function (value) { $translateSanitizationProvider.useStrategy(value); return this; }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#preferredLanguage * @methodOf pascalprecht.translate.$translateProvider * * @description * Tells the module which of the registered translation tables to use for translation * at initial startup by passing a language key. Similar to `$translateProvider#use` * only that it says which language to **prefer**. * * @param {string} langKey A language key. * */ this.preferredLanguage = function(langKey) { setupPreferredLanguage(langKey); return this; }; var setupPreferredLanguage = function (langKey) { if (langKey) { $preferredLanguage = langKey; } return $preferredLanguage; }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#translationNotFoundIndicator * @methodOf pascalprecht.translate.$translateProvider * * @description * Sets an indicator which is used when a translation isn't found. E.g. when * setting the indicator as 'X' and one tries to translate a translation id * called `NOT_FOUND`, this will result in `X NOT_FOUND X`. * * Internally this methods sets a left indicator and a right indicator using * `$translateProvider.translationNotFoundIndicatorLeft()` and * `$translateProvider.translationNotFoundIndicatorRight()`. * * **Note**: These methods automatically add a whitespace between the indicators * and the translation id. * * @param {string} indicator An indicator, could be any string. */ this.translationNotFoundIndicator = function (indicator) { this.translationNotFoundIndicatorLeft(indicator); this.translationNotFoundIndicatorRight(indicator); return this; }; /** * ngdoc function * @name pascalprecht.translate.$translateProvider#translationNotFoundIndicatorLeft * @methodOf pascalprecht.translate.$translateProvider * * @description * Sets an indicator which is used when a translation isn't found left to the * translation id. * * @param {string} indicator An indicator. */ this.translationNotFoundIndicatorLeft = function (indicator) { if (!indicator) { return $notFoundIndicatorLeft; } $notFoundIndicatorLeft = indicator; return this; }; /** * ngdoc function * @name pascalprecht.translate.$translateProvider#translationNotFoundIndicatorLeft * @methodOf pascalprecht.translate.$translateProvider * * @description * Sets an indicator which is used when a translation isn't found right to the * translation id. * * @param {string} indicator An indicator. */ this.translationNotFoundIndicatorRight = function (indicator) { if (!indicator) { return $notFoundIndicatorRight; } $notFoundIndicatorRight = indicator; return this; }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#fallbackLanguage * @methodOf pascalprecht.translate.$translateProvider * * @description * Tells the module which of the registered translation tables to use when missing translations * at initial startup by passing a language key. Similar to `$translateProvider#use` * only that it says which language to **fallback**. * * @param {string||array} langKey A language key. * */ this.fallbackLanguage = function (langKey) { fallbackStack(langKey); return this; }; var fallbackStack = function (langKey) { if (langKey) { if (angular.isString(langKey)) { $fallbackWasString = true; $fallbackLanguage = [ langKey ]; } else if (angular.isArray(langKey)) { $fallbackWasString = false; $fallbackLanguage = langKey; } if (angular.isString($preferredLanguage) && indexOf($fallbackLanguage, $preferredLanguage) < 0) { $fallbackLanguage.push($preferredLanguage); } return this; } else { if ($fallbackWasString) { return $fallbackLanguage[0]; } else { return $fallbackLanguage; } } }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#use * @methodOf pascalprecht.translate.$translateProvider * * @description * Set which translation table to use for translation by given language key. When * trying to 'use' a language which isn't provided, it'll throw an error. * * You actually don't have to use this method since `$translateProvider#preferredLanguage` * does the job too. * * @param {string} langKey A language key. */ this.use = function (langKey) { if (langKey) { if (!$translationTable[langKey] && (!$loaderFactory)) { // only throw an error, when not loading translation data asynchronously throw new Error('$translateProvider couldn\'t find translationTable for langKey: \'' + langKey + '\''); } $uses = langKey; return this; } return $uses; }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#storageKey * @methodOf pascalprecht.translate.$translateProvider * * @description * Tells the module which key must represent the choosed language by a user in the storage. * * @param {string} key A key for the storage. */ var storageKey = function(key) { if (!key) { if ($storagePrefix) { return $storagePrefix + $storageKey; } return $storageKey; } $storageKey = key; return this; }; this.storageKey = storageKey; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#useUrlLoader * @methodOf pascalprecht.translate.$translateProvider * * @description * Tells angular-translate to use `$translateUrlLoader` extension service as loader. * * @param {string} url Url * @param {Object=} options Optional configuration object */ this.useUrlLoader = function (url, options) { return this.useLoader('$translateUrlLoader', angular.extend({ url: url }, options)); }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#useStaticFilesLoader * @methodOf pascalprecht.translate.$translateProvider * * @description * Tells angular-translate to use `$translateStaticFilesLoader` extension service as loader. * * @param {Object=} options Optional configuration object */ this.useStaticFilesLoader = function (options) { return this.useLoader('$translateStaticFilesLoader', options); }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#useLoader * @methodOf pascalprecht.translate.$translateProvider * * @description * Tells angular-translate to use any other service as loader. * * @param {string} loaderFactory Factory name to use * @param {Object=} options Optional configuration object */ this.useLoader = function (loaderFactory, options) { $loaderFactory = loaderFactory; $loaderOptions = options || {}; return this; }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#useLocalStorage * @methodOf pascalprecht.translate.$translateProvider * * @description * Tells angular-translate to use `$translateLocalStorage` service as storage layer. * */ this.useLocalStorage = function () { return this.useStorage('$translateLocalStorage'); }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#useCookieStorage * @methodOf pascalprecht.translate.$translateProvider * * @description * Tells angular-translate to use `$translateCookieStorage` service as storage layer. */ this.useCookieStorage = function () { return this.useStorage('$translateCookieStorage'); }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#useStorage * @methodOf pascalprecht.translate.$translateProvider * * @description * Tells angular-translate to use custom service as storage layer. */ this.useStorage = function (storageFactory) { $storageFactory = storageFactory; return this; }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#storagePrefix * @methodOf pascalprecht.translate.$translateProvider * * @description * Sets prefix for storage key. * * @param {string} prefix Storage key prefix */ this.storagePrefix = function (prefix) { if (!prefix) { return prefix; } $storagePrefix = prefix; return this; }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#useMissingTranslationHandlerLog * @methodOf pascalprecht.translate.$translateProvider * * @description * Tells angular-translate to use built-in log handler when trying to translate * a translation Id which doesn't exist. * * This is actually a shortcut method for `useMissingTranslationHandler()`. * */ this.useMissingTranslationHandlerLog = function () { return this.useMissingTranslationHandler('$translateMissingTranslationHandlerLog'); }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#useMissingTranslationHandler * @methodOf pascalprecht.translate.$translateProvider * * @description * Expects a factory name which later gets instantiated with `$injector`. * This method can be used to tell angular-translate to use a custom * missingTranslationHandler. Just build a factory which returns a function * and expects a translation id as argument. * * Example: *
   *  app.config(function ($translateProvider) {
   *    $translateProvider.useMissingTranslationHandler('customHandler');
   *  });
   *
   *  app.factory('customHandler', function (dep1, dep2) {
   *    return function (translationId) {
   *      // something with translationId and dep1 and dep2
   *    };
   *  });
   * 
* * @param {string} factory Factory name */ this.useMissingTranslationHandler = function (factory) { $missingTranslationHandlerFactory = factory; return this; }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#usePostCompiling * @methodOf pascalprecht.translate.$translateProvider * * @description * If post compiling is enabled, all translated values will be processed * again with AngularJS' $compile. * * Example: *
   *  app.config(function ($translateProvider) {
   *    $translateProvider.usePostCompiling(true);
   *  });
   * 
* * @param {string} factory Factory name */ this.usePostCompiling = function (value) { $postCompilingEnabled = !(!value); return this; }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#forceAsyncReload * @methodOf pascalprecht.translate.$translateProvider * * @description * If force async reload is enabled, async loader will always be called * even if $translationTable already contains the language key, adding * possible new entries to the $translationTable. * * Example: *
   *  app.config(function ($translateProvider) {
   *    $translateProvider.forceAsyncReload(true);
   *  });
   * 
* * @param {boolean} value - valid values are true or false */ this.forceAsyncReload = function (value) { $forceAsyncReloadEnabled = !(!value); return this; }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#uniformLanguageTag * @methodOf pascalprecht.translate.$translateProvider * * @description * Tells angular-translate which language tag should be used as a result when determining * the current browser language. * * This setting must be set before invoking {@link pascalprecht.translate.$translateProvider#methods_determinePreferredLanguage determinePreferredLanguage()}. * *
   * $translateProvider
   *   .uniformLanguageTag('bcp47')
   *   .determinePreferredLanguage()
   * 
* * The resolver currently supports: * * default * (traditionally: hyphens will be converted into underscores, i.e. en-US => en_US) * en-US => en_US * en_US => en_US * en-us => en_us * * java * like default, but the second part will be always in uppercase * en-US => en_US * en_US => en_US * en-us => en_US * * BCP 47 (RFC 4646 & 4647) * en-US => en-US * en_US => en-US * en-us => en-US * * See also: * * http://en.wikipedia.org/wiki/IETF_language_tag * * http://www.w3.org/International/core/langtags/ * * http://tools.ietf.org/html/bcp47 * * @param {string|object} options - options (or standard) * @param {string} options.standard - valid values are 'default', 'bcp47', 'java' */ this.uniformLanguageTag = function (options) { if (!options) { options = {}; } else if (angular.isString(options)) { options = { standard: options }; } uniformLanguageTagResolver = options.standard; return this; }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#determinePreferredLanguage * @methodOf pascalprecht.translate.$translateProvider * * @description * Tells angular-translate to try to determine on its own which language key * to set as preferred language. When `fn` is given, angular-translate uses it * to determine a language key, otherwise it uses the built-in `getLocale()` * method. * * The `getLocale()` returns a language key in the format `[lang]_[country]` or * `[lang]` depending on what the browser provides. * * Use this method at your own risk, since not all browsers return a valid * locale (see {@link pascalprecht.translate.$translateProvider#methods_uniformLanguageTag uniformLanguageTag()}). * * @param {Function=} fn Function to determine a browser's locale */ this.determinePreferredLanguage = function (fn) { var locale = (fn && angular.isFunction(fn)) ? fn() : getLocale(); if (!$availableLanguageKeys.length) { $preferredLanguage = locale; } else { $preferredLanguage = negotiateLocale(locale); } return this; }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#registerAvailableLanguageKeys * @methodOf pascalprecht.translate.$translateProvider * * @description * Registers a set of language keys the app will work with. Use this method in * combination with * {@link pascalprecht.translate.$translateProvider#determinePreferredLanguage determinePreferredLanguage}. * When available languages keys are registered, angular-translate * tries to find the best fitting language key depending on the browsers locale, * considering your language key convention. * * @param {object} languageKeys Array of language keys the your app will use * @param {object=} aliases Alias map. */ this.registerAvailableLanguageKeys = function (languageKeys, aliases) { if (languageKeys) { $availableLanguageKeys = languageKeys; if (aliases) { $languageKeyAliases = aliases; } return this; } return $availableLanguageKeys; }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#useLoaderCache * @methodOf pascalprecht.translate.$translateProvider * * @description * Registers a cache for internal $http based loaders. * {@link pascalprecht.translate.$translateProvider#determinePreferredLanguage determinePreferredLanguage}. * When false the cache will be disabled (default). When true or undefined * the cache will be a default (see $cacheFactory). When an object it will * be treat as a cache object itself: the usage is $http({cache: cache}) * * @param {object} cache boolean, string or cache-object */ this.useLoaderCache = function (cache) { if (cache === false) { // disable cache loaderCache = undefined; } else if (cache === true) { // enable cache using AJS defaults loaderCache = true; } else if (typeof(cache) === 'undefined') { // enable cache using default loaderCache = '$translationCache'; } else if (cache) { // enable cache using given one (see $cacheFactory) loaderCache = cache; } return this; }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#directivePriority * @methodOf pascalprecht.translate.$translateProvider * * @description * Sets the default priority of the translate directive. The standard value is `0`. * Calling this function without an argument will return the current value. * * @param {number} priority for the translate-directive */ this.directivePriority = function (priority) { if (priority === undefined) { // getter return directivePriority; } else { // setter with chaining directivePriority = priority; return this; } }; /** * @ngdoc function * @name pascalprecht.translate.$translateProvider#statefulFilter * @methodOf pascalprecht.translate.$translateProvider * * @description * Since AngularJS 1.3, filters which are not stateless (depending at the scope) * have to explicit define this behavior. * Sets whether the translate filter should be stateful or stateless. The standard value is `true` * meaning being stateful. * Calling this function without an argument will return the current value. * * @param {boolean} state - defines the state of the filter */ this.statefulFilter = function (state) { if (state === undefined) { // getter return statefulFilter; } else { // setter with chaining statefulFilter = state; return this; } }; /** * @ngdoc object * @name pascalprecht.translate.$translate * @requires $interpolate * @requires $log * @requires $rootScope * @requires $q * * @description * The `$translate` service is the actual core of angular-translate. It expects a translation id * and optional interpolate parameters to translate contents. * *
   *  $translate('HEADLINE_TEXT').then(function (translation) {
   *    $scope.translatedText = translation;
   *  });
   * 
* * @param {string|array} translationId A token which represents a translation id * This can be optionally an array of translation ids which * results that the function returns an object where each key * is the translation id and the value the translation. * @param {object=} interpolateParams An object hash for dynamic values * @param {string} interpolationId The id of the interpolation to use * @returns {object} promise */ this.$get = [ '$log', '$injector', '$rootScope', '$q', function ($log, $injector, $rootScope, $q) { var Storage, defaultInterpolator = $injector.get($interpolationFactory || '$translateDefaultInterpolation'), pendingLoader = false, interpolatorHashMap = {}, langPromises = {}, fallbackIndex, startFallbackIteration; var $translate = function (translationId, interpolateParams, interpolationId, defaultTranslationText) { // Duck detection: If the first argument is an array, a bunch of translations was requested. // The result is an object. if (angular.isArray(translationId)) { // Inspired by Q.allSettled by Kris Kowal // https://github.com/kriskowal/q/blob/b0fa72980717dc202ffc3cbf03b936e10ebbb9d7/q.js#L1553-1563 // This transforms all promises regardless resolved or rejected var translateAll = function (translationIds) { var results = {}; // storing the actual results var promises = []; // promises to wait for // Wraps the promise a) being always resolved and b) storing the link id->value var translate = function (translationId) { var deferred = $q.defer(); var regardless = function (value) { results[translationId] = value; deferred.resolve([translationId, value]); }; // we don't care whether the promise was resolved or rejected; just store the values $translate(translationId, interpolateParams, interpolationId, defaultTranslationText).then(regardless, regardless); return deferred.promise; }; for (var i = 0, c = translationIds.length; i < c; i++) { promises.push(translate(translationIds[i])); } // wait for all (including storing to results) return $q.all(promises).then(function () { // return the results return results; }); }; return translateAll(translationId); } var deferred = $q.defer(); // trim off any whitespace if (translationId) { translationId = trim.apply(translationId); } var promiseToWaitFor = (function () { var promise = $preferredLanguage ? langPromises[$preferredLanguage] : langPromises[$uses]; fallbackIndex = 0; if ($storageFactory && !promise) { // looks like there's no pending promise for $preferredLanguage or // $uses. Maybe there's one pending for a language that comes from // storage. var langKey = Storage.get($storageKey); promise = langPromises[langKey]; if ($fallbackLanguage && $fallbackLanguage.length) { var index = indexOf($fallbackLanguage, langKey); // maybe the language from storage is also defined as fallback language // we increase the fallback language index to not search in that language // as fallback, since it's probably the first used language // in that case the index starts after the first element fallbackIndex = (index === 0) ? 1 : 0; // but we can make sure to ALWAYS fallback to preferred language at least if (indexOf($fallbackLanguage, $preferredLanguage) < 0) { $fallbackLanguage.push($preferredLanguage); } } } return promise; }()); if (!promiseToWaitFor) { // no promise to wait for? okay. Then there's no loader registered // nor is a one pending for language that comes from storage. // We can just translate. determineTranslation(translationId, interpolateParams, interpolationId, defaultTranslationText).then(deferred.resolve, deferred.reject); } else { var promiseResolved = function () { determineTranslation(translationId, interpolateParams, interpolationId, defaultTranslationText).then(deferred.resolve, deferred.reject); }; promiseResolved.displayName = 'promiseResolved'; promiseToWaitFor['finally'](promiseResolved, deferred.reject); } return deferred.promise; }; /** * @name applyNotFoundIndicators * @private * * @description * Applies not fount indicators to given translation id, if needed. * This function gets only executed, if a translation id doesn't exist, * which is why a translation id is expected as argument. * * @param {string} translationId Translation id. * @returns {string} Same as given translation id but applied with not found * indicators. */ var applyNotFoundIndicators = function (translationId) { // applying notFoundIndicators if ($notFoundIndicatorLeft) { translationId = [$notFoundIndicatorLeft, translationId].join(' '); } if ($notFoundIndicatorRight) { translationId = [translationId, $notFoundIndicatorRight].join(' '); } return translationId; }; /** * @name useLanguage * @private * * @description * Makes actual use of a language by setting a given language key as used * language and informs registered interpolators to also use the given * key as locale. * * @param {key} Locale key. */ var useLanguage = function (key) { $uses = key; $rootScope.$emit('$translateChangeSuccess', {language: key}); if ($storageFactory) { Storage.put($translate.storageKey(), $uses); } // inform default interpolator defaultInterpolator.setLocale($uses); var eachInterpolator = function (interpolator, id) { interpolatorHashMap[id].setLocale($uses); }; eachInterpolator.displayName = 'eachInterpolatorLocaleSetter'; // inform all others too! angular.forEach(interpolatorHashMap, eachInterpolator); $rootScope.$emit('$translateChangeEnd', {language: key}); }; /** * @name loadAsync * @private * * @description * Kicks of registered async loader using `$injector` and applies existing * loader options. When resolved, it updates translation tables accordingly * or rejects with given language key. * * @param {string} key Language key. * @return {Promise} A promise. */ var loadAsync = function (key) { if (!key) { throw 'No language key specified for loading.'; } var deferred = $q.defer(); $rootScope.$emit('$translateLoadingStart', {language: key}); pendingLoader = true; var cache = loaderCache; if (typeof(cache) === 'string') { // getting on-demand instance of loader cache = $injector.get(cache); } var loaderOptions = angular.extend({}, $loaderOptions, { key: key, $http: angular.extend({}, { cache: cache }, $loaderOptions.$http) }); var onLoaderSuccess = function (data) { var translationTable = {}; $rootScope.$emit('$translateLoadingSuccess', {language: key}); if (angular.isArray(data)) { angular.forEach(data, function (table) { angular.extend(translationTable, flatObject(table)); }); } else { angular.extend(translationTable, flatObject(data)); } pendingLoader = false; deferred.resolve({ key: key, table: translationTable }); $rootScope.$emit('$translateLoadingEnd', {language: key}); }; onLoaderSuccess.displayName = 'onLoaderSuccess'; var onLoaderError = function (key) { $rootScope.$emit('$translateLoadingError', {language: key}); deferred.reject(key); $rootScope.$emit('$translateLoadingEnd', {language: key}); }; onLoaderError.displayName = 'onLoaderError'; $injector.get($loaderFactory)(loaderOptions) .then(onLoaderSuccess, onLoaderError); return deferred.promise; }; if ($storageFactory) { Storage = $injector.get($storageFactory); if (!Storage.get || !Storage.put) { throw new Error('Couldn\'t use storage \'' + $storageFactory + '\', missing get() or put() method!'); } } // if we have additional interpolations that were added via // $translateProvider.addInterpolation(), we have to map'em if ($interpolatorFactories.length) { var eachInterpolationFactory = function (interpolatorFactory) { var interpolator = $injector.get(interpolatorFactory); // setting initial locale for each interpolation service interpolator.setLocale($preferredLanguage || $uses); // make'em recognizable through id interpolatorHashMap[interpolator.getInterpolationIdentifier()] = interpolator; }; eachInterpolationFactory.displayName = 'interpolationFactoryAdder'; angular.forEach($interpolatorFactories, eachInterpolationFactory); } /** * @name getTranslationTable * @private * * @description * Returns a promise that resolves to the translation table * or is rejected if an error occurred. * * @param langKey * @returns {Q.promise} */ var getTranslationTable = function (langKey) { var deferred = $q.defer(); if (Object.prototype.hasOwnProperty.call($translationTable, langKey)) { deferred.resolve($translationTable[langKey]); } else if (langPromises[langKey]) { var onResolve = function (data) { translations(data.key, data.table); deferred.resolve(data.table); }; onResolve.displayName = 'translationTableResolver'; langPromises[langKey].then(onResolve, deferred.reject); } else { deferred.reject(); } return deferred.promise; }; /** * @name getFallbackTranslation * @private * * @description * Returns a promise that will resolve to the translation * or be rejected if no translation was found for the language. * This function is currently only used for fallback language translation. * * @param langKey The language to translate to. * @param translationId * @param interpolateParams * @param Interpolator * @returns {Q.promise} */ var getFallbackTranslation = function (langKey, translationId, interpolateParams, Interpolator) { var deferred = $q.defer(); var onResolve = function (translationTable) { if (Object.prototype.hasOwnProperty.call(translationTable, translationId)) { Interpolator.setLocale(langKey); var translation = translationTable[translationId]; if (translation.substr(0, 2) === '@:') { getFallbackTranslation(langKey, translation.substr(2), interpolateParams, Interpolator) .then(deferred.resolve, deferred.reject); } else { deferred.resolve(Interpolator.interpolate(translationTable[translationId], interpolateParams)); } Interpolator.setLocale($uses); } else { deferred.reject(); } }; onResolve.displayName = 'fallbackTranslationResolver'; getTranslationTable(langKey).then(onResolve, deferred.reject); return deferred.promise; }; /** * @name getFallbackTranslationInstant * @private * * @description * Returns a translation * This function is currently only used for fallback language translation. * * @param langKey The language to translate to. * @param translationId * @param interpolateParams * @param Interpolator * @returns {string} translation */ var getFallbackTranslationInstant = function (langKey, translationId, interpolateParams, Interpolator) { var result, translationTable = $translationTable[langKey]; if (translationTable && Object.prototype.hasOwnProperty.call(translationTable, translationId)) { Interpolator.setLocale(langKey); result = Interpolator.interpolate(translationTable[translationId], interpolateParams); if (result.substr(0, 2) === '@:') { return getFallbackTranslationInstant(langKey, result.substr(2), interpolateParams, Interpolator); } Interpolator.setLocale($uses); } return result; }; /** * @name translateByHandler * @private * * Translate by missing translation handler. * * @param translationId * @returns translation created by $missingTranslationHandler or translationId is $missingTranslationHandler is * absent */ var translateByHandler = function (translationId, interpolateParams) { // If we have a handler factory - we might also call it here to determine if it provides // a default text for a translationid that can't be found anywhere in our tables if ($missingTranslationHandlerFactory) { var resultString = $injector.get($missingTranslationHandlerFactory)(translationId, $uses, interpolateParams); if (resultString !== undefined) { return resultString; } else { return translationId; } } else { return translationId; } }; /** * @name resolveForFallbackLanguage * @private * * Recursive helper function for fallbackTranslation that will sequentially look * for a translation in the fallbackLanguages starting with fallbackLanguageIndex. * * @param fallbackLanguageIndex * @param translationId * @param interpolateParams * @param Interpolator * @returns {Q.promise} Promise that will resolve to the translation. */ var resolveForFallbackLanguage = function (fallbackLanguageIndex, translationId, interpolateParams, Interpolator, defaultTranslationText) { var deferred = $q.defer(); if (fallbackLanguageIndex < $fallbackLanguage.length) { var langKey = $fallbackLanguage[fallbackLanguageIndex]; getFallbackTranslation(langKey, translationId, interpolateParams, Interpolator).then( deferred.resolve, function () { // Look in the next fallback language for a translation. // It delays the resolving by passing another promise to resolve. resolveForFallbackLanguage(fallbackLanguageIndex + 1, translationId, interpolateParams, Interpolator, defaultTranslationText).then(deferred.resolve); } ); } else { // No translation found in any fallback language // if a default translation text is set in the directive, then return this as a result if (defaultTranslationText) { deferred.resolve(defaultTranslationText); } else { // if no default translation is set and an error handler is defined, send it to the handler // and then return the result deferred.resolve(translateByHandler(translationId, interpolateParams)); } } return deferred.promise; }; /** * @name resolveForFallbackLanguageInstant * @private * * Recursive helper function for fallbackTranslation that will sequentially look * for a translation in the fallbackLanguages starting with fallbackLanguageIndex. * * @param fallbackLanguageIndex * @param translationId * @param interpolateParams * @param Interpolator * @returns {string} translation */ var resolveForFallbackLanguageInstant = function (fallbackLanguageIndex, translationId, interpolateParams, Interpolator) { var result; if (fallbackLanguageIndex < $fallbackLanguage.length) { var langKey = $fallbackLanguage[fallbackLanguageIndex]; result = getFallbackTranslationInstant(langKey, translationId, interpolateParams, Interpolator); if (!result) { result = resolveForFallbackLanguageInstant(fallbackLanguageIndex + 1, translationId, interpolateParams, Interpolator); } } return result; }; /** * Translates with the usage of the fallback languages. * * @param translationId * @param interpolateParams * @param Interpolator * @returns {Q.promise} Promise, that resolves to the translation. */ var fallbackTranslation = function (translationId, interpolateParams, Interpolator, defaultTranslationText) { // Start with the fallbackLanguage with index 0 return resolveForFallbackLanguage((startFallbackIteration>0 ? startFallbackIteration : fallbackIndex), translationId, interpolateParams, Interpolator, defaultTranslationText); }; /** * Translates with the usage of the fallback languages. * * @param translationId * @param interpolateParams * @param Interpolator * @returns {String} translation */ var fallbackTranslationInstant = function (translationId, interpolateParams, Interpolator) { // Start with the fallbackLanguage with index 0 return resolveForFallbackLanguageInstant((startFallbackIteration>0 ? startFallbackIteration : fallbackIndex), translationId, interpolateParams, Interpolator); }; var determineTranslation = function (translationId, interpolateParams, interpolationId, defaultTranslationText) { var deferred = $q.defer(); var table = $uses ? $translationTable[$uses] : $translationTable, Interpolator = (interpolationId) ? interpolatorHashMap[interpolationId] : defaultInterpolator; // if the translation id exists, we can just interpolate it if (table && Object.prototype.hasOwnProperty.call(table, translationId)) { var translation = table[translationId]; // If using link, rerun $translate with linked translationId and return it if (translation.substr(0, 2) === '@:') { $translate(translation.substr(2), interpolateParams, interpolationId, defaultTranslationText) .then(deferred.resolve, deferred.reject); } else { deferred.resolve(Interpolator.interpolate(translation, interpolateParams)); } } else { var missingTranslationHandlerTranslation; // for logging purposes only (as in $translateMissingTranslationHandlerLog), value is not returned to promise if ($missingTranslationHandlerFactory && !pendingLoader) { missingTranslationHandlerTranslation = translateByHandler(translationId, interpolateParams); } // since we couldn't translate the inital requested translation id, // we try it now with one or more fallback languages, if fallback language(s) is // configured. if ($uses && $fallbackLanguage && $fallbackLanguage.length) { fallbackTranslation(translationId, interpolateParams, Interpolator, defaultTranslationText) .then(function (translation) { deferred.resolve(translation); }, function (_translationId) { deferred.reject(applyNotFoundIndicators(_translationId)); }); } else if ($missingTranslationHandlerFactory && !pendingLoader && missingTranslationHandlerTranslation) { // looks like the requested translation id doesn't exists. // Now, if there is a registered handler for missing translations and no // asyncLoader is pending, we execute the handler if (defaultTranslationText) { deferred.resolve(defaultTranslationText); } else { deferred.resolve(missingTranslationHandlerTranslation); } } else { if (defaultTranslationText) { deferred.resolve(defaultTranslationText); } else { deferred.reject(applyNotFoundIndicators(translationId)); } } } return deferred.promise; }; var determineTranslationInstant = function (translationId, interpolateParams, interpolationId) { var result, table = $uses ? $translationTable[$uses] : $translationTable, Interpolator = defaultInterpolator; // if the interpolation id exists use custom interpolator if (interpolatorHashMap && Object.prototype.hasOwnProperty.call(interpolatorHashMap, interpolationId)) { Interpolator = interpolatorHashMap[interpolationId]; } // if the translation id exists, we can just interpolate it if (table && Object.prototype.hasOwnProperty.call(table, translationId)) { var translation = table[translationId]; // If using link, rerun $translate with linked translationId and return it if (translation.substr(0, 2) === '@:') { result = determineTranslationInstant(translation.substr(2), interpolateParams, interpolationId); } else { result = Interpolator.interpolate(translation, interpolateParams); } } else { var missingTranslationHandlerTranslation; // for logging purposes only (as in $translateMissingTranslationHandlerLog), value is not returned to promise if ($missingTranslationHandlerFactory && !pendingLoader) { missingTranslationHandlerTranslation = translateByHandler(translationId, interpolateParams); } // since we couldn't translate the inital requested translation id, // we try it now with one or more fallback languages, if fallback language(s) is // configured. if ($uses && $fallbackLanguage && $fallbackLanguage.length) { fallbackIndex = 0; result = fallbackTranslationInstant(translationId, interpolateParams, Interpolator); } else if ($missingTranslationHandlerFactory && !pendingLoader && missingTranslationHandlerTranslation) { // looks like the requested translation id doesn't exists. // Now, if there is a registered handler for missing translations and no // asyncLoader is pending, we execute the handler result = missingTranslationHandlerTranslation; } else { result = applyNotFoundIndicators(translationId); } } return result; }; var clearNextLangAndPromise = function(key) { if ($nextLang === key) { $nextLang = undefined; } langPromises[key] = undefined; }; /** * @ngdoc function * @name pascalprecht.translate.$translate#preferredLanguage * @methodOf pascalprecht.translate.$translate * * @description * Returns the language key for the preferred language. * * @param {string} langKey language String or Array to be used as preferredLanguage (changing at runtime) * * @return {string} preferred language key */ $translate.preferredLanguage = function (langKey) { if(langKey) { setupPreferredLanguage(langKey); } return $preferredLanguage; }; /** * @ngdoc function * @name pascalprecht.translate.$translate#cloakClassName * @methodOf pascalprecht.translate.$translate * * @description * Returns the configured class name for `translate-cloak` directive. * * @return {string} cloakClassName */ $translate.cloakClassName = function () { return $cloakClassName; }; /** * @ngdoc function * @name pascalprecht.translate.$translate#fallbackLanguage * @methodOf pascalprecht.translate.$translate * * @description * Returns the language key for the fallback languages or sets a new fallback stack. * * @param {string=} langKey language String or Array of fallback languages to be used (to change stack at runtime) * * @return {string||array} fallback language key */ $translate.fallbackLanguage = function (langKey) { if (langKey !== undefined && langKey !== null) { fallbackStack(langKey); // as we might have an async loader initiated and a new translation language might have been defined // we need to add the promise to the stack also. So - iterate. if ($loaderFactory) { if ($fallbackLanguage && $fallbackLanguage.length) { for (var i = 0, len = $fallbackLanguage.length; i < len; i++) { if (!langPromises[$fallbackLanguage[i]]) { langPromises[$fallbackLanguage[i]] = loadAsync($fallbackLanguage[i]); } } } } $translate.use($translate.use()); } if ($fallbackWasString) { return $fallbackLanguage[0]; } else { return $fallbackLanguage; } }; /** * @ngdoc function * @name pascalprecht.translate.$translate#useFallbackLanguage * @methodOf pascalprecht.translate.$translate * * @description * Sets the first key of the fallback language stack to be used for translation. * Therefore all languages in the fallback array BEFORE this key will be skipped! * * @param {string=} langKey Contains the langKey the iteration shall start with. Set to false if you want to * get back to the whole stack */ $translate.useFallbackLanguage = function (langKey) { if (langKey !== undefined && langKey !== null) { if (!langKey) { startFallbackIteration = 0; } else { var langKeyPosition = indexOf($fallbackLanguage, langKey); if (langKeyPosition > -1) { startFallbackIteration = langKeyPosition; } } } }; /** * @ngdoc function * @name pascalprecht.translate.$translate#proposedLanguage * @methodOf pascalprecht.translate.$translate * * @description * Returns the language key of language that is currently loaded asynchronously. * * @return {string} language key */ $translate.proposedLanguage = function () { return $nextLang; }; /** * @ngdoc function * @name pascalprecht.translate.$translate#storage * @methodOf pascalprecht.translate.$translate * * @description * Returns registered storage. * * @return {object} Storage */ $translate.storage = function () { return Storage; }; /** * @ngdoc function * @name pascalprecht.translate.$translate#use * @methodOf pascalprecht.translate.$translate * * @description * Tells angular-translate which language to use by given language key. This method is * used to change language at runtime. It also takes care of storing the language * key in a configured store to let your app remember the choosed language. * * When trying to 'use' a language which isn't available it tries to load it * asynchronously with registered loaders. * * Returns promise object with loaded language file data * @example * $translate.use("en_US").then(function(data){ * $scope.text = $translate("HELLO"); * }); * * @param {string} key Language key * @return {string} Language key */ $translate.use = function (key) { if (!key) { return $uses; } var deferred = $q.defer(); $rootScope.$emit('$translateChangeStart', {language: key}); // Try to get the aliased language key var aliasedKey = negotiateLocale(key); if (aliasedKey) { key = aliasedKey; } // if there isn't a translation table for the language we've requested, // we load it asynchronously if (($forceAsyncReloadEnabled || !$translationTable[key]) && $loaderFactory && !langPromises[key]) { $nextLang = key; langPromises[key] = loadAsync(key).then(function (translation) { translations(translation.key, translation.table); deferred.resolve(translation.key); useLanguage(translation.key); return translation; }, function (key) { $rootScope.$emit('$translateChangeError', {language: key}); deferred.reject(key); $rootScope.$emit('$translateChangeEnd', {language: key}); return $q.reject(key); }); langPromises[key]['finally'](function () { clearNextLangAndPromise(key); }); } else if ($nextLang === key && langPromises[key]) { // we are already loading this asynchronously // resolve our new deferred when the old langPromise is resolved langPromises[key].then(function (translation) { deferred.resolve(translation.key); return translation; }, function (key) { deferred.reject(key); return $q.reject(key); }); } else { deferred.resolve(key); useLanguage(key); } return deferred.promise; }; /** * @ngdoc function * @name pascalprecht.translate.$translate#storageKey * @methodOf pascalprecht.translate.$translate * * @description * Returns the key for the storage. * * @return {string} storage key */ $translate.storageKey = function () { return storageKey(); }; /** * @ngdoc function * @name pascalprecht.translate.$translate#isPostCompilingEnabled * @methodOf pascalprecht.translate.$translate * * @description * Returns whether post compiling is enabled or not * * @return {bool} storage key */ $translate.isPostCompilingEnabled = function () { return $postCompilingEnabled; }; /** * @ngdoc function * @name pascalprecht.translate.$translate#isForceAsyncReloadEnabled * @methodOf pascalprecht.translate.$translate * * @description * Returns whether force async reload is enabled or not * * @return {boolean} forceAsyncReload value */ $translate.isForceAsyncReloadEnabled = function () { return $forceAsyncReloadEnabled; }; /** * @ngdoc function * @name pascalprecht.translate.$translate#refresh * @methodOf pascalprecht.translate.$translate * * @description * Refreshes a translation table pointed by the given langKey. If langKey is not specified, * the module will drop all existent translation tables and load new version of those which * are currently in use. * * Refresh means that the module will drop target translation table and try to load it again. * * In case there are no loaders registered the refresh() method will throw an Error. * * If the module is able to refresh translation tables refresh() method will broadcast * $translateRefreshStart and $translateRefreshEnd events. * * @example * // this will drop all currently existent translation tables and reload those which are * // currently in use * $translate.refresh(); * // this will refresh a translation table for the en_US language * $translate.refresh('en_US'); * * @param {string} langKey A language key of the table, which has to be refreshed * * @return {promise} Promise, which will be resolved in case a translation tables refreshing * process is finished successfully, and reject if not. */ $translate.refresh = function (langKey) { if (!$loaderFactory) { throw new Error('Couldn\'t refresh translation table, no loader registered!'); } var deferred = $q.defer(); function resolve() { deferred.resolve(); $rootScope.$emit('$translateRefreshEnd', {language: langKey}); } function reject() { deferred.reject(); $rootScope.$emit('$translateRefreshEnd', {language: langKey}); } $rootScope.$emit('$translateRefreshStart', {language: langKey}); if (!langKey) { // if there's no language key specified we refresh ALL THE THINGS! var tables = [], loadingKeys = {}; // reload registered fallback languages if ($fallbackLanguage && $fallbackLanguage.length) { for (var i = 0, len = $fallbackLanguage.length; i < len; i++) { tables.push(loadAsync($fallbackLanguage[i])); loadingKeys[$fallbackLanguage[i]] = true; } } // reload currently used language if ($uses && !loadingKeys[$uses]) { tables.push(loadAsync($uses)); } var allTranslationsLoaded = function (tableData) { $translationTable = {}; angular.forEach(tableData, function (data) { translations(data.key, data.table); }); if ($uses) { useLanguage($uses); } resolve(); }; allTranslationsLoaded.displayName = 'refreshPostProcessor'; $q.all(tables).then(allTranslationsLoaded, reject); } else if ($translationTable[langKey]) { var oneTranslationsLoaded = function (data) { translations(data.key, data.table); if (langKey === $uses) { useLanguage($uses); } resolve(); }; oneTranslationsLoaded.displayName = 'refreshPostProcessor'; loadAsync(langKey).then(oneTranslationsLoaded, reject); } else { reject(); } return deferred.promise; }; /** * @ngdoc function * @name pascalprecht.translate.$translate#instant * @methodOf pascalprecht.translate.$translate * * @description * Returns a translation instantly from the internal state of loaded translation. All rules * regarding the current language, the preferred language of even fallback languages will be * used except any promise handling. If a language was not found, an asynchronous loading * will be invoked in the background. * * @param {string|array} translationId A token which represents a translation id * This can be optionally an array of translation ids which * results that the function's promise returns an object where * each key is the translation id and the value the translation. * @param {object} interpolateParams Params * @param {string} interpolationId The id of the interpolation to use * * @return {string|object} translation */ $translate.instant = function (translationId, interpolateParams, interpolationId) { // Detect undefined and null values to shorten the execution and prevent exceptions if (translationId === null || angular.isUndefined(translationId)) { return translationId; } // Duck detection: If the first argument is an array, a bunch of translations was requested. // The result is an object. if (angular.isArray(translationId)) { var results = {}; for (var i = 0, c = translationId.length; i < c; i++) { results[translationId[i]] = $translate.instant(translationId[i], interpolateParams, interpolationId); } return results; } // We discarded unacceptable values. So we just need to verify if translationId is empty String if (angular.isString(translationId) && translationId.length < 1) { return translationId; } // trim off any whitespace if (translationId) { translationId = trim.apply(translationId); } var result, possibleLangKeys = []; if ($preferredLanguage) { possibleLangKeys.push($preferredLanguage); } if ($uses) { possibleLangKeys.push($uses); } if ($fallbackLanguage && $fallbackLanguage.length) { possibleLangKeys = possibleLangKeys.concat($fallbackLanguage); } for (var j = 0, d = possibleLangKeys.length; j < d; j++) { var possibleLangKey = possibleLangKeys[j]; if ($translationTable[possibleLangKey]) { if (typeof $translationTable[possibleLangKey][translationId] !== 'undefined') { result = determineTranslationInstant(translationId, interpolateParams, interpolationId); } else if ($notFoundIndicatorLeft || $notFoundIndicatorRight) { result = applyNotFoundIndicators(translationId); } } if (typeof result !== 'undefined') { break; } } if (!result && result !== '') { // Return translation of default interpolator if not found anything. result = defaultInterpolator.interpolate(translationId, interpolateParams); if ($missingTranslationHandlerFactory && !pendingLoader) { result = translateByHandler(translationId, interpolateParams); } } return result; }; /** * @ngdoc function * @name pascalprecht.translate.$translate#versionInfo * @methodOf pascalprecht.translate.$translate * * @description * Returns the current version information for the angular-translate library * * @return {string} angular-translate version */ $translate.versionInfo = function () { return version; }; /** * @ngdoc function * @name pascalprecht.translate.$translate#loaderCache * @methodOf pascalprecht.translate.$translate * * @description * Returns the defined loaderCache. * * @return {boolean|string|object} current value of loaderCache */ $translate.loaderCache = function () { return loaderCache; }; // internal purpose only $translate.directivePriority = function () { return directivePriority; }; // internal purpose only $translate.statefulFilter = function () { return statefulFilter; }; if ($loaderFactory) { // If at least one async loader is defined and there are no // (default) translations available we should try to load them. if (angular.equals($translationTable, {})) { $translate.use($translate.use()); } // Also, if there are any fallback language registered, we start // loading them asynchronously as soon as we can. if ($fallbackLanguage && $fallbackLanguage.length) { var processAsyncResult = function (translation) { translations(translation.key, translation.table); $rootScope.$emit('$translateChangeEnd', { language: translation.key }); return translation; }; for (var i = 0, len = $fallbackLanguage.length; i < len; i++) { var fallbackLanguageId = $fallbackLanguage[i]; if ($forceAsyncReloadEnabled || !$translationTable[fallbackLanguageId]) { langPromises[fallbackLanguageId] = loadAsync(fallbackLanguageId).then(processAsyncResult); } } } } return $translate; } ]; } $translate.$inject = ['$STORAGE_KEY', '$windowProvider', '$translateSanitizationProvider', 'pascalprechtTranslateOverrider']; $translate.displayName = 'displayName'; /** * @ngdoc object * @name pascalprecht.translate.$translateDefaultInterpolation * @requires $interpolate * * @description * Uses angular's `$interpolate` services to interpolate strings against some values. * * Be aware to configure a proper sanitization strategy. * * See also: * * {@link pascalprecht.translate.$translateSanitization} * * @return {object} $translateDefaultInterpolation Interpolator service */ angular.module('pascalprecht.translate').factory('$translateDefaultInterpolation', $translateDefaultInterpolation); function $translateDefaultInterpolation ($interpolate, $translateSanitization) { 'use strict'; var $translateInterpolator = {}, $locale, $identifier = 'default'; /** * @ngdoc function * @name pascalprecht.translate.$translateDefaultInterpolation#setLocale * @methodOf pascalprecht.translate.$translateDefaultInterpolation * * @description * Sets current locale (this is currently not use in this interpolation). * * @param {string} locale Language key or locale. */ $translateInterpolator.setLocale = function (locale) { $locale = locale; }; /** * @ngdoc function * @name pascalprecht.translate.$translateDefaultInterpolation#getInterpolationIdentifier * @methodOf pascalprecht.translate.$translateDefaultInterpolation * * @description * Returns an identifier for this interpolation service. * * @returns {string} $identifier */ $translateInterpolator.getInterpolationIdentifier = function () { return $identifier; }; /** * @deprecated will be removed in 3.0 * @see {@link pascalprecht.translate.$translateSanitization} */ $translateInterpolator.useSanitizeValueStrategy = function (value) { $translateSanitization.useStrategy(value); return this; }; /** * @ngdoc function * @name pascalprecht.translate.$translateDefaultInterpolation#interpolate * @methodOf pascalprecht.translate.$translateDefaultInterpolation * * @description * Interpolates given string agains given interpolate params using angulars * `$interpolate` service. * * @returns {string} interpolated string. */ $translateInterpolator.interpolate = function (string, interpolationParams) { interpolationParams = interpolationParams || {}; interpolationParams = $translateSanitization.sanitize(interpolationParams, 'params'); var interpolatedText = $interpolate(string)(interpolationParams); interpolatedText = $translateSanitization.sanitize(interpolatedText, 'text'); return interpolatedText; }; return $translateInterpolator; } $translateDefaultInterpolation.$inject = ['$interpolate', '$translateSanitization']; $translateDefaultInterpolation.displayName = '$translateDefaultInterpolation'; angular.module('pascalprecht.translate').constant('$STORAGE_KEY', 'NG_TRANSLATE_LANG_KEY'); angular.module('pascalprecht.translate') /** * @ngdoc directive * @name pascalprecht.translate.directive:translate * @requires $compile * @requires $filter * @requires $interpolate * @restrict A * * @description * Translates given translation id either through attribute or DOM content. * Internally it uses `translate` filter to translate translation id. It possible to * pass an optional `translate-values` object literal as string into translation id. * * @param {string=} translate Translation id which could be either string or interpolated string. * @param {string=} translate-values Values to pass into translation id. Can be passed as object literal string or interpolated object. * @param {string=} translate-attr-ATTR translate Translation id and put it into ATTR attribute. * @param {string=} translate-default will be used unless translation was successful * @param {boolean=} translate-compile (default true if present) defines locally activation of {@link pascalprecht.translate.$translateProvider#methods_usePostCompiling} * * @example

        
TRANSLATION_ID

        

        
{{translationId}}

        
WITH_VALUES

        
WITH_VALUES


      
angular.module('ngView', ['pascalprecht.translate']) .config(function ($translateProvider) { $translateProvider.translations('en',{ 'TRANSLATION_ID': 'Hello there!', 'WITH_VALUES': 'The following value is dynamic: {{value}}' }).preferredLanguage('en'); }); angular.module('ngView').controller('TranslateCtrl', function ($scope) { $scope.translationId = 'TRANSLATION_ID'; $scope.values = { value: 78 }; }); it('should translate', function () { inject(function ($rootScope, $compile) { $rootScope.translationId = 'TRANSLATION_ID'; element = $compile('

')($rootScope); $rootScope.$digest(); expect(element.text()).toBe('Hello there!'); element = $compile('

')($rootScope); $rootScope.$digest(); expect(element.text()).toBe('Hello there!'); element = $compile('

TRANSLATION_ID

')($rootScope); $rootScope.$digest(); expect(element.text()).toBe('Hello there!'); element = $compile('

{{translationId}}

')($rootScope); $rootScope.$digest(); expect(element.text()).toBe('Hello there!'); element = $compile('

')($rootScope); $rootScope.$digest(); expect(element.attr('title')).toBe('Hello there!'); }); });
*/ .directive('translate', translateDirective); function translateDirective($translate, $q, $interpolate, $compile, $parse, $rootScope) { 'use strict'; /** * @name trim * @private * * @description * trim polyfill * * @returns {string} The string stripped of whitespace from both ends */ var trim = function() { return this.toString().replace(/^\s+|\s+$/g, ''); }; return { restrict: 'AE', scope: true, priority: $translate.directivePriority(), compile: function (tElement, tAttr) { var translateValuesExist = (tAttr.translateValues) ? tAttr.translateValues : undefined; var translateInterpolation = (tAttr.translateInterpolation) ? tAttr.translateInterpolation : undefined; var translateValueExist = tElement[0].outerHTML.match(/translate-value-+/i); var interpolateRegExp = '^(.*)(' + $interpolate.startSymbol() + '.*' + $interpolate.endSymbol() + ')(.*)', watcherRegExp = '^(.*)' + $interpolate.startSymbol() + '(.*)' + $interpolate.endSymbol() + '(.*)'; return function linkFn(scope, iElement, iAttr) { scope.interpolateParams = {}; scope.preText = ''; scope.postText = ''; var translationIds = {}; var initInterpolationParams = function (interpolateParams, iAttr, tAttr) { // initial setup if (iAttr.translateValues) { angular.extend(interpolateParams, $parse(iAttr.translateValues)(scope.$parent)); } // initially fetch all attributes if existing and fill the params if (translateValueExist) { for (var attr in tAttr) { if (Object.prototype.hasOwnProperty.call(iAttr, attr) && attr.substr(0, 14) === 'translateValue' && attr !== 'translateValues') { var attributeName = angular.lowercase(attr.substr(14, 1)) + attr.substr(15); interpolateParams[attributeName] = tAttr[attr]; } } } }; // Ensures any change of the attribute "translate" containing the id will // be re-stored to the scope's "translationId". // If the attribute has no content, the element's text value (white spaces trimmed off) will be used. var observeElementTranslation = function (translationId) { // Remove any old watcher if (angular.isFunction(observeElementTranslation._unwatchOld)) { observeElementTranslation._unwatchOld(); observeElementTranslation._unwatchOld = undefined; } if (angular.equals(translationId , '') || !angular.isDefined(translationId)) { // Resolve translation id by inner html if required var interpolateMatches = trim.apply(iElement.text()).match(interpolateRegExp); // Interpolate translation id if required if (angular.isArray(interpolateMatches)) { scope.preText = interpolateMatches[1]; scope.postText = interpolateMatches[3]; translationIds.translate = $interpolate(interpolateMatches[2])(scope.$parent); var watcherMatches = iElement.text().match(watcherRegExp); if (angular.isArray(watcherMatches) && watcherMatches[2] && watcherMatches[2].length) { observeElementTranslation._unwatchOld = scope.$watch(watcherMatches[2], function (newValue) { translationIds.translate = newValue; updateTranslations(); }); } } else { translationIds.translate = iElement.text().replace(/^\s+|\s+$/g,''); } } else { translationIds.translate = translationId; } updateTranslations(); }; var observeAttributeTranslation = function (translateAttr) { iAttr.$observe(translateAttr, function (translationId) { translationIds[translateAttr] = translationId; updateTranslations(); }); }; // initial setup with values initInterpolationParams(scope.interpolateParams, iAttr, tAttr); var firstAttributeChangedEvent = true; iAttr.$observe('translate', function (translationId) { if (typeof translationId === 'undefined') { // case of element "xyz" observeElementTranslation(''); } else { // case of regular attribute if (translationId !== '' || !firstAttributeChangedEvent) { translationIds.translate = translationId; updateTranslations(); } } firstAttributeChangedEvent = false; }); for (var translateAttr in iAttr) { if (iAttr.hasOwnProperty(translateAttr) && translateAttr.substr(0, 13) === 'translateAttr') { observeAttributeTranslation(translateAttr); } } iAttr.$observe('translateDefault', function (value) { scope.defaultText = value; }); if (translateValuesExist) { iAttr.$observe('translateValues', function (interpolateParams) { if (interpolateParams) { scope.$parent.$watch(function () { angular.extend(scope.interpolateParams, $parse(interpolateParams)(scope.$parent)); }); } }); } if (translateValueExist) { var observeValueAttribute = function (attrName) { iAttr.$observe(attrName, function (value) { var attributeName = angular.lowercase(attrName.substr(14, 1)) + attrName.substr(15); scope.interpolateParams[attributeName] = value; }); }; for (var attr in iAttr) { if (Object.prototype.hasOwnProperty.call(iAttr, attr) && attr.substr(0, 14) === 'translateValue' && attr !== 'translateValues') { observeValueAttribute(attr); } } } // Master update function var updateTranslations = function () { for (var key in translationIds) { if (translationIds.hasOwnProperty(key) && translationIds[key] !== undefined) { updateTranslation(key, translationIds[key], scope, scope.interpolateParams, scope.defaultText); } } }; // Put translation processing function outside loop var updateTranslation = function(translateAttr, translationId, scope, interpolateParams, defaultTranslationText) { if (translationId) { $translate(translationId, interpolateParams, translateInterpolation, defaultTranslationText) .then(function (translation) { applyTranslation(translation, scope, true, translateAttr); }, function (translationId) { applyTranslation(translationId, scope, false, translateAttr); }); } else { // as an empty string cannot be translated, we can solve this using successful=false applyTranslation(translationId, scope, false, translateAttr); } }; var applyTranslation = function (value, scope, successful, translateAttr) { if (translateAttr === 'translate') { // default translate into innerHTML if (!successful && typeof scope.defaultText !== 'undefined') { value = scope.defaultText; } iElement.html(scope.preText + value + scope.postText); var globallyEnabled = $translate.isPostCompilingEnabled(); var locallyDefined = typeof tAttr.translateCompile !== 'undefined'; var locallyEnabled = locallyDefined && tAttr.translateCompile !== 'false'; if ((globallyEnabled && !locallyDefined) || locallyEnabled) { $compile(iElement.contents())(scope); } } else { // translate attribute if (!successful && typeof scope.defaultText !== 'undefined') { value = scope.defaultText; } var attributeName = iAttr.$attr[translateAttr]; if (attributeName.substr(0, 5) === 'data-') { // ensure html5 data prefix is stripped attributeName = attributeName.substr(5); } attributeName = attributeName.substr(15); iElement.attr(attributeName, value); } }; if (translateValuesExist || translateValueExist || iAttr.translateDefault) { scope.$watch('interpolateParams', updateTranslations, true); } // Ensures the text will be refreshed after the current language was changed // w/ $translate.use(...) var unbind = $rootScope.$on('$translateChangeSuccess', updateTranslations); // ensure translation will be looked up at least one if (iElement.text().length) { if (iAttr.translate) { observeElementTranslation(iAttr.translate); } else { observeElementTranslation(''); } } else if (iAttr.translate) { // ensure attribute will be not skipped observeElementTranslation(iAttr.translate); } updateTranslations(); scope.$on('$destroy', unbind); }; } }; } translateDirective.$inject = ['$translate', '$q', '$interpolate', '$compile', '$parse', '$rootScope']; translateDirective.displayName = 'translateDirective'; angular.module('pascalprecht.translate') /** * @ngdoc directive * @name pascalprecht.translate.directive:translateCloak * @requires $rootScope * @requires $translate * @restrict A * * $description * Adds a `translate-cloak` class name to the given element where this directive * is applied initially and removes it, once a loader has finished loading. * * This directive can be used to prevent initial flickering when loading translation * data asynchronously. * * The class name is defined in * {@link pascalprecht.translate.$translateProvider#cloakClassName $translate.cloakClassName()}. * * @param {string=} translate-cloak If a translationId is provided, it will be used for showing * or hiding the cloak. Basically it relies on the translation * resolve. */ .directive('translateCloak', translateCloakDirective); function translateCloakDirective($rootScope, $translate) { 'use strict'; return { compile: function (tElement) { var applyCloak = function () { tElement.addClass($translate.cloakClassName()); }, removeCloak = function () { tElement.removeClass($translate.cloakClassName()); }, removeListener = $rootScope.$on('$translateChangeEnd', function () { removeCloak(); removeListener(); removeListener = null; }); applyCloak(); return function linkFn(scope, iElement, iAttr) { // Register a watcher for the defined translation allowing a fine tuned cloak if (iAttr.translateCloak && iAttr.translateCloak.length) { iAttr.$observe('translateCloak', function (translationId) { $translate(translationId).then(removeCloak, applyCloak); }); } }; } }; } translateCloakDirective.$inject = ['$rootScope', '$translate']; translateCloakDirective.displayName = 'translateCloakDirective'; angular.module('pascalprecht.translate') /** * @ngdoc filter * @name pascalprecht.translate.filter:translate * @requires $parse * @requires pascalprecht.translate.$translate * @function * * @description * Uses `$translate` service to translate contents. Accepts interpolate parameters * to pass dynamized values though translation. * * @param {string} translationId A translation id to be translated. * @param {*=} interpolateParams Optional object literal (as hash or string) to pass values into translation. * * @returns {string} Translated text. * * @example
{{ 'TRANSLATION_ID' | translate }}
{{ translationId | translate }}
{{ 'WITH_VALUES' | translate:'{value: 5}' }}
{{ 'WITH_VALUES' | translate:values }}
angular.module('ngView', ['pascalprecht.translate']) .config(function ($translateProvider) { $translateProvider.translations('en', { 'TRANSLATION_ID': 'Hello there!', 'WITH_VALUES': 'The following value is dynamic: {{value}}' }); $translateProvider.preferredLanguage('en'); }); angular.module('ngView').controller('TranslateCtrl', function ($scope) { $scope.translationId = 'TRANSLATION_ID'; $scope.values = { value: 78 }; });
*/ .filter('translate', translateFilterFactory); function translateFilterFactory($parse, $translate) { 'use strict'; var translateFilter = function (translationId, interpolateParams, interpolation) { if (!angular.isObject(interpolateParams)) { interpolateParams = $parse(interpolateParams)(this); } return $translate.instant(translationId, interpolateParams, interpolation); }; if ($translate.statefulFilter()) { translateFilter.$stateful = true; } return translateFilter; } translateFilterFactory.$inject = ['$parse', '$translate']; translateFilterFactory.displayName = 'translateFilterFactory'; angular.module('pascalprecht.translate') /** * @ngdoc object * @name pascalprecht.translate.$translationCache * @requires $cacheFactory * * @description * The first time a translation table is used, it is loaded in the translation cache for quick retrieval. You * can load translation tables directly into the cache by consuming the * `$translationCache` service directly. * * @return {object} $cacheFactory object. */ .factory('$translationCache', $translationCache); function $translationCache($cacheFactory) { 'use strict'; return $cacheFactory('translations'); } $translationCache.$inject = ['$cacheFactory']; $translationCache.displayName = '$translationCache'; return 'pascalprecht.translate'; })); ././@LongLink0100644 0000000 0000000 00000000160 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/lib/angular-translate/angular-translate-loader-static-files.jsguacamole-client-0.9.9/guacamole/src/main/webapp/lib/angular-translate/angular-translate-loader-stat0100664 0001751 0001751 00000006012 12612226251 035431 0ustar00mjumpermjumper0000000 0000000 /*! * angular-translate - v2.7.2 - 2015-06-01 * http://github.com/angular-translate/angular-translate * Copyright (c) 2015 ; Licensed MIT */ (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module unless amdModuleId is set define([], function () { return (factory()); }); } else if (typeof exports === 'object') { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. module.exports = factory(); } else { factory(); } }(this, function () { angular.module('pascalprecht.translate') /** * @ngdoc object * @name pascalprecht.translate.$translateStaticFilesLoader * @requires $q * @requires $http * * @description * Creates a loading function for a typical static file url pattern: * "lang-en_US.json", "lang-de_DE.json", etc. Using this builder, * the response of these urls must be an object of key-value pairs. * * @param {object} options Options object, which gets prefix, suffix and key. */ .factory('$translateStaticFilesLoader', $translateStaticFilesLoader); function $translateStaticFilesLoader($q, $http) { 'use strict'; return function (options) { if (!options || (!angular.isArray(options.files) && (!angular.isString(options.prefix) || !angular.isString(options.suffix)))) { throw new Error('Couldn\'t load static files, no files and prefix or suffix specified!'); } if (!options.files) { options.files = [{ prefix: options.prefix, suffix: options.suffix }]; } var load = function (file) { if (!file || (!angular.isString(file.prefix) || !angular.isString(file.suffix))) { throw new Error('Couldn\'t load static file, no prefix or suffix specified!'); } var deferred = $q.defer(); $http(angular.extend({ url: [ file.prefix, options.key, file.suffix ].join(''), method: 'GET', params: '' }, options.$http)).success(function (data) { deferred.resolve(data); }).error(function () { deferred.reject(options.key); }); return deferred.promise; }; var deferred = $q.defer(), promises = [], length = options.files.length; for (var i = 0; i < length; i++) { promises.push(load({ prefix: options.files[i].prefix, key: options.key, suffix: options.files[i].suffix })); } $q.all(promises).then(function (data) { var length = data.length, mergedData = {}; for (var i = 0; i < length; i++) { for (var key in data[i]) { mergedData[key] = data[i][key]; } } deferred.resolve(mergedData); }, function (data) { deferred.reject(data); }); return deferred.promise; }; } $translateStaticFilesLoader.$inject = ['$q', '$http']; $translateStaticFilesLoader.displayName = '$translateStaticFilesLoader'; return 'pascalprecht.translate'; })); ././@LongLink0100644 0000000 0000000 00000000170 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/lib/angular-translate/angular-translate-interpolation-messageformat.jsguacamole-client-0.9.9/guacamole/src/main/webapp/lib/angular-translate/angular-translate-interpolati0100664 0001751 0001751 00000012063 12612226251 035547 0ustar00mjumpermjumper0000000 0000000 /*! * angular-translate - v2.7.2 - 2015-06-01 * http://github.com/angular-translate/angular-translate * Copyright (c) 2015 ; Licensed MIT */ (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module unless amdModuleId is set define([], function () { return (factory()); }); } else if (typeof exports === 'object') { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. module.exports = factory(); } else { factory(); } }(this, function () { angular.module('pascalprecht.translate') /** * @ngdoc property * @name pascalprecht.translate.TRANSLATE_MF_INTERPOLATION_CACHE * @requires TRANSLATE_MF_INTERPOLATION_CACHE * * @description * Uses MessageFormat.js to interpolate strings against some values. */ .constant('TRANSLATE_MF_INTERPOLATION_CACHE', '$translateMessageFormatInterpolation') /** * @ngdoc object * @name pascalprecht.translate.$translateMessageFormatInterpolation * @requires pascalprecht.translate.TRANSLATE_MF_INTERPOLATION_CACHE * * @description * Uses MessageFormat.js to interpolate strings against some values. * * Be aware to configure a proper sanitization strategy. * * See also: * * {@link pascalprecht.translate.$translateSanitization} * * {@link https://github.com/SlexAxton/messageformat.js} * * @return {object} $translateMessageFormatInterpolation Interpolator service */ .factory('$translateMessageFormatInterpolation', $translateMessageFormatInterpolation); function $translateMessageFormatInterpolation($translateSanitization, $cacheFactory, TRANSLATE_MF_INTERPOLATION_CACHE) { 'use strict'; var $translateInterpolator = {}, $cache = $cacheFactory.get(TRANSLATE_MF_INTERPOLATION_CACHE), // instantiate with default locale (which is 'en') $mf = new MessageFormat('en'), $identifier = 'messageformat'; if (!$cache) { // create cache if it doesn't exist already $cache = $cacheFactory(TRANSLATE_MF_INTERPOLATION_CACHE); } $cache.put('en', $mf); /** * @ngdoc function * @name pascalprecht.translate.$translateMessageFormatInterpolation#setLocale * @methodOf pascalprecht.translate.$translateMessageFormatInterpolation * * @description * Sets current locale (this is currently not use in this interpolation). * * @param {string} locale Language key or locale. */ $translateInterpolator.setLocale = function (locale) { $mf = $cache.get(locale); if (!$mf) { $mf = new MessageFormat(locale); $cache.put(locale, $mf); } }; /** * @ngdoc function * @name pascalprecht.translate.$translateMessageFormatInterpolation#getInterpolationIdentifier * @methodOf pascalprecht.translate.$translateMessageFormatInterpolation * * @description * Returns an identifier for this interpolation service. * * @returns {string} $identifier */ $translateInterpolator.getInterpolationIdentifier = function () { return $identifier; }; /** * @deprecated will be removed in 3.0 * @see {@link pascalprecht.translate.$translateSanitization} */ $translateInterpolator.useSanitizeValueStrategy = function (value) { $translateSanitization.useStrategy(value); return this; }; /** * @ngdoc function * @name pascalprecht.translate.$translateMessageFormatInterpolation#interpolate * @methodOf pascalprecht.translate.$translateMessageFormatInterpolation * * @description * Interpolates given string agains given interpolate params using MessageFormat.js. * * @returns {string} interpolated string. */ $translateInterpolator.interpolate = function (string, interpolationParams) { interpolationParams = interpolationParams || {}; interpolationParams = $translateSanitization.sanitize(interpolationParams, 'params'); var interpolatedText = $cache.get(string + angular.toJson(interpolationParams)); // if given string wasn't interpolated yet, we do so now and never have to do it again if (!interpolatedText) { // Ensure explicit type if possible // MessageFormat checks the actual type (i.e. for amount based conditions) for (var key in interpolationParams) { if (interpolationParams.hasOwnProperty(key)) { // ensure number var number = parseInt(interpolationParams[key], 10); if (angular.isNumber(number) && ('' + number) === interpolationParams[key]) { interpolationParams[key] = number; } } } interpolatedText = $mf.compile(string)(interpolationParams); interpolatedText = $translateSanitization.sanitize(interpolatedText, 'text'); $cache.put(string + angular.toJson(interpolationParams), interpolatedText); } return interpolatedText; }; return $translateInterpolator; } $translateMessageFormatInterpolation.$inject = ['$translateSanitization', '$cacheFactory', 'TRANSLATE_MF_INTERPOLATION_CACHE']; $translateMessageFormatInterpolation.displayName = '$translateMessageFormatInterpolation'; return 'pascalprecht.translate'; })); guacamole-client-0.9.9/guacamole/src/main/webapp/lib/angular-translate/LICENSE0100664 0001751 0001751 00000002106 12612226251 030652 0ustar00mjumpermjumper0000000 0000000 The MIT License (MIT) Copyright (c) <2014> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. guacamole-client-0.9.9/guacamole/src/main/webapp/images/arrows/up.png0100664 0001751 0001751 00000000355 12612226251 027373 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞbKGDÿÿÿ ½§“ pHYs × ×B(›xtIMEß!5e¡÷zIDATxÚí×± €0 EAoM ›CC‰I|'y€÷;GÔ±ì—Òë~sæøt#ŧá,~ø®Ä;ÂøáF(‰f„'ñÝðF|·#¼ßÝ_Äw3—ñÍP#¾ÙjÆ77Âñi¿H ÜÆõþ{S‚IEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/arrows/down.png0100664 0001751 0001751 00000000432 12612226251 027712 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<—IDATxœíÐ;Â@DÁFšq4ÎÑH°½ª¤w^'˒ܽó}‹´¡›ø²çÝÅ—=Fè6¾l9B÷ñe‹†‰/kŽ0\|Yc„aãË/# _¾ašøòÉÓÅ—%#L_Þ0}|y5ÂßÄ—çšÅ[}œä–ääšäÒðàO<#`ÿ†¯O;IEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/user-icons/guac-user.png0100664 0001751 0001751 00000002031 12440135707 031411 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<–IDATxœí›ÍK•AƧ/©E™…Ñ&oßTX`‹"F-Äýnûlýn"(J\µj£Q …eR$öEE(ž3‚Õ}ß¼÷œ÷¾÷æ}àp‘¹óÌsž;ï̼3£¨*«kò7êä- o¬«dc"r8ìv-±hx <†UõiÅD©j¦4WW€®0^Å:™ëË0ñà2ð±„ÄÿŒ‘£¡¦ tí1CâÆÐRmÀ´còK1 ´yë•(Ú"²¶¸‘þŽÏ@»ªNzºMƒ"Ò ‘]òDî¡Ø– <׃@Á‘/ …Ø–œžû.üŸùEWUŒ"²˜ŽšˆJÇส.ZH<n*Ÿ<±Ín+‰‡8rkÛôˆˆÖñ;¬BÊÄ;©ì$¬=àù%Olû€…ÀjÀ.c}˜4X Øi¬ï“«ÍÆú0i°ðÉXß& VfŒõ=`Ò°ê °®6³À&‹¾Mªú£\Sˆ ß·pqß’<ø,…ï:päÖ¶ÇÛàf` h²Š)³@«ª~±˜{@0`å)ÖäÁ¡ˆÈzà°ÇL¶2¼©ê¼•ÈeK, é!ŒÊYã;Ðã‘<8î ªêc °]•è‹m91úŸ ô øï.ýîz½ £ ØŽÄŠ‘uf¢5 ÒhBvH~(d¦3+âeFœFËH|8›µ>×£±4ˆÈnÂ&æyÂt¹X‹ç÷„ém¸£ª/+¢«RüÕpØP]:âšÓœ„df€ˆlZ GåÍ„_|[ü\ú» ø ̺bŸsÀ3U}‰N/Dd-pèNíø”.“ÀxŒ‡ª:îAìñ2t¸\¤r›¤‹„Aò.pSU§Êf2Œî½À=ÂVéƒÑåñ¸¬È4Ág~÷Ž/À5`k&Þ®_« Ù´˜޹@¸O8XÉ­4Þ.®»Ýª‚¤JYà”‡׫ ™rãÉ€÷­ ±D¯Å€ÛU€5&ÒrL\‰È JCÑ/Ô—(>+LÛk£ö“‡0ƒO*L3`Ÿ¿–Üx1Í€<¯¾xc{RAšÅ‡ÚDb.i˜. VsYõÿ3Tï)•êc@BòB½$Ô (§R ¢n@RAÚÛàIÂÞþÿ€U}T¬àïIsÿ7áIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/cog.png0100664 0001751 0001751 00000011053 12612226251 026177 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDRö{`íbKGDÿ‡Ì¿ pHYs7]7]€F]tIMEÞ  #9²Åów¼IDATxÚíi[[GÒ†o­€$@ ˜eðîdH<¯¯wþÿ/˜¹K—àFŽUßµ§4ipª˜eVhÓur=O•…¾á˜MΘáZtf‡evHcº´iO©R³T©9¾ôñŽ#4§þ‚ JÖâiH(þOèӦͱ'•¿M54lB“מLB •ÿñ)¡@"âÿˆ#³c;”  ¼W÷å+RÍ @"4âÿ´_3 y¶¦z¹G“ל\«î¿öõ>S 2ñÍ,„]øW‘àÏÔ}h(©ø?™…6mzäØf%BÞU‹×œR¤BÅse‘¯H„ZüŸ–$Š˱Å÷úFTÄÕpUÂâ/«s؉áR*þÙ¦@JÅ?ÛH©øg›)ÿlS ¥âŸm ¤Tü³M”ж)RñÏ6dÄ´¢âÔŠÌc%ÐVyEeUeÐw{Õ@0¦½ìLChSIÙj5YQ%ÀŒ@ª[ø~èêèFœrÆ)§\2bĈKF@Š4)R¤H“#ǹÐULõ¥ª‘Ò‚Œ &Ñã“+—ð’Ë/~–%Ï2E–B’Œ"¦Q% p3àE;¦KCFSi°!] Å2EJTEœÇ k´>䀃+Ò³½š Ï*«‡Ì0ðT4ã ’ î>7ð–[ÔÅœÐkD9ßñKô4´}&Àˆ}ÞŠ–nwé’e“uŸ‰‚>UZt¹üKç¾d=.|1//ù 6|›·$V“dòħ«c^óœŽáç1=Þ1öÉGèpM@J¨aÛçhñ„vwci0ïÅßÊ¥ÇÆIÇÏÙ ¦­Âïÿÿ)eî9šLÈ*Êj€Ñ•qææ ÏCЦåŒwÀ²˜1èó6º€¬Ð¼O8`B0¡G’6}Ça” 0b]äXôSÈZ4 8 Ï‚À“_Èö$“&ÀŽwÆ„ÿ°„“1 Æ›‚/e_[> ±àôRè’'!žå{Ä¡ãHaCú+¨qy†ò/z„=þåTe‹çU$}XWêúŒÇN.xdqÂcgéÛcyºË‡3sŽHÖç'G¡ÞæÈ’eŽ,0dÀ!.œøç<æ{'†/IN2ô~1¤±ã$Ÿ½ÏSÝ뾘EV(0GæÊ¯žpÁ€cZÖz+ÅßœP ÅÓh Ïß)ÿéwš2+”=xGthÑù“\!SdxäÄ-ü‡¬Ù“&À_4vòxÊŽ¼YªT,\³ =ÚS7‚瑃’&Ï¢ëæ¹çÀñûaªCUš›|K…«fM ”Ù M “pI×ÁÛ<-ÉknYܳžÞ1á ý)ìý&;”é·ˬÇžˆôL0ÊÐŒ&röÿ¦û¬òUÇn’k\x¶ÇçŒ)Y뀦œ$À]ëýßa×ã_”Ùa]ȹM³B•séˆ%ëÃ`†Vôã¾åüäÉò&¸Ç]áÌÝ,«d=¦hu©YRRP¤B»ÿ'<ñtã—æ{ŸZI/²ì)iìà$ ¦Rb;åå'¿ñ” —sx1uðªt=ìÉ ˼ˆï,aWnd`;Íãœ7ž,ÿ#‘»ø«±À#OäßXÎJHi7) p×2ÌK²×&(æLRcÄ‘±A;³<›¦ 0o9ÊÉË­»Ü ¨€3Áîzðhì¬ø’Lê© ìÔÕ˜vÿAbƒMãß}ayÁTìF:™ÛË2· ·ÏÞÎ5®WÕG䬊©/Ù3þ?߆ z?Á·ÆÅ!{·‹P(BI†nÿ›.Ršï|«Îsõ&æäöMHÀÆV —(ÁŽÏŽß×ÂC]´gåÍW£@;Eµo^¹+Tp2-ІþÀûWŸ`£¦Æ†ePe‘r;¬ßZù΀ÛÊ"Ûܰ UÓ0ü»X³–¯ë>“Ý="oqKR`Ž —‰ç®aûl³h¥S^9€«!ÜÿYÎòF1Á$‹Ü J’37éööçèeÖ¨8pȆF׬InVÜ¢i –.CK –ç·iS§c›Èn§rlñ 7È9ñÇ÷°âùÁiÃÛ,Ë6^Ž7È2°ÉHMýwkÜã6ËO»’ù(ŠEÞ耡3#–b™ Ê$8›NL#¾"7ù†Ç­Žymð[7­3ìd‘$a Ç.XqzŒ£Â&9.½_:§=þ£5Ö„¢˜¨ÿlÀW?&Ø0jU×uîÑ'Ye•sêÔ¸×IªÜáE±ðëƒdË5šNÙÛæA"{Â2;àê ]d“e&¦&ÁTœß «Þ‰Q#”ð‹ÿý[^Î>d"x‘U¢D—Ív¶)¯eqd#O‡,ü{õéúmežK4½&Â%¤aR_Iëv“Ȉ›/öåÔ˜€¢‚G_²ê|Žë»æŒ¥§è:DÇ G@>¯˜ẗŽh9úâ"@Ñh_E#01ÒUÅh@zé— ,âeÈ'‡:­\Ÿê‘²l¨ïLb¦‘&§¤—‡…éHÔî4ˆ( 5ˆx.¯xå¶AĘ Ñ1EÅÙävèSXÆFc¥ €X‹˜îÍ!{’pÔîoòÎàÿ§ÃrxºoŽ€·'¹ið‚zs—§¹èÑc—kNíX¼AÉôn„¤IüUç“¡yÇ•ŽGÔiL'™61á˜:MÆ,8 hŒ :…!Ôâ^T·œé±!ûüÂoSÌ5¶$À{\Ðe>I«Ý±`Ô+ü˜µÐê€? â›fB›—ìzš_èœïqF“}†ÌYÚµG'\„¶WÀ¯F—@%ë>¡'üÆÏÔ]˜»RßcŽØ§Ã„œÅ9=at€:¡Ê:ÃQ×·,ÂÀ#êìòŠ#7½Â]§% éXÓ¨eÎE½èiño£™å¾…¹Üã…ÛüH÷^uÃêm̦ðu¬¯È`ßð²jÓjÍ®_Û=Ž­¬6‚ø5daá¿ý^ÆÊþŸºO‹‘ˆ«5-þ6eØzÂÓpôØùp ~jè†mXݦûW— €šÚ0žÁ÷$õµÞ$mÙí¼ Ø)*óE:åy.ˆ'&oXE0Ž%º É\­Ø1õ/Æ×M^N€—Æ™Jóü%lû_ŠMËwºcü»o-‡±ÚbÏpÒ!ÀËÕnJ|€Lbâ%e«ž9úÆG¼#Jô›ð’WÆ¿]¶œvväjR–©˼3¶ïGô©øž'pÉ3Ã1—ïµÚ÷–7¿¤™„ˆ6­ve/õÏhSöµ•ä?zù°my1á™´x)ŒÈY&>/ÓóÙrÁKB¹J_¢ÇžJÕ–y`i¤š´M(§–>o‚®<Æ4顏Ŀ÷yîé*&ÃCë ìgRűr¸°Öiò]Ÿ} ¢7…'ü۳߱cMˆAº\è`¯`±ÇáJgìs΢HÊÈ€_Ùõ€Þr0%ô™\q¼$.¬¥¿G‘CÏÓ0OØgd9ÇüË3ÿ+~ž"ÂYäkµ)y÷)[ xjÍþ+t<óÂïH°è$>0æ-ÏèMvÎóÐÁ ?—lŽ!K: É ­)®}Æt©3 ÁüÔ4˜Ðc_hM•3Ïÿ9pM›²¹Ò´<wâu?¶ØiʬPöDöZt,î3GV& À?Ør~l-±D×UeŒ3dØaÝù¡:M[¶Q¶4nˆX±jôCÕ;t™‡B7‘CÙAóÒ¸#4î)Í* ŽBÑ6*É6Äü©”ÜM ¼˜¼ŸO°Í*»w,sO4e‘¬¤Õ5*ÂQŒU ô+™ç·Äc)g’Á YÜô¡nŽuœø~(L³Å_}ŠK4$?CÒ6úÓÌ%ÉM6ÙcÏ·™6,«|¼ DRŽÞ’ ìc’"ë¤9o)e›o)ù˜†žðP%*lù’¢ù9 –Ù ÏH,VXâ÷·M1¢EDWÏ$5j 9àÀé=zžUVkL#¸’r(8ð9Ë[Ó¥çµqâr™"%Ç}ý¼bŽ‚”' G€ A£@-&ÑãOÞt–<ËY ¤êðÏVS 0õjùCÛª§œqÊ)—Œ1â’"MŠ)Òäȱ@.Ó<þ°š¯£E€¬Ï@3u¾·2ƒX<0óý­¨@ òÔ’JÌy "”B?Ö!zÚT2‚R™U•!@+t [ÑÇXflžŒ¿{Æ1Õ„Pâ"þ§2©/R¥@$Ä/y¨ˆ€øe¯ƒ•¡¿tJ˜R äâ—O W „Zü~”†)B,~|¹öT „Vüøtï­©øñ-ñÁŽãH’glñ½Oýªxò/óe: hðŠ]Zd}(2q‡ÏyÉ#²ž“n|?>ï¬2;Æ·}Ú´ÿ —g›j„ßäõ2’ T¨ç"ù*~|W­×S`L—ö•Uña'Áÿ ÿ²T¨\{Qî³ø À¶^MÚ­rlS ¡ð¼¾¶©S’•+ç©ø.~9\}I/Õ=$_1¡i |®1 ˆŸ€–ð#¾®î¯C–*5–þ šSÁ'³ˆø l•Y1R÷×cž*µ@*wŽiÐtP‡øÞ,´‚iu—àLŽUßÅSš4ä›8*¼¢@uÑØÆˆ}Òí[ýD*VÒeN´ú§Î Ù¶m~#~éÛõ?] à}Áþú'2ÕQÙ¥õø-V p Ô>r"5¼Q àBuÚ¾u!S„RU×ã¸Tñ$@GÀUÞ”V ¨µ>Ecj%@`êºÏ…Š+NO9ŠGävàzÇÖãºLñ%@Ãa‚±d¿n%€ F4=«)Þ‚Z j#Pï"Å™=G]ÃÏé)fYÔã¼DJ%@Œ1 kýŒn¨æ*|ß½õx/P:æhѵJ|ÈtçS( …B¡P( …B¡P( …B¡P( …B¡P( …Â9þ FZ“䤩QIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/file.png0100664 0001751 0001751 00000000727 12612226251 026354 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<TIDATxœíÛ1NÃ0…á"Kâ»°"%S%&FN‘#„)’opGÊŠY‚‘Ä)’`‚ºØ<Þ7;îë¯Z@DDþ¯³Èó—9†áÀsêK/"Ïß¶m;t]—zÇ—ö}Ç4M¯Û¶Ýà=Íý0 a]×`ŒÙ\§|Cç)/Ëižg,ËRcF(&€µ6K„by"H¡¸@ÚEÒE(6&BÑ€ιºià*æùØo‚4!„oÏô}_ãxðtì½Å>«ª*ú™?à ÀÀ¦ìl ÀÀ¦ìl ÀÀ¦ìl ÀÀ¦ìl ÀÀ¦ìl ÀÀ¦ìl ÀÀ¦ìl ÀÀ¦ìl ÀÀ¦ìlÑ?“óÞÃZ›cËy9å/3wѯò»¼°GˆˆH Þ4—Îéùò^IEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/lock.png0100664 0001751 0001751 00000000777 12612226251 026372 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<|IDATxœíÙ1NÃ0Æñ±L ¡2 î'à œq*Ä*±#̈†¨… *ÊwqS’´yyP}?Ƀ#Ûùüš&Q """""""bl8n€.ðZ7; cæÒІ9­ì;e4±\’¿ñ¸]„¹ÿÞ9å7?jgÖáÆëï·ÇßðúëÀ°:a{‹puxdüSmÍŒ±Mà*cüC-I l2¾™¿_u Ò§A<¯ešÔÈ)ãÙ*0o;cÞ‰QFÓ»ìZÔž Ì{z9kUƲIÔÿ(1·õWfÌ2‘eâïú°ÄÜx¬ÙÓj©âõ6€ÒÂÆ7®8,¸N|õ´ÂÜoàx™>¢EнêÎÚzá\N‚ýæG-¾B¦6ïÚ³P¼xS¼xS¼xS¼xS¼xS¼xS¼xS¼xS*\kPáZy¾jäÇV·©êÜäµêÁ;@ ˆÈàl쪷SÄnV¿îVÕ¥ ©—"2˜l±‹.àZà2U]œ˜bexH9ø!°a‚Ý.þLÆæËÏ¥eI""ßJ «w€+€'ýŒà !Dd+à'À¾ ¹s†±¹óسD­ŸK€7Tõ½,”‘€W€A v;8[UïMªCï1‘±À)ÀqÀ€œÕqa°˜<ÈUµ+ÉADä»Àג쳌ßçªê‚¸yˆ€ˆl œ 옫2Éð"p‚ª>™DgÁ3ÐlÒ½ ¼ƒ9ØO4†{D¤¶9  þK± ¶vÆä fÚtW«êŠ8‰ÈUÀ—Ѫ1ÇF]-jK‘ÑØÒä˜Q$W¥ŠÃ À1ª:=ʇE¤?0Ÿdñp¼ªÞú“ªÚòt€›€aWt/µe9pÁ2ä±>6'W—ë„Ò7oãÌÀøwfÀ¨šQî‡<Þä¬ó#ÀW}[z $"'7}òÖ¥‰Yˆ= ?Ш¡ˆŒÀ.6y3ØSÞ•´l8´ˆœ üoüqÙ¸_DNqh{rÚÊ82 ø£ˆ¬ß¨aK:€ˆ|¸ÿ`›Bƒ+»ˆôNÈD7v~/"}ë5j9‘K±POrÌRÕÉ ÚLÏòqQØ8ª^ƒ–qé‘€¯ç­K ò3‡6E™þT2®Þ¶ÄCpux3pXÞº´ «€aZ'ì@D>¼ f«c€»ªêßk5húDƒÅ†ŒÌ[—åþzÆpÅ3~€Ûê?4ù(Xæ|oüiRwúìn;)#]°¸ Q£¦tÙVDîn¤úÎ*O2, g#{-öÆv¸‰¥Àª:¯Q暉ÈPà›÷–Ûjܬª«´qy?%ËÏ©kdkÊab!«ëÆìglkÜ2òh'Ñà¼|¨`çdðÙ0¶•ÚHDŽþ ¼®ª+#ö±Ÿˆü{µ}°^‚*zê3YUg5h3 .,³=TõžPŸJáª?Û»ªX.˜Ž}üü¯(í,g„8_{å¨ëmÀÀHöš á¯|XY¦ØA!ûؘV€“ßî²øPÈs×m¨ÉRÏ÷°@½èv›ñ¼V¡ÜS!>?ÛÙ“÷‰÷brgD;è »;÷3À¶±m7¦áþTCAgÏÄÖšó>é^zäô6±oú] ôKäâñK p>v«¬¦à›ÀzŽ}íJϾ[/ÅåÀ—‰¶#¬x=%½ÞIÂð#;¶!üîŠ^¢¿F}yÉO&ÛD°‘«RÐå`ó$?´`+‘[s= ¯„Q‹èÌû$z‰'—D0ΟVéç·Àð¸†ô¶ ¼ÿ)ÔY=ªÕÑþ¸?¼¬ƇPrg,j/ïè%žtcaÇa t=«~Ó€Ý2ü¾X"„ZºÎ6qr¬„Ï’âÚŠöž/ÀÉó’Œ,vi¬G&aøe}Þï ëTª¼=®ìh},9ªë˜ t†P4ë`)/éË<º’Çp€Su½­‘\òËBÉ=°ÛfÞ'ÌKòÒ |Çð— xÒQׯWu`[ÂÍÍŸ¡`'0«'ÊKº2ƒσ ;kVêeÀÕ VTg-99„r×àäxÉF–r‰´ŽÝ \èØvµƒ3+eâZŒù%áí¹WNŠ—låG1 {ìm°bF½ãç\†{1\µgOð „ã—ªºÜ±í7Böíi~N‘†ýˆ ‘‰Øf—RµÍ~Øú¾ /9¶ëCP¾µCD:ÃÃ(Š•çlˆˆìì²oOó3+=儈¬'"çcω§Ñ;å«}Îv˜ "w`ù4;C|,Ûƒ G†ì×Ó:Œri$"GaWîË5šíâ8fèvê¢ór^vl÷±}{Zƒºç^DvÃÞÞîîÐׇÇœãØ®ÄÎÀ–!?´XU—8¶ =ô´ Uï"²…ˆÜA áØW§ˆ¸ä°ú?ì„+‘ LEñE!ûö´k9€ˆ¬+"_ÅÞI¸ê=‹TuµC»Ò‹VWvêÀj@…aHPÇ…WCöíi6 *Æ#"ã€`¡0aŸ7ÁÝŽ6%\¾Û­£¤Fì>âØ¶nnvO˳ˆÜ‚•.Ý.F?O;¶²ßA·pl÷;,¬ÚÓžü QˆËoÛ¹Úe‰•„›3•íÒHUßÄÞèyò{B$æ&D瀙1}2¤¢L,ÀöR\™D¸;}± ¸(c=ÓAø¢rÆ"âSÓ¯béT<žrnPÕ0Ï‹g[Go%À‰ç±s"ìú9·d»^Z_VS±YÝц†Ò;V¹¿ NŠÃ0à–àAÄU½Û.9?æøžæ¦ 8ZU¿æC"ÒË*·QŒ±g&áa룪> | ”ò´oŸVÕÛ#|öGÀn1ÇŸ –Ç=‰ÛX7!²CWÜÊ:ñ/ËÚMž&bNPà˜„t8 Ôá3 uø1²„ŸÂ=Á‘—æ”ùÀ‰D¨ØÈgpOÒH¶.uzN‚_p)°W '(½Ôˆ¢á¥¸ò>–‰Á9•fÛø<É%VžH©ãÄ{!VíË~:ê t\ˆ_)jvY ÜLÌwØ´'j¹¥jr¾ªR>Àž$›»spxœ/èµVGØçm>™ ì’€ œ“ðù_ YË‚¢¾Q«'WâÑ×à ìˆm°ñµŠ/SÃ8ç€[RÐïî5cT 8)¥2™Àã8(cÕkž+À‰öÒ#ó±’D‰·¶IñŸWËnJñ- dðœÃAÚ¸x»Ð޲¸ [™Y'Áóz$¶¢˜–Þk2XWü­ ÚÀ– ;B,Üznå\½Ä“ç±zÁUËÅ8åIqÓ”k9ÀIÀeÀe$P°ÊAÜ ¸{÷¡´’Ìn>žÂ9Û»“'¹ÊSó{¬5v…"ŸÎø ¾œLÄ—"v3àh¬:¡/ÐþÜÜŽIO¥h5Êü²Â>PÏFät°Ÿ!áçƒ|(p<ö¬3·FV$y .;Ø.ƒsq(ñö¢D•+ë9@?òCÿ‘öÁ/û¾[cw [±*åya–²88ÞçbsoçM1ùà¡¿÷±åúH ÔDd0„üX\\­ªs³XD#«È¬‚f³±{·3³ŠÌ×Ê“Ÿ""²-6Ý9ˆš- vPÕgKÿ¨æO?Ì4 Vc+7÷¨êŠ<•‘ͨíýsT­ ÛÕWÍÈ_ËÒÈ+‘À€S°@ǼY ¬_nKÕàŠWØâ à—ÀÏTÕµ N&6Ââ©Ö¤³ì÷FÒ»R¿ç(ïWü{±ºeNÎ Ù3ú À9«SÎsª:¦üÕ2éf:ípdìöù™ŠÝ~£ªä«WØÅ´-"²!°v ¶RDz•öª6+¢”³ð `ˆÜ "cóV¨]co¹{Ó Å5~€g+ÿÐ,w€j NN‘éØÞ$àIuÏ&æ ‰ˆ¬|{gtö Ô,ôr€jÏc€éYi”K)˜3L¦mŽÜLˆÈºØ¾íñÀ>XY£~¹*-Tõ_卿ý€¿`ûZ%X4ê$`’ª¾˜³>…FDÖv¦Çà÷$ZU—¢±DU{Ûëå"²>ðGZÇ ÊYˆ9ÃØC„)­Ùr«Xc0c¥ª)ÒÊMR\¡ªçWþ±ª@Ë;A9obu_®YªºWÉ¢†³¶‘—$Ï·ûy±8KU'ÖjP×`܃Ý"=F=!‹±RKéy9µ´ÞÏZw–`:2{A6°âgµ¿•~nŠùGu’þ²MÊ àUý]½F Ö<}ø:ùÆq´ «èqˆ.Ìèb›þøÆçmà ªúP£†N°¦±Õ{½ Ø<ºnOª<œ¤ª¯¹4uµQÕÉÀØ”Èã)ïg«êþ®Æ!ïk}Päàû4ïKOë08AUg…ý`äù¦ª^ƒ…M÷z½ìñdDV€q¯(Æ1¸Tu:öÖð,l=ÝãÉŠiÀXU½2Î’tìUíVÕë±—,W”ñxRâŸÀçUuwU}>ng‘Ÿjvh[ß~ˆÍðx’bp1ðó$£}w€5‹ˆ9¨Tð´ ïbùe”ƨÔ@Dú`i6.zEây¢l¼‚Õöú¬ª¾eð<‘QôÜv†Ñ¾Ï+§ ®îÀTU]’¯JîçòxU½ r1ö_Œ¥Jùð諸0ªy""ý±œ:£*d,]I+°€µ{Í ~ÎλòNTDä`à:àq^ªÅq€ °»À‡°£ÓTµe²EIª†aé+£ˆÏoÑÛÀ_fªê{y*–4"ò5¬ÎtݬoN}ÅY‘¯b›JÜ |©ÙWäÙÜØ«S00×ß«Å<­Àe…•…˜‘·üžléÄj>OÃ6ÃÇZÇë}€¿cÙ…KÌNVÕã(ÖÊÇ­” n°´Y§"Y!"‡`h†a‹“¨7+Ä*¬JKù®ü¡ÀŸDäÚÀc=¨ê*U}KU_SÕÅÞøk#"›‹ÈX2¶aÁŸ/OªXb"o‚EäZ,5J%³±yÚŸcâi+D¤‹¾”µÓ·¿„]ý¹h$åƒ0Å>R£ÉdàU}<ö`ž–GDvÆÂ¦+ *6ï,©±ÉD¼8·N“qÀT¹WDvHbLOë!"ÃEägÀßèmü?IÒø!á`8ù9pbƒf Ü\Ôj¯Õ=Ñ^h]Míú¯£“^aLÚúc{<·wh¾ ¸ Ûý?/1%"Ò+…;K9sé×vx3þi)Ó‹ÌóiLÄ6”dM7ðæ Ó€iQ« ¶ÁÚüǰEŠ’Œúf¨Æ à`U“á˜kÈ%1VØê`—ÌïÍØ]b¶ú0˜ÛjK®ÁÏáØ•½dì;c›ròâàpU}+/ò¬0¸ Ø7êÓ­FÌ dvÙïsŠ˜9!0ð!À–5dÅJ?3pjÞš\S#·àË€ÿÉM‰h¼M™C¯c[uÅõdI;±¬2[cßœbx=.RÕoç­$7¨ˆ†E†ÊY•´YÁÚNñ6ß.7îNj‡7;]ÀIªú«¼)Q‘m€;ÑyëâI…ÅXhÔ¼)'‘aI ª/ŸÀ¢ASÿðdÎÀ.E3~(Рœ`Oè5À'óÖÅ‹…Àyªz{ÞŠÔ¢0w€rTõi,ŠðD¬®”§¹PàFàcE6~(è œ ã%XÅùV}8l%f`i2 7Ý©Fá „ˆlM‹öÊ[OUº€Ë±¤UM“è«i „ˆ |Ú9ˆ<Ù38]Uÿ™·"a)ä3@=‚5äQXRÞ¦©KТ<|اšðPN°ãl–Ÿ´Z"%Oò¬Æ¶ºþ Yæùõhj(GDvÁ2L ߯VåàÀU­AÛ2P"ÈSzvWpIÐå©Ï|lñabƒãÒrPŽˆìŽÝŽ yÅŠÂtà‡ÀíÍ´ª––v€AQ¿#€ñX¼MòÕ¨tSû€û’Ê¿_tÚÂÊ âæÿg‡Õ9kG–`q:÷ä¹1%/ÚÎ* 6}ïˆ9Ãx,ÍG’™ŠÆ‹Wyàñ";Ï’¶w€J‚ú]c±:£¤½1< æaÿ"V{’ª¾’¯JÅÂ;€#AEò’3Œ,û}08GÕ˜K¡¯U]š£^Mw€ö7—œaÕ·1Vþ­Rºé)}ú.½Ë¡Vþí`ðR)[•ÿ ¥C”^iŠUIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/settings/zoom-out.png0100664 0001751 0001751 00000002761 12471273273 031077 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR€€¤Çµ¿sBIT|dˆ pHYsÃÃpMBtEXtSoftwarewww.inkscape.org›î<nIDATxœí×± À ð¦èÿ/§gdy7Á³»=ïõn@”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D}×êff¯7À•Ýë e@”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @ÔìîõxQ%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q?£3 ûYgÑIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/settings/touchscreen.png0100664 0001751 0001751 00000056731 12471273273 031636 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDRÀ€b ÞsBIT|dˆ pHYsÃÃpMBtEXtSoftwarewww.inkscape.org›î< IDATxœìÝw|Uþÿñ÷I¡†ÞADl¬ Ä.½* ¨° ‚QPÜ/–]÷«®ë."UTEøêÚP±ý±­)"ØAÐE@Wz/’ÌïI’¹ÉÜdîÌ-¯çãq9gf>¸ûÈ÷œ™sŒ¤{˲9/ÀfŒ¹'ˆóòû%)èðÀ€„@$0 !¤”q¿ƒ’~+ø{¶¤ÊÞ”ˆr{%m””/©¦¤jÁ–à^YðVIÏ[–µU’Œ1]%ãiU€hsPÒ[–e}uôFcLsI—Š¡ „ûô*I3 Ã/ a¼Q4üJ’eYk$Í  €°…€WJzÁ²¬ƒÆ˜ª‘*u6Y–õ­$c*c:c:c²$ɲ¬µ’ÖZ!€ áà]’dŒ¹@RŸÈ”ˆBkúû’.”t¾¤6GmÿMQ.œw€3%ýQÒ ’~‰L9€(ÔÀÓ]ÒI-Ú^÷¨¿ï÷·$€ð…€O‹X€hÖ àSÔÑ7CkúT @™±0 ,öHZ*IƘdI‚- t`@¸IškYÖ‚ŸÏË €@„#WÒs–e­—$cL}I[€;`€[–¤W –=’1&[ÒI©VàR8“`Û–e}/IƘ’®–”lIî1 pãIŸJGF~¯–T)ЊÂD”&OÒ[–eY3>áÄ pÞ-iÁß·µ}¤¯ f€x³Ü²¬]¯/iKÁçhÄRH Ê…€?¶,kYÑ–e-”´P’Œ1­$]âQm€èðCá_ f^_´ƒ1¦‰¤!¾UP< HጟkŒ9µ”>¼ñç<¿ÿ+úR @9„€«|‰å¸  ð@À€„@$0 !¤X–uOÐE·Ê²¬çƒ. ’$0 !€ H`@B À€„@$„h Àé’n º@`*)º¾—@œ‰¦ #i’Á¨R$e+º¾›@‰Æ‹ B0$®T‚@„Dë!!DD4_\‚ q‚€ç¢ýÂb’¤ÑA!x*.*&Kt€@‚€gbå‚bФ›ƒ.B0ðD,]LL•tSÐEAåkKt€@†`t! 6ÅZ–¤i’n º@ R%U!”A,`ÉÁ#‚.B0(“¨ÀtÚl$="髸i×®]¡šÁ lQ€‡®_ýÕ©ÉHzTÒõþVðËóÏ?¯)S¦„j&€°D}þñÇÕ¶mÛ’Bðc’®ó·*€_n¹åM:5T3!¸õX²Cp»víôßÿþשÙHš.éZ«øeôèÑzøá‡C53;4p%&°$­Y³FmÛ¶-)?.i¸¿Uü2jÔ¨’BpšÁ 1€%;·k×N¿ýö›S³‘4CÒ0«øeÔ¨Qš6mZ¨fB0(QL`IZ½zµÚ¶m[Rž)髸忛oÖ#<ª™ Bй,Ù!¸”‘à'$ õ·*€_nºé&=ú裡š ÁÀQL`Iúá‡Ô®];mܸѩ¹0ñµ(€onºé&=öØc¡š Á ˜˜ ÀR©!8IÒ“’®ö·*€,ËÒÈ‘#5}úôP]Áà1€%iÕªUjß¾½6mÚäÔœ$i–¤ÁþVðƒeYºñÆõøã‡êBGÄ|–¤•+Wª]»v%…à§$ ò·*€,ËÒˆ#Á Tq€%;—2<[ÒU¾ðEaž1cF¨.„`?X’¾ÿþ{µoß^›7ovjN’4G„`ˆK–eé†nÐÌ™3Cu!àâ*K®BðlI}- à ˲týõ×ë‰'žÕ… @‹»,Iß}÷Ú·o¯-[¶85'Ë àoU?X–¥ë®»NO>ùd¨.„`T\`ÉUþ?Iô·*€,ËÒµ×^«Y³f…ê’&©šÁ$”¸ À’ôí·ß–‚Ÿ–Ôßߪ~°,KÇ/)§‹ @B‰ë,Ù!¸C‡Úºu«Ss²¤g$õó·*€ G‚Ÿzê©P]Á$¸À’ôÍ7ߨ}ûö%…àIºÒߪ~ÈÏÏ×ðáÃ5{öìP]Á$ˆ„À’‚;tè mÛ¶95†à+ü­ à‡üü| 6LsæÌ Õ… @H˜,I_ýuI!8EÒ³’.÷·*€òóóuÍ5×èÿþïÿBu!ç*KÒW_}UZ~NR_«ø!??_C‡ÕÓO?ª !€8–pX²CpÇŽµ}ûv§æIÏ‹ q)??_C† !€2KÒ—_~©:”‚ûø[À…!ø™gž Õ… @JØ,Ù!ØÅHðeþVðC~~¾®¾újýë_ÿ Õ… @œIè,I+V¬P§N´cǧæTI/HêíoU?†àgŸ}6TB0q$á°$-_¾\;v,)¿(©—¿Uü——§Áƒë¹çž Õ%R!¸NŽ J@.°|ùòÒF‚çJêéoU?äååiРA¥…àªò6°V•tœÇÇ% å‹/¾P§N´sçN§æÂ|©¿UüP‚ŸþùP]2d‡V/‚ðQJÐD›Âüî»ïªjÕb×9i’^’t¹¤7|/Qyyyºêª«dŒQ¿~ýœºdÈ~Úñq¡2* Áÿ‘dyxܘÖfÌÒœÒú,™xv†µâ#À–-[¦Î;‡ . Á—ø[Àyyy8p ^|ñÅP] C°— .ÂÈJ/ít€ØCáóÏ?Wçεk×.§æ4I/KêáoU?‚ˆOภÁÝý­ à‡ÜÜ\ 8PsçÎ Õ… @Œ!—béÒ¥%…àtI¯HêæoU?äææjÀ€z饗Bu!CÀ.,]ºT]ºtÑîÝ»šÓ%Í“ÔÕߪ~ÈÍÍÕÿøG½üòË¡º‚ˆ`—–,YRZ~U„`ˆK¹¹¹êß¿?!€GÃâŋ݌wñ·*€ Cð+¯¼ªK¤BpS‚ð8L‹/V×®]µgϧæ Ù#Áý­ à‡Âùd§® éMImƒ89À6iÒ$åççqê°‡>pà€zöì©÷ß?T—lIMÄãи·øÔSO Ù6eÊåååùXÍ…!ø¢ N‰¢iÓ¦jÞ¼¹cÛ?þXÒ;»‘V¦|饗‚ð@\à~ýúiÙ²ejÓ¦cûÚµk5oÞ<Ÿ«:"KÒ[’. ªˆggœq†>ûì3ÝqÇ!ûLœ8ÑÇŠŠ)sþàƒSI†à3fIÆ9c–L:gÌ’ìÒú¢üγ$»à¿wFеlq÷ðm·Ý¦ñãÇË£1cƨ_¿~Žý&L˜ Ë/¿Ü©éI!vôÐE’–H:äù !tìØQ¯¼òŠ*Uª¤ê®»îÒ¦M›Šõûä“O´lÙ2µjÕÊé0»åÏ;µÉát. Áo¾ù¦ÚµkçÔ%[¿OŒULA{URIŸ3fIÇÅÛl¯d¸Up“á=IgH:éœ1Kz/žØ&'ಠáÅMNJJÒĉ™á¹oß¾jܸ±Ö­[W¬ÿâÅ‹µhÑ"{î¹E›êJú»¤=‘¬à­ê©§ž:2#szzºFŽ©¿þõ¯Žý'Nœ¨gŸ}Ö©É’´?b…þ.ìwqöï߯K.¹¤¤\½àÏŸÞX$üJv({E¯dÿw• Á‹‹G ÓÓÓõüóÏ[Þ(99¹Ä%B<WYÒpO DÔØ±cõôÓO[ŽhĈÊÌÌtÜgîܹڰaƒS“·k$y¬0ÿûßÿÕ¥ºŽzÚ!ü* Á<í!‡ð[¨0ó84(æpÕªUµ`Á]qÅŽíÇWåÊ•ÛæÍ›§µk×:5R˜¦ü—””¤‡~XãÆ“1Å瀪Q£†ì¸onn®¦NêÔ”")ÝÓB=¶ÿ~õèÑC}ôQ¨.Õ%5)!ü"{¨„ð[ˆ ‹éܰaC}ú駺è¢Ð“*WªTI×^{­c[^^ž¦L™âÔÔDR/jDFFF†æÎ«›nº©Ä~·Þz«c8–¤™3gjïÞ½NMQ= ,•‚ÙšS73=ù…¿…Áp~ ‚ @1€[¶l©E‹é”SN)µï¨Q£”’âüºó“O>©]»v95ÝV¾ ‘R­Z5½ûî»êÓ§ô{•-Z´P=ÛvíÚjmøtI©N Ñdß¾}êÑ£‡>þøãbm£_uÓƒyn—Ý#—Cá·PIÿˆ\E€Pb2·k×NŸ|ò‰êׯ画FÔ·o_Ƕ½{÷jÆŒNMm$Wæ"ѨQ#-\¸P\pë}ÆŒ²­„µá£~X²Cp÷îÝõÉ'Ÿ³}ìåMŸÈLKú!ŒC‚Ë  áW’VHú[d*”$æpÿþý5þ|U©R%T—Ù’vÝxÛm¡t~øaåææ:5…¾bøî´ÓNÓ¢E‹tÒI'…êrHÒ¢Ûµk§3ÎpÎ'k×®Õ«¯:®~—¡ùžt ÁmZTÙýÏ¡'Œ GN9Â/³o@@b⋽Ðm·Ý¦gŸ}Viii¡ºÜ#i¨¤bCº­[·9Z°aýøâ‹NM½%5-S±OuèÐAü±êÕ«ªKޤí’_ê-i8ĪF12 ,ÙO4uïÞ]Ÿ~úé‘m„àÈ!ü@lЉlŒÑ¤I“ôÐC…šÈ$WÒ0I÷ü;²\~„_ˆ}Q€7nªi¤s%- Ѿ\Ò¿‹n:t¨ªU«æ¸Ã›o¾©5kÖ85’伎 bBý¾–~·Ëa«£ ÆÕªUÓ5×\ãØ9//OS§NujJ‘=!VLÙ³gºvíªE‹ÙF.;Â/ć¨À!,–½DÑJéWl8++K×]wcçüü|Mž<Ù©©¤ÃÐ_åIÚ*{”·$ úcôèÑJJrþú{â‰'´{wѧ©%Åà(°d‡à.]º‚ˉð ñ#ðë’:Ⱦø)Í›’VÝxóÍ7+55Õq‡Ù³gkûvÇï*–D€àåJÚVð§ÅF›5k¦^½z9vÞ½{·ž|òI§¦4IÎ_Q®p$xñâÅG¶‚Ý#ü@|‰µ<]RIû]ö·$M*º±~ýúêׯŸãû÷ï×ôéӚΒt±Ëó¼wHöÍÏb£º%Ø/û»à%­ ?eÊåå9ž"&G%;ØwéÒ…&Â/ÄŸX À‘4Bá]øHÒÙ£Ç(iI¤iÓ¦éÐ!Ç'ë€`ýÎo±0[ K7MÏ?ÿ|}öÙŽ;¬[·N¯¼òŠSS¦ìåñbRa>z¹'Bph„_ˆO±€K"éïeÜÿ€ì‘ãcœqÆj×®ã¿ýö›ž{î9§¦K%5/c€²Ù'{¶çpÃïÑûSÒ(pˆ%‘${]à˜U‚—.]zd!¸8Â/įhÀ{$]"{·<¦Éa²”’FC\üI·–³€{Nkü†+ORNÑ}ûö ¹ÒÀâÅ‹YBè(dĬ]»v©sçÎQ‚ZéÕJûD⼄_ˆoƲÊzCÝs:v9‹’ºËþRñÂS²G’°,K't’~øÁùûþÝwßUÇŽ‹nÞ/©¡ìGñ fcî9êÇU–e=T-ªé÷å† ×ø-\Ë(MRõ¢'Mšòfhß¾}õÒK/95íVˆQå0¨"ïß{コçž{<8téªT©¢wß}W­[·>²mÉ»*ßñÔêÇÊoÆ¡â&ü~ þEëð*Ùküz~%‡%‘Œ1ºõÖк!F+HºÁ»²Eä˾ÉèUø•ì§€Ý8lØ0U®\Ùq‡W_}Uk×®ujŠÙɰŽV8üùçŸÙ#Á~!ü@bˆÆ¼PÒù’~öø¸ßÈþb;ÆàÁƒU½z±AIÒüùóõý÷ß;5Ý${4à­<Ù–¶ÆoYì-º¡råÊ>|¸s!yyš2eŠSS²~©Ži;wîTçεlÙ²#Û1~ qD[~ERGEîñâ E7dffjĈŽ-ËÒ¤IÅVQ’¤º’þèmiðÂ]ã7\9rXI`ôèÑJIIqÜaÖ¬YÚµk—SS\ŒKvîÔ©“¾øâ‹#Û)~ ±DS>(é yûÈ[QïH*6¤;räH¥§§;îðÌ3ÏhË–-NM,‰Þڣ𗺠W±ww5j¤¾}û:´gfΜéÔ”&)ÕÛÒ‚“¨!˜ð ‰'š°%û½¯HŸ£Øn:u4`ÀÇrrrôè£:5ýAöh5 vì—ÃrJ%-‰4uêTåæ:JÇÍ(°$íØ±C:uÒòåËl+Gžçy‘1O„_H(Ñ€ýòŒ¤ÍE7–4Ö#<¢œÇiF ¶X²Cð1Z·n­ .¸Àq‡ 6„š :SöûÀqcÇŽêØ±cyCð^IŽHÞû³Þ ð q pޤbCº-[¶T§Nwزe‹žy槦®’Nò´:@¤9.aTÒ(pˆU¤8–~ ^±â÷…ÂÁ{%u[<±Í§-Ò#uvSé!˜ð q"°dàbCº%]üLš4Ik&I¡‡ŽÑ(OÇ®;/IêÙ³§š5kæ¸Ã矮O?uÌtdÄ•íÛ·«cÇŽúòË/ls‚Ë~1ÙÆ˜31—cÆc3Æ,0Æ|mŒùØ3Ïó„1f\A{?cLµ²žOr‚ ¿G5o‘ôtÑ]ºtÑ)§œâ¸Ã÷߯ùóç;5 ’TÓÓê‘Vl8))I·ÜrKÈBŒÙ!8îlß¾]:tp‚Ë~1YƘ¡Æ˜ÏdÏþ…¤¹’ÆIºAR'I-%](©·¤a’Æ´?/isAHaŒ©ö?T%†`Â/Ä™D À’=V±!Ý’Þqñ“!éFϪJpƘ&ƘƘk1ÿ0Æò6üJÒi’^/˜,ů‹,±©Ø(p½zõÔ¿çÎûöiÆŒNM©’Ò¼--ºŽóÍ7¡ºÔ•TÏ©ÁØÆJúHþ…_Iª"ûÑè71é>ž¥=KR±{kÖ¬©Aƒ9v>tè¦M›æÔt²ìM”ÀÓEömGN×[ÒJcÌmƘ¸Z«€gHÊ/ºq̘Ð˼?üðÃŽ£ÃŠãw mݺUíÛ·/Kž*{Òª‡6?t—4×ãõ W@ŒIôG %ûŽýϲ¿´X¹r¥N9å§¥”­ 6¨B…b¾'{"aŒI‘ô€¤?)˜‘ò÷%]jYV±¥O€DÅ#ÐGT’T±èÆ:èƒ>pÜá™gžÑÀš6Ë^fɈ?]«V-½úê«JJòö~wÕªUÕ¢E‹’ºyÚ3XÒO (»—$õ·,ËíÿF€8ÔØhrHÒ4I?zãI'¤nݺ魷Þ*¶ÃöíÛ5{ölÝxc±ÉŸ;Júƒ«†¦Ë^º"($Í3Æô²,ë`€uˆ>ûå€ÇŒ2Oš4)TÎ’´ÛÓêÊaóæÍÚ±c‡ºw{Räòª+IƘڲÿG‹Ë%ýŸ1æ*Þ €ÄÄ#жé²/€Žê8cŒV­ZêX¡×QJPƘëlø-ÔE< ¸<ÙB£{÷î:ñÄw¨P¡‚vîÜ騤(›"Ä~÷þûï·HIIyURf „6@öú€D¶m—ÃãY:tÐi§väçÌÌLÝpà Z¹r¥¦NêX$Õ‰H•1Ès®ìw¿¢Å¥’ž-˜… › ËsÌÚðÉÉɺòÊ+µtéR}üñÇN3AKvø-ö~L¤5iÒDÙÙÙŽmï¿ÿ~¨%ü"êÆo¼:77·QyŽ‘ššªã?^mÛ¶ÕÙgŸ­fÍš©jÕªòàWø£Ó€CþÝ$…˜¥víÚºï¾û´~ýz=öØc¥½÷”&{]à„gŒ©'éeEß̨—Kº.è"D•ò_‰9ÆàÁƒuÜqÇé–[nÑO?ý¤^xA­[·.íX¾N†uæ™gjÑ¢E1bDÈ>~/X° òO?ýtE¸û%''«Gzùå—µnÝ:åäähÍš5úðõdÉýøãÚ±c‡víÚ¥^xA P•*UÊRbUI•eG@lc¬c½&©çÑrss•——§ôt׫',”=ÙSñ—‡Œ1æ1•ó1³Š+ªiÓ¦jРöìÙ£­[·jëÖ­Ú¾}»òó‹Ý¯ÇNI'Z–µ©<b“`“.©Ø0j^^ž’“]O$Ÿ+i¯©vPîI°ºt颗^zI+VÔ¦M›Ô¸qcÖG’¾£ÿ^Ùï·/©Ÿ¤Ï#QTŒúÙ£)® :T¿þú«^xáuíÚU5kÖ Ù·B… êÝ»·æÌ™£ŸþY#GŽ,Ë2Œ1~¬G v{¸–ì°»Uvøõm†ù¿üå/zê©§”’rìbG¿³\ÔôéÓuà€c6÷ú]ØË$¹~.ùÏþ³F]î“^xá…š?¾*UªÎníŒ1ÍÊ}r@Ì çfTð7IwIj$i´ìu„QÀSCa¼c›œœ¬9sæhÖ¬YªQ£FØç«\¹²¦M›¦… ªzõêáî~{Ø'ÏÈa>ˆ"òeßÝ"ûuŠÃ‘.ªPrr²¦OŸ®¿ýíoŽí§œrŠºvíêØ¶mÛ6Í™ã¸oey;S³sÚ¶m«{ï½×³·iÓFsçÎ g#i¸g¢¸¸%ý¢í;I×Hj"é’vøTS¬é¥0fA}à4xðàrŸôœsÎÑüùóU¹råpvë`ŒiZ––Å+'{ßÍ’öüì›ÌÌL½òÊ+ºþúëKìj ?É^¿8Ä{²µÊWÍ“$Éõ“5÷ßYžÞ)Q—.]B­ÑÊPcLJéÝñ€\\®¤)E¶} ©»¤–’ž’ÃL¡8Fw·{ô衱cÇzvâV­ZéÅ_ g£èX£@ôØ';:$û†çf‡6_Ô¨QC|ðzöìYjßN:©eË–Žm«W¯Ö›o¾éÔT]’!°UÁ±JuñÅëüóÏ÷à”ÅM˜0!œ›¡µ%‘BQ‡ìl†ì÷¹ž•t¦¤’ÞV=±Æ“*©“Ûþ÷ß¿ç5téÒ%äD0! 1Ƹžâ@ÜË—ý(tޤmŸœ ŠiÚ´©.\¨sÎ9Çõ>%‡XÉÈ›Qà³Ýv¼üòË=8³Úµk‡; L€Av¶[öû½%­¸–Xs$W3téÒE§Ÿ~zDŠóî}Im"R€XµKö¨o Oü®ñ{ '„ê²§àsŒ8-y$Iú÷¿ÿ­+¿Újªü×®'Ôêܹs9OU²aÃÂz¸ÇýÝ@Lã—Й ¿kå¶ãe—]±"jÕª¥þýûkÆŒnwi#鳈a8û–EŸcÒ$©×¸ŸU–¹ÿñU5Ñ6âéCÒÓK\ë •^mŤÓwºèꜺ‹¨P¡BI¡Þgu–š4i¢ŸþÙMwWøŒ[¿¬šnFÍü–ÌÁ%Ïκˆ%ŒÃk®ïþwéÒ%’uèšk\/C)q÷@1Æ]BY¹ ÀuëÖt’¤sÏuýds]cLø‹b^suñ“­&MšD´6mÚ¨~}××3`QáÏþ3Ø#á¼7-fÂD70¼æêâ§^½z‘®CRXwÿc\Õ‘rà 7Ddr@EU>å”SÂéž©:у ¯EÕÅO˜wÿ ÀU»¶ë·H¢Ž±‡­]ýBMÐåµêÕ]­ÈTˆ € ¯EU>ùä“Ãé^5Ru@¨&)ÕMG¿¾²³ÃÊ´a¥e@l"Ã3k»ºÚˆÒ‹0”]šÛŽ™™™‘¬ãˆ´4×%©E‹ "X J€á¥š’\ÍÜâ×ãoaàj‘ª݆:RR³ ëD^r}«=#ße SS]=W(=Ru@8¬  H@éééU%ý[„`ˆk)Aޱqñ„³ËsC®¢¤E7Ž?^cÇŽuÜ¡ÿþzî¹çœšÖKÚRŽZbMIJj+é?EWL:}§\>éˆNŒ=ÖKúµœÇØ+i_Ñ×^{­*V¬è¸ÃK/½¤ 685%âÚ¸ e7 ¸@€^¾¤ŸäÝhëæ¢ªV­ªk®¹Æ±snn®¦NêÔ”!©ŠG5ÅB0Ä)0ÁÊ•´FÒN¹CÒ¡¢o¹å%'';î0sæLíٳǩ)v'.ŸF²Cp“`Ëx‰ @pIúAöcË^²ä0 Ü´iSõîÝÛq‡]»viÖ¬YNM•$Uð´º(dYŽS‚ ΀ÆI«$åDèø[%åÝxÛm·…ÜaÊ”)ÊË+¶‹”£ÀsçÎ ‚ËÁ}-`‚±NÒá?OÒ¶¢Ï=÷\sÎ9Ž;¬]»V¯¾úªSS5Ia­+k-Z¤n¸ qŽ @üÚ$‡e…Kž0a‚Óf£˜zÆŒ1bD¨ÜDöI|- à)0ñë&׺ì²ËÔ¤IÇ-Z¤Å‹;5ÕT\7<þøãºñÆC…লG‚ Á£âþ‹ €·©è†ääd=:ä'NtÚœ,©†weE¯éÓ§käÈ‘%…àe/•ˆ1`âÛ¾‚Ï1† ¦*Uœ—ø}å•WôóÏ?;5ÅýcÐ…{ì±’Bðq²G‚ ÁcÀÄ¿b£À•*UÒµ×^ëØ9//OS§NujJ—TÕÛÒ¢×c=¦›nº)Tóq²G‚øW ¼ÀÄ¿’Ý8jÔ(¥¤¤8îðÄOh÷îÝNMq¿$ÒÑ}ôÑ’Bp3Ù#Á„`ˆ`âŸ%isÑ 6ÔW\á¸Ãž={4sæL§¦Š’²<­.Ê=òÈ#ºùæ›C57“=\ß¿ŠeE 1l•½6ð1ÆŒr‡©S§*77ש)¡F%iÚ´i5jT¨æãE€˜@ 1äËÁÇhÕª•.ºè"ÇÖ¯_¯—_~Ù©©ª¤4O«‹?üpI³g7—‚ëùW \`ÇfÙC£¤QàK"%ÐŒÐG›:uªn¹å–PÍ„`ˆr`Ç!I;Šn¼ôÒKÕ¼ysÇ–.]ª… :5Õ½6p™2eJI!øÙ!¸®Ü"XŠ-‰”””TR  5 œ,;'¤)S¦èÖ[o ÕL€(E ±ì—´·èÆ!C†(;;Ûq‡7êðáÃNMµd?&Ož\Òãã-$} ©ŽJC ñ®P¡‚n¸á†#?'%%é²Ë.Ó§Ÿ~ª… *55Õé8i²'ÄJX“&MÒm·ÝªùDÙ#Á„`ˆ`ÏNI‹n¼é¦›T­Z59R«W¯Ö+¯¼¢óÏ?¿´c%Ü’HEMœ8QúÓŸB5Ÿ({$8áÿ;@4H ºˆM’½¡nݺúïÿ«ŒŒ ·ÇÈ‘ÃÒJ‰h„ 2ÆhüøñNÍ'É n'‡Ñw€ 1m“”[t£Ëð»GÒ’¾øˆ‡zHcÇŽ Õ|’ì‘à„\> ¢€Ä”¯ð«%i»¤•’VKÚ‰¢bÝøñãuûí·‡j>Y„`€ÄµYv°-IžìÇv¿•´Vö,Ò(Áƒ>¨;î¸#Tó)²CpMÿ*â`×aÙ£ºÕÚÉÈ[e‡à¸vñÅëŒ3ÎðìxÔ7ß|£–-[:5†àö’¶xvR@©À$¶M:6ï+ضS¥Çž={–´¦o$œ*é}ID߀Hl$í–v7JÚl9Áøú믃8mK‚ÀW¼ ~,ø$dø•¤?ü0¨S†`Þ €@Â<êÊúõëõé§ŸuzB0ø„G $Mœ8Q\pSÓvI|(a‚¤á²' D@Òk¯½¦Ÿ~úIÍš5+Ú´CÒ™²×NÄ0”ŸŸ¯)S¦855“ÔÓçr@(0kÖ,íܹөÉ×5’‘A(°oß>͘1ééBI­}.à10ÀQ¦M›¦ÜÜ\§¦[ü®à-0ÀQ6lØ ¹sç:5])©Ïåù䧦ë$Uô¹€GÀBŒW•tÏ¥—ð$¼¤$çK¢Ù³gkÇŽNM·E´ @D€@Â;ýôÓ·ïÛ·O?þ¸SÓy’ÚD²&€÷À áµmÛ6dÛ´iÓtøða§¦1‘ª`ð6l¨ /¼Ð±í×_Õ /¼p̶œœåååõ•ÔØ‡ò!Hºí¶Ð¯õŽ3F½zõÒ™gž©5j(33S©©©É*TøÁ³Ö³Üó¾1æ%cÌdcLwcL¦å\H º c’$U“´ß²¬A×(¿K/½T5Òúõ답mÙ²E¯¿þú1Û,ËÒÒ%5)øm´¤cÌG’Þ–ô¶eY«#Q7À=0PcÌé’ΑÔè¨OCIõ%¥ô9(iG‘ÏzI $½oYÖÿ+„ã7ÞдiÓÃo9dHêRð™lŒY-é.˲^öò$÷ÀÀQ FvÏ—t™ì5›ºØ-]R‚ÏÑFH:lŒùT¿ßýÿÖÃrùàƒü8Í ’^2Ƽ)i¤eYž¦m@éxøÝU’6JúXÒ­r~K“*©¤%}cŒYoŒéçÁq±ëIßcÆc’ƒ. øÝù’jFø %=oŒ™oŒiás¢W–¤ ’–cN ºH< £‹¤o1÷KoYÖ¡  âtI Œ1ç[–µ¡¤ŽÆ˜tI'Kª()Sö;Æ…V´MÒJIk,ËʉhÕ£À@p2$Ý/i 1¦ŸeYß]  %½cŒ¹À²¬í…1i’ÚÈ~•¦ìI3\/ß³NÒª‚ÏrIó,ËÚçyåcÀ@ðN’}ás®eYëJêhŒ©,é4Ùwü+ùdê÷»ÿ+¾ˆD½“$½ ©“1¦¿¤a²_Í)ËZÂI²ç±h*©[Á¶ÝƘIšaYÖ—Ô 1‰ D‡º’æ<wôÝÿ,Iè÷»ÿgIr5aŠ1f³¤ïeâÏ%½Ä’LÕ:c¾‘tjŽ]Yöê#Œ1K%Íô<£Â ˆ'JzÖÓCÒµ’Ij­‚õ†Ë VÁ§­ì‹ž‡1Ï˾û¿´üå@âHIIQ½zõÔ°aC5jÔH 4eYÚ±cǑϦM›´råJY–UžSE"üuvÁçcLo˲¾ðáœÀ@té"éGIM"pì,ÙÔ 3Æ|%ûîÿÓŒ ˆW99囪aÆêÓ§úôé£óÏ?_ÉÉ¥?€³yóf½óÎ;zçw4þ|mÛ¶­\5DXIŸc†Z–õBÐÅ€À€KYYYjР6lx䓟ŸÌÝÿ7ꫯ¾R~~~yNÕÄ£’Krš¤G$ÝiŒ¹Ì²¬e>œ|µ{÷î2í×µkWÝ{ï½jݺµŒ1aí[«V- 4Hƒ Òž={ô—¿üEÓ¦M+ï÷B$eÊ^žïIÿk•sø¢qmïÞ½åÚÿÔSOUïÞ½uÙe—éÌ3ÏtµÏöíÛõî»ïjþüùzë­·´yóærÕa…wÿ‡Y–õlÐÅ@j×®­É“'«ÿþž¯R¥Jš2eŠ ¤!C†è»ï¾óä¸r·¤SŒ1ƒ,ËÚt1)IADÒÎ;Ë´_ÿþýµzõj}óÍ7úÛßþæ:üJRvv¶úõë§§žzJ?ýô“ÆŒãê±¹eHú—1æŸÆ~'HH5kÖÔçŸîYø=Z«V­ôÞ{ï©V­ZžÛc}$Mºˆ$.v£4oÞ\ ,ÐsÏ=§æÍ›—ûx+VÔ„ ´lÙ2vÚiTQ·KzÝS)èBÀowÝu—6l±ãשSGýë_#v|]oŒ9'T£1¦‰1抂›¦“1S1c¦c5Æ$û‚Éõ÷ƒ’JzDz¬¨~ ü’––¦”””2àãŽ;N;vT‡T¿~}edd(==]ééé:|ø°~øá­ZµJ«V­Ò·ß~«åË——µÔ?HZ,û÷|yžL“Ô¾à3Þ³^ÒC’±,+jgßÀ@´´´ˆŸcàÀºõÖ[cáîÿtcL+Ù“dµÑïwþÏ’äö‘¶ë%YƘ/Tp÷_Ò.z$ª·ß~[ kŸŒŒ ;VC† QÓ¦MKì{òÉ'óó?ü Ç\sæÌÑöíÛÃ-÷¬pwp¡‘¤©’úc®µ,ëûœJD|”””¤äää2त$vÚijß¾½Úµk§ºuë¹óŸžž®C‡iõêÕGF¾úê+-^\l×­Ó$­tjYPÀÈέdÏ0ºÍó¤ñ–eEõ]ðZnnnXý{õê¥I“&•|CiÑ¢…&Nœ¨x@ýë_5~üø2'Γ´Â3Ú²¬éA ±€½ñÆ:tèPXûTªTIwß}·† ¦ìììû6kÖLݺu;òóêÕ«5sæLÍž=[[·n ·Üò†_'Õ%ýCR_cÌ5–e}s@Ì;v¬Æçɱ222ôàƒêôÓO×°aÔ““ãÉqË)MңƘ˲f] €ÄÁ,ЀÂù5ÆèꫯÖêÕ«õ?ÿó?¥†_''œp‚Ư_ýUcÇŽ {ÿj%é cÌÈ  €h4tèPÏ9`À3Æóã–ƒ‘ô„1抠 8À@”7nœfÏž­:uê”ûXiii7nœž}öY_f:u)UÒ4cÌõA‰â¼óÎ º„¢’%=cŒ9!èB$0¥.½ôRÏùÇ?þ1ÚîþKö#p‚.A£F‚.ÁIšì%ø âÀ@‚iÕªUÐ%•$é)cL³  €x¥X’zcÚ]€øGL”^üp÷|P¥JU®\Ù“c¥¤¤(33SYYYªT©’*W®¬””rͯúOO €€¥X’.3Æ\tï6lö>))):í´Ótíµ×jæÌ™úòË/•““£ýû÷kïÞ½Ú½{·víÚ¥m۶饗^Ò°aÃT·nÝpOÓÚS¶5ŸÀ%–ALÍš5•‘‘áÉ2*TPff¦RRR”œœ¬””mÞ¼¹<Ç~@Òùå. R£FôÝwß¹êÛ®]; 2D}úôQÅŠKí_¹reõíÛW}ûöÕ¡C‡t÷ÝwëÁ §¼n’ gH@ 6Ôš5kÂÚ'++Kgu–Zµj¥Ö­[«U«V:þøã‹õ;pà€>üðC½õÖ[zóÍ7µnݺpNsž1¦eY¿„UÀ5·O%%%iÁ‚e~¬¹p‚M›6iΜ9nw#ˆ(Û‹cŒzô衹sçjÛ¶múè£4aÂõïßß1üJRff¦ºwï®iÓ¦éÇÔwÜny]ÃÝàžÛï€üü|ýúë¯å>ßC=äjô¸@;c ×§"†_0@r{ñ“’’¢7ÞxC—_~¹ÒÓÓÃ>OJJŠþñèúëÃZê·[Ø'¸Î;Àëׯ/÷ùjÔ¨¡Î;»íž%©q¹O !€äöâçðáÃÚ¸qc¹Ï÷À„s÷Ÿe0 ‚™ qÆ žœ3Œ,I'zrRp@ß?ÙÙÙêØ±£ÛîU1õË}R€£ pË–-Ãé~’''` qñÓ©S§pºs÷"¤AƒJJrw èÅ#Ð’tÒIaeZ¾D H@~¿ÿ%I§žzj8ݹø€IMMUíÚµ]õõê&hµjÕ\ŸS|ˆ 0€‚æî?D·ß^Ý•¤Otý«ïCP… T½zuW}½ºø©Y³¦²³³Ývçâ"Èmöê&¨ÖКÆ×_0 Ü>íåÅwÿ :¸ ÀÛ·o×¾}û<9gßß"„ $¨  ãîcŒëu“á g.^…OÀ@‚r€7mÚ¤C‡yrÎ0ïþ·ðä¤ÊÅS9èའæ‚`@4H ºÁp{ñcY–~ùåwÜqå>gîþQî“(•1¦¤ö’KjtÔŸ$ec¶JZ^äó˲¬`*Fy…€½z¨aÆÊÊÊrûH5@D€Q"cL²¤ê’¶p¡_Â}üÍ‹æÝÿ°Ò2€ðcÚK)©—¤äºÖÔ¹àSh—1f¤,Ëú2rU"‚6ƨE‹Z¾|¹›î|ˆ0$IƘS$],©Ž½û_OöÿOöc¾”ô¥¤Ÿï-ËòæÙXø.ˆ‹Ÿ¦M›*==]tÓ»ÿ@„cþ iޤÓËq˜*’®t…1æÿIzÒ‹Úàš5k*##C999¥öõz&h—¸©1&ë ^#'0cL’¤K%–Ô®”î•$]Xð)t°àîÿ?,ËZ™*)A<þ–””¤N8Aß|ó›î`ÀcÆ#i”¤q’Ò=’½¼]ÂËÈÈP“&MÔ¤I“ K‰®úyyT²¿\`žà9pœ3ÆT•t_Ðu¥víÚúúë¯%IyyyÚ²eKÈ€ºuë\­ÿ‚ÎÌÌTµjÕ´cÇ7ݹøÊÈÓ@ÒØp÷KKKSãÆU¿~}Õ¯__iiiúꫯôí·ßêÐ!o–dµçäBЂ¸ *ÙO}ñÅ®ºzzb8üERõ ‹ˆÉÉɪS§NB>æJ?.0?@Ù]%)Ëm礤$ýéOÒØ±cU½zñ¯Ã‡ëÛo¿Õ“O>©'Ÿ|²\¿,Ë*󾱬OŸ>ZµjU‰boÚ´I›6mò¥·ß–eéàÁƒž½VÆ“@u1I–eñøÏ€ã˜1æxI7‡»_VV–Ž?þx5jÔH 6TjjªV¬X¡+VhÏž=^ÕæÉqP>A=þV¿~}}ûí·nº2 ”]?·ÓÓÓõÚk¯©K—.!û¤¦¦êŒ3ÎдiÓt÷ÝwkÒ¤Izä‘G´wï^OŠMiiijÑ¢E©³á[–åË+;n°dßõ*‡1DФš’ü¹# !€ãÛ`Ùk©º’žž®{ï½W#GŽTÅŠ‹µ[–¥Õ«WkÖ¬YzôÑGËuÑ“¨wÿ ¤¶mÛ–xç¿ðO?¸½ø9xð §ç ãî?#À@cNtºÛþ³fÍ*1üU»vmýóŸÿÔàÁƒÕ½{w­[·®,e"cŒ’“#¿Za8øÀªR¥Š'ç-Ãdˆ`ž!Ç·ËÝv¬\¹²Þ{ï=µnÝ:dcŒZ´h¡qãÆéöÛo×äÉ“5eÊO׈wÉÉÉ®'a‰¦»ÿ–e)''Çõˆqi¸øÉ4ÆTµ,k§''‡ëßÿ]t‘ P¦“œ|òÉZ´h‘zôè¡+V”éNçÎ5nÜ8Ç›°Û¶m;æfuÀ“!.÷ìä8NcN‘t’ÛþÏ>ûl‰á·¨ììlÝwß}0`€ºw﮵k×–¥L” ŒuË,ÜÇßÀ’}ñCÂãúú]wÝU®Õ­[W~ø¡Z´háÛÓ+ðF³fÍ4v¬óIDATOÔâŋճgO-Y²¤LÇ@pzöì©äää“°>|øHßxvT†»ÿßyrb qœì¦S5Ô¡C‡rŸ¬J•*úûßÿ®áÇ—ûXˆ)))ªW¯^¸¿¯] ó˜`ž"ǯsÝv¼óÎ;Ëu¢ZµjiÁ‚jÞ¼¹6oÞ\®cÁ_Íš5Ó¨Q£Û,ËÒŽ;ŽâJ•*yv^îþ‘cŒI“tœ›¾]»vUJŠ7—C‡Õ#<£Ð(Uvv¶’““•——离7/@È?c‰ ¸zü¹~ýúºà‚ Ê}²Ê•+ëoû[¹ƒèaŒQvv¶N>ùdµk×Îqb´²*Ã#ÐÜk.—7¸O>ÙÕ@±+IIIš4i’gÇC|ËÊr½BW…HÖ ñ€ã1¦’¤nú^rÉ%ž-I4|øpýáðäXˆo5jÔ§»wCÏ@b8ÁuÇ\wuåâ‹/Vß¾}==&âS7U]'epƒŸ\O~ÅÝ!%%%œõ$¹û„Çõòwnf¤×øñã=[/ñ‹`A!ǧãÝvlÞ¼¹§'nß¾½z÷îíé1ŸÂ¸øáî?W/VJ‘™m¾iÓ¦3fŒçÇE|a@PÀñÉõŒ&7öüä=ôÒÒ\@ A…qñÃÝ <®?zW/Ýu×]ªS§NDŽøÀ0€ €ã“닯Þÿ=Z³fÍ4zôhÏ‹øÂ01àŠ+êˆÈ±p| üâç/ù‹jÕª‘c#>0 DLàß’tõÕWëÌ3ÏŒØñÛp| üâ§råʺÿþû#rlÄF€ˆqýŸïºkØ’’’4yò䈱› ‚BŽO`I6l˜N?ýôˆ±‹ b¢â;@’.¼ðB]yå•=b7A…Ÿ¢æî?Ë"!.~€ˆqý‹}÷îÝ‘¬C’ôàƒ*###âçAlá&(€ €ãSÔÜýoÛ¶­úôéÑs 6qñDÌ·üñÇHÖ!É^mà¶Ûn‹øy[¸ šnŒIŽd- 8>¹À;wîŒd’¤ñãÇ+===âçAlaˆ˜5n;ú€%éÎ;ïT½zõ|9bCjjj8Ý]/ï¥!ǧ­n;®Yãú:©ÌŽ;î8Ýzë­?b #À@dX–µY’«g›ý ÀYYY,‹„c:tÈmW˲¬ƒ‘¬@b!ǧÕn;ú€%é®»îR:u|9bCJŠëú)Ƙ°† ¸ûð+KÒàÁƒÕªU+ß·薓“ã¶«ë¤ n€ãP8wÿý À•*UÒßÿþw_Î…Øpð ëú–eY‡#Y ‡\ýrÿÏþñ¹ c4eÊ_Î…èÆw£¿×sA|ùå—‘¬£˜FiÀ€¾žч`A!Ç/×·ôý| Z’|ðAeffúzND.~€ˆrý‹ý£>ŠdŽÂ˜qŠïA!Ç/×?ÿ¿½{®²¾ó8þyV@êmÁ]{ÑÕJìÚñÒ:ÚZƒÝÑva×ÁµÖ®®U±Pªîx«¨…q‹J-B*BQ@@²DjjQP¹ KÂ%„„Ø„;'÷ïþq’ò{NÎïÜž÷kæLHç÷{Χ­ò<Ÿßsûøã}æø‚³Ï>›³ÀàhÀ#3Û-ÉéòžÒÒÒ”Ý ´ã)ÐÒ…œ»œ ð;ï¼ã3ÇQqÿ¸ðÎé/÷ºº:­^½Úwà0œ.àefŸIªqËê?ÒƒÀ;çk›Ó±Šhc ](À¹Íéà§¶¶Vk×®õ8 g€ïœ[m:îF´Åb1ס`IEÎmK]²úTã`À/3ûTÒN—±ìjUUU®CwùÌ z(À¹ÍyIŸÕ¤«ÿ@J85Ûšš­_¿Þw@’ÔÜܦ§î]"œÛÖIúÌe «ÿHµ?|ær—A#ãTUU©¥¥Åux¥Ï,¢‡œÃ,þd+§ƒŸêêꔿÑ‹ÅT[[ë:œÕ qέvÞ¼y>s*** g ©(À¹o©óÀ¥ÎCNáàH™5’ê\i÷îÝžãì¤8÷9¯þÏ;×g ?@j´] ä´hjjb?€”¹àhIEÎ}k$9]kºdÉíÙ³Çs@Ú¾=T§ u¤à 渜5k–Ï€¤PØ$íð@Q€s\˜û€¹ )Á` ¥æIjt¸dÉíÝ»×sD]ˆ}@™5ùÌ z(ÀÑàÜjgΜé3 )ÔÁO³¤jQ€œgf{$-qÛÐРùóç{N„¨ q  ’Ž s%9­ .^¼˜Õx¢W™Y«Ï,@D8_ÛÌeÐð-Ä>€û$8Ìl—¤—± zë­·<'BÔ…8øáþ_ 9œB µÿ~ÏqUuuuª®v¾°‡3À’ŽÎKú\ ßB`~€$h[-v[__ÏeÐðfùòåa†ÿŸ¯¢‹o*~?åq±úŸ¶oßæiã` yœW7 |æ@„…,À¡€ pD˜Ù_%-u[__ÏeÐðæƒ>3|ƒ¯@Í•ãBhQQ‘ÊËË=ÇA…Ø´HZå1 €ˆ¢G‹óeЯ¼òŠÏˆ°«ÿúÊD™ÕJzÛq¬^zé%ωE!öëÍì Ï,¢‰-s_Q=®Â°ïjœ„Xýo´Úc ŠœB ÔÜìtÂpRQQæX,€ð‚!fV#©Ôelkk«&Nœè9¢ÆÌ´bÅ ×á›™ÓSk8›.é€ËÀêêj†…¤ y €àèyÝuà¤I“ÔÚÊ+X‘<7n ó,~€$3³½’f¸ŽñÅ=¦AÔp €L@Žž×$9ÝSSQQ¡E‹yŽƒ(aõÈέvñâÅÚºu«Ï,ˆû€&I{Œ Â(Àcf{â,ð„ <¦AÔ°ú¤Ÿ™½/闱܃d‰Åbzÿý÷]‡¯1³zŸyD8šœ[í‚ TUUå3 "$ÄêÿAIë³ ‡-Z´H±XÌuxIÛ ÿƺ\¼x±V­Zå3 rPKK‹æÎf €Wà3³RIŸºŽgõ‰ ùð~€Ô™,i§ëà§Ÿ~Úcä¢ÒÒR}öÙg®ÃWšÙq€ ýÁuàÂ… µfÍŸYƒ´`Á×á-’ÞôÀ!̬AÒó®ãgΜ©òòr‰kfÍr¾ÛJb@ P€á¼úof¬þ#´¢¢"íÝ»×ux©™9Ÿ*ã$íwØÒҢѣG{Žƒ\±{÷nM™2%Ì 0ï(Àgfõ ±ú?mÚ4UTTxL„\Ãê?ÙÌl·¤ ®ã TSSã1rÅ /¼ }ûö¹_kf|扌¸ñr\ýonnÖïÿ{Ïq+jjj4}út×á&iŽÇ8Ží9ÅŸÀ~\±XLÏ>û¬ç8Èv±XLcÆŒ 3…P)AFû+‘œWÿ'L˜ ºº:‰+ÆŒ£úz§×MKñw?VúÌàè̬B’ójÕØ±cµc¯jűMœ81Ìï$éu_YàP`´{NR³ËÀè™gžñÙnß¾}7n\˜)¬þéåü‡X,¦ßþö·>³ ‹577‡½J`±™­õ•E†¤ŽÕÿi®ãŸþyUWW{L„l—ŸŸ¯Ý»w»7Q€´2³5’¹ŽŸ8q¢ÊÊÊ<&B¶š6mš¶nÝfÊï|e€#Q€q(çÓºÔÈ‘#}fAkhhÐsÏ=fÊ›¼ûÈO¹lnnÖðáÃ}fA235*Ì”÷Íìm_yàH`t0³Õ ±ú?aÂmÙ²Å_ d­)S¦¨ªª*Ì”PGKü0³?K*t?}út­^½Úc"d›3fhíÚPW3sö@JQ€q$çQcc£üqŸY…Z[[þ/z©™½ï+€ÐQü¶„ãjmmÕ£>ê9²E]]† fÊzIs=Å€£¢ã0föޤ%®ã§NªuëÖyL„lSPP M›6…™ÂÙ_ ƒ˜Ù*IÎ/ðž?¾–-[æ1²Å°aôsçÎ0Sž63§ÅH 0ŽÆy9¿µµU¿ùÍo|fA©ªªÒ<fÊÇfæ|¹%€”yLR‹ë`öX´h‘^yå•0S*$½ê)_`fË%½á:~Μ9Z¾|¹ÇDȃóäg‰³¿@F2³ ’&»Ž/))Ñܹ\ÉUûöíÓ]wÝvÚh3kò‘¾ Çò˜¤V×Á=ôÇ(ȯ¿þºÞ|óÍ0S6KzÝS÷¤¤×Á¿þõ¯U__ï12Õƒ>¨ŠŠŠ0Sª%½ä)|) 0ŽÊÌÖIšê:¾¤¤D³góרª­­Õ!CÂNmfΗXH-3Û&)ßuüæÍ›õ»ßñ@ߨyçw”ŸïüI»_šÙyàxž=€c ‚àlI$usÿõ¯]ëׯW÷îÝ}ÆBºõÖ[5uªóz‰?û{™Å2€+ 0¾TÛŽÊyõ¿¡¡Õÿˆ¨­­U^^žª«CËTIúoO‘x`fûòßÛ¡C‡ª¡ÁùùYÈ"f¦;î¸CK–, ;u±™½ì!„B†‹§bõÁ‚š?¾Ç8H·X,¦n¸A6l;u°™…zO€ô3³i’þä:¾¬¬L÷ÝwŸÇDH—‡~8ìû~%i¿¤_xˆ¡Q€q\mOj¼?ÌœaÆñ:ŒÕÒÒ¢AƒiÙ²ea§Î4³PïIQ†Hr~rû¸qãTPPà1RmìØ±5*¡×·?bf¡n_(Àpbf3$-u_^^®¡C‡ú „´¹÷Þ{5oÞ¼°Óê?x¥Ìlµ¤Â̹çž{´|ùrO‰J#FŒHt¿^¢ÿÜ€O`„1DR³ëà &èü£Ç8Hµ#F$úÿé}f¶3Ùy¤ÜpI5®ƒ4`ÀUV:ßEƒ ‹Å4hÐ >\ ¼:s¤fÖê!$„ gf¶FÒØ0s†šÈ¥²È0­­­zðÁ5|øðD¦Ï1³ÉÉÎ õÌl¤‡ÂÌÙ¾}».½ôR­X±ÂS*ø²cÇ]uÕUš1cF"Ó·Kº¾íŸÈ`„õ„$ç3y8p`Ø÷"ƒìÝ»W7Üpƒž~úéD¦ÿEÒ$9€ôzYÒ{a&´©Y³fùI„¤[¾|¹.¹ä}øá‡‰Lß­xùÝžäXÐi`„Ò¶’û`˜9UUUºì²ËôÞ{¡Ž—6mÚ¤Ë/¿<Ñwzntƒ™ñ44 ‡Xü:ØŸKÚfÞÁƒuÓM7iäÈ‘^r!y¦OŸ®ïÿûÚ±cG"Ó$ý¤íª1È8`$bФP×5ïܹSW_}µ^{í5O‘lEEEºôÒKµ~ýúD¦WKêofuIŽ ˜ÙI?•ê¦P3Óc=¦k¯½Vk×®õ Û¹s§n¿ývÝ|óÍŠÅb‰lÂ$ýÌÌœ_™©FFhm«ÿ?Sü'gõõõºå–[}Rh̘1êß¿¿víÚ•Èôý’þÅ̶$7€Lbfó%=žÈÜââb]tÑE2dH¢Ï ‰ššš4zôhwÞy}uÕýföz²r€`$ÄÌ6IºERè';Ž1B×\sÖ¬áê¨L³uëV 0@Æ SK‹óë>Õ,éßÍle’£ÈL#%ÍIdbKK‹ÆŽ«sÏ=WãÇOôïtRaa¡úöí«x@{÷†ºªýHÿcfÏ%+øBFÂÌl¡\ýûí·õío›Õÿ ‹Åôä“OªOŸ>š3'¡cÙv¿0³ÂdåÙ¹"h]¢Û¨­­ÕàÁƒÕ·o_åççkÿþýÉ ˆcÚ´i“n¼ñF]ýõÚ°aCg6Õ(éN3{$IÑÀ«€KQÑA’fKú×D·Ñ³gO9RwÞy§N8á„ä…ƒ“7ÞxC÷ß¿¶lÙÒÙM=df£’ )Á‡üú©™MOWd· Î’4OÒÅÝÖ)§œ¢Ûn»MƒÖùçŸßùp8ÌG}¤ñãÇëå—_Vcccg7÷™¤fVš„h`tZ'Kz_RŸÎl§OŸ>‘t£™ñžCY…Œ¤‚àLÅﻼ³ÛêÑ£‡n¾ùfÝ}÷ÝúÎw¾Óùp8ÌŠ+4~üxM™2EMMMÝÜ6Å_w±* Ñb`$[ÛUA#$=šÌížqÆÊËËS^^ž®»î:õêÕ+™›ÏY7nT~~¾&Ož¬ºº¤>”ÿMI?5³ÉÜ(¤IÁI’ $ JÖ6/¹äÝ}÷Ý4hºwÍFÎ4mÚ4åççkÅŠÉÚì»’˜YM²6ˆÔ¢Ã— n‘4QÒ‰¶­‹/¾Xyyyêß¿¿¾ûÝïªK—.Éþš¬UUU¥¢¢"MžÞº0RÒpã@–¢#é‚ x\ÒÉÜæ©§žªë®»Nýû÷W^^žÎ:ë¬dn>g}òÉ'ÊÏÏ׫¯¾ÚÙ§{i¢¤ÁfÖéÈ>`øÁå’fJúªÏï9å”StÍ5×èÊ+¯Tß¾}Õ¯_¿H!Þ½{·–.]ª’’kݺ„ŸGv<ûØÕ __©@†A RülðI>¶ß·oߎÕÿ+¯¼Rݺuóñ5Y©¬¬LEEEzùå—“y¯W»I÷™ÙóÉÞ0R ß‚ è!éI÷ËÃÙàcéÕ«—úõë×Qˆûõë§o}ë[:é$/»¤”ŠÅbz÷ÝwU\\¬ââb­\¹Ò÷+¤LÒTIšY•Ï/€T Ã› .“4MÒ9>¿§Gºúê«uÅWtð|ík_óù•¥²²R%%%ŸmÛ¶ùúªÅïùZâë Z`¤Jÿ(i´¤Ÿ¤+à 'œ sÏ=W\pÎ:ë,õîÝ[gžyæa?{÷î­®]»¦+b‡šš•——õSYY©ÖÖÖTEY)iˆ™ý%U_¾Q€áUÛ}ÁHzXRÊnâ=í´Ó:VþÛ^xá…:ùä“SÁ›êêj•––vÞ7úþÊfIc%=af{|R‡ŒT ‚àZItAº³M:ýôÓ+Ægœq†N<ñDuëÖ­ãÓµk×ãþÞµkW566ª¾¾¾ãÓÐÐpØï‡~vìØ¡²²2mÞ¼9Þ…üWÅdö’™¥¬m@*P€‘A|UÒ3Jâ²È ³Ï>[ßüæ7;Vú]õoÿsÏž=iz´¶¶ª²²R›6mRYYYÇÏö?ïÛ·/•q–*¾ú¿&•_ŠÔ #‚ è"éÅ Vï4ÇÁáZ$Wü!W»Ò| #¥‚ ¸JÒó’.Jw–céÒ¥‹zõêuX)îÑ£GÇ ûÊþñþÜ¥K—/¬öÇb±cþ¾ÿ~mÞ¼YåååjhhH÷ÿ •’ å6 0Ò)‚®Š¿3ønI?HoHZ"é~3[î à)Á ’îR|õÿÒ‡k”ôœ¤‘f–ökðà™"‚>Šïn“tZšãDÉ.IS$å›Ù§é©ð7é€è1³3'é’J*Rü)“HŸIoHêgfQ~¤’™­7³aŠ/ŠÞ.iyš#åº$ý§¤³Ìlå@”p!‚Rü2¸ŸKê™Þ4‘²]ÒKŠ?è¤2ÝaZœF& ‚à|IymŸHúÛ´Ê~$½&i¼™­JwH 02J'JºIñ2ü½4ÇÉU&é%åKzË̼¾@™‹ŒlÑöF+%õW¼gäS¤3P½¤e’fIšjf{ӜҎŒŒÁ7?Øé/釒NIo¢¬W#i’¤ÍlsºÃ ý(ÀÈVmoÈS|ÿp…¤¿Oo¢ŒÑ¤øåÍoK*‘´ÌÌêÓ 2 Y¡íµßÓç«ÿÿ,)}ï*ʵ’þ¤øêÿl3kLsd 0rE=%õmûôkûy¡¤éÌ•-’V)^vß–TjfÒ 2Y)‚^úüÞ°+ …XÚ/©TR±âDÿ’ã(ÀÈeAü…îçèóBÜ·í÷^’ΔÔ-}éBk’´ERÙ!Ÿ’þlf{Ò˜ ²9!‚¯(¾Úßþi_ýï•Î\)Рøý]%mŸ̬)½‘-(Àˆ² NW¼÷nûyæQ~ï-éï/Ë>Y¿O·^ñ¿ÏëôyÁÝtÈŸ+x^$G—t’¡íµ=ïµ}:´)>´Ÿ§ø½bgJ:9Å1;c¯¤ò¶OYÛÏŠßßKg0ÈFf¶Kñ÷à®wßöû®_òévÄï]txÁ=òÓ`f­ÉûopAFN3³}~vô0At×—¯ü·ÿÞ]ŸØ´ÿL†#Œb’ªtxÉ-—TnfMÒwÐv¶Eñ¿¯YŠŒÈ2³ƒ’6·}B ‚ ½ YŒýϺêKVÿ¹7H- 0€¶ûl›$ñ´M KüMº `@$P€‘@D `@$P€‘@D `@$P€‘@D `@$P€‘@D `@$P€‘@D ÿŠu•LÂîÏÚIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/settings/tablet-keys.png0100664 0001751 0001751 00000006147 12471273273 031534 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDRb—³NsBIT|dˆ pHYsÃÃpMBtEXtSoftwarewww.inkscape.org›î< äIDATxœíÝÏkÜw~Çñ÷÷;²G,•TZˆM”¥Y“kIºÉ¶CI KKÛKï…þ/¹,ôÒî_B {ñqI“¶)9µakJ¡Nâ’ÄIˆÉ3IóýôUšÙÖèë÷G3kôÒ‡õðœÉŒf¦)¥Äþ‹öø4Mó“ˆøUˆ0@Ÿ~+"n6MóõómM)åø1áÿˆ­´£¬–‹ˆ?*¥LŽïÿeˆ0Àãôûñ·ÿø_“sÛçñƒ&"ž‹ˆO> ÀªºÑFÄßdŸ`…ýd›Ù'XQ_61‰ˆõ쓬¨;MD̳ z×>ø"ôIˆ’ 1@2!H&ÄÉ„ ™$b€dB Lˆ’­õý†Ãaß? WÓé´×ýÞßk⻟ pÑ4MÓ뾇&’ 1@2!H&ÄÉ„ ™$b€dB Lˆ’ 1@2!H&ÄÉ„ ™$b€dB Lˆ’ 1@2!H&ÄÉ„ ™$b€dB Lˆ’ 1@2!H&ÄÉ„ ™$b€dB Lˆ’ 1@2!H&ÄÉ„ ™$b€dB Lˆ’ 1@2!H&ÄÉ„ ™$b€dB Lˆ’ 1@2!H&ÄÉ„ ™$b€dB Lˆ’ 1@2!H&ÄÉ„ ™$b€dB Lˆ’ 1@2!H&ÄÉ„ ™$b€dB Lˆ’ 1@2!H&ÄÉ„ ™$b€dB Lˆ’ 1@2!H&ÄÉ„ ™$b€dB Lˆ’ 1@2!H&ÄÉ„ ™$b€dB Lˆ’ 1@2!H&ÄÉ„ ™$b€dB Lˆ’ 1@2!H&ÄÉ„àœ5MóH—_ëé+åé§ŸŽ^x!žzꩇñÑGÅ­[·âwÞ‰®ëNýÞ&"JŸ‡+¥×y€Þ=èîË/¿/¾øâ‰—ûðÃãõ×_û÷ïÏý~M,àÆñÒK/Íõõë×ãÕW_=uCˆΨišxå•Wx¹gŸ}6žy晹_b€3zâ‰'âêÕ«uÙëׯÏýšœÑÖÖÖ¹\VˆÎè³Ï>;—Ë 1À}ñű»»ûP—ýàƒæ~MˆpóæÍ^æ½÷Þ‹÷ßî×…`·nÝŠ7ß|sî‹6nß¾ýÀX{AÀ<ÌK–¯]»Ï?ÿü÷^Y÷î»ï>°ƒ½¿Äùµ×^ëûG¤»sçN¼ñÆgúÞÞC<›ÍâÉ'ŸìûÇôâã?îýgôâ«W¯ÆsÏ=×÷èÅÞÞ^ï?ÓuÉ„ ™$b€dB Lˆ’ 1@2!H&ÄÉzeÝ£ºtéRlnnÆÚÚÚC½ÑƱ®ëb2™ÄÞÞÞ©o°aß¾ýúöûTJ‰£££¸ÿ~ìïïgçDU…¸mÛØÞÞ>Ó?`Û¶±±±1÷šíÛ·_ç~߃A ‡Ã¸{÷neç{ªzhb}}}áÄÑh4wþ}ûuï÷m4eáDÕ…xQƒÁ ÖÖN¾£oß¾ýº÷û6³p¢ªBܶçsœy;öíÛ¯¿Oµž±ÎS¬!H&ÄÉ„ ™$b€dB Lˆ’ 1@²ªB|Ú»:ÇŽ}ûöëßïS­g¬*ÄÓétá®ëâððо}ûp¿oµž±ªO&“…o±ö÷÷çnØ·o¿îý¾M&“ì#œ¨ªÏf³ØÙÙ‰®ëù{K)1Nç¾Wª}ûöëÝï[)%vwwãàà û('ªêá#"ÆãqŒÇã ô}]×=Ô­±}ûöëÛïS)åë?µª.ÄÇf³™}ûöí¯„ªšXEB Lˆ’ 1@²êž¬»|ùrlllÌý€ÂyJ)1™LbooïÔgGíÛ·¿zûGGG±··Wí :ª qÛ¶±µµuæäÞØØˆ¦iâË/¿´oß¾ý¯ ƒ‡ñÉ'ŸTùU=4±¾¾~æÄcW®\±oßþî_¹reáýÑh´Ð÷÷¥º/j0ÄåË—íÛ·¿dûÃápáýó8cª ñ¢·vÚ±oßþjïŸ×Ï[U!XEB Lˆ’ 1@2!H&ÄÉ„ ™$b€dKâ¾?›Ê¾}ûw¿FU…ø<Þ¢®ëº8<<´oßþ’í/³ªB<™LÞØßߟ{‹jß¾ý‹»¿Ìª ñl6‹{÷îE×ugúþét»»»öíÛ_ÂýeVÕÃGDŒÇãÇѶí#½SR×uuKjß¾ý‹»¿¬ª ñ±³ÞªÚ·où÷—MUM¬"!H&ÄÉ„ YuOÖ]ºt)677cmm푟uL&±··w곯öíÛïoÿòå˱±±kk矖¶]ÞûU…¸mÛØÞÞ>ÓüµmsѾ}ûýîommUû5«ê&f}}}áÄÑh4wþ}ûýí_¹rE„Ϩº/j0ÌýÏ"ûöí÷·?Þ_UU…ø¼š·cß¾ýþöÝ>»ªB °Š„ ™$b€dB Lˆ•Qëov1@2!H&Äɪ ñy}fÕ¼ûöí÷·ÔzöªBË«”GGGqÿþýØßß?õ²µ^?kÙÏRUˆÛ¶ííí3¦mÛØØØˆˆ˜û»Ž«¼Ïr 1ãîÝ»qtttâej¾~Ö°Ÿ©ª‡&Ö×׎Ìh4š»aŸe7æ~í"\?3÷3UâE ƒX[;ù޾}–Ýp8œûµ‹pýÌÜÏTUˆÛö|Ž3oÇ>Ëî´ëÈE¹~fígªïD+Fˆ’ 1@2!H&ÄÉ„ ™$b€dB ¬ªŸ×»"Í۱ϲ;í:rQ®ŸYû™ª ñt:]x£ëº8<<´ÏJ:í:r®Ÿ™û™ª ñd2YøÖjî†}–Ýd29õkµ_?3÷3UâÙl;;;gúØRJL§ÓSßkt•÷Yn¥”ØÝ݃ƒƒ¹—©ùúYÃ~¦êÞn<Çx<ŽÁ`ðHß×uÝCÝÒ­ê>Ë«”òõŸ©õúYË~–êB|l6›ÙOÜg¹]ôëç²]ÿ«zh` 1@2!H&ÄÉ„ Yï¿5ñùçŸÇ[o½Õ÷èÅãøÝã&"êýå:€à¡ €dB Lˆ’ 1@2!H&ÄÉ„ ™$b€dB Lˆ’µq;û+lÒFÄ?gŸ`…}*Ĺ~ÑDÄïEÄfŸ`EÝhK)·"âÓ쓬 ·K)¿>þ­‰L= Àjú»ˆˆ¦”MÓ #â—ñãÜ3¬ŒO#âZ)å ˆ(¥L#âO#â¿S°îFÄOK)¿¹G|¬išFį"â·sΰônEÄŸ”Rþçø/¾õʺRÊEÄŸEÄgù`«à͈xᛎ8á%Î¥”·#âZDüUDüÓc9Àr»?‹ˆ?.¥Üûî¿õÐÄI~ópÅ_GÄO#â1êáËäýˆx;¾zÁÜÛ¥”Û§]øÿe§3 &î¬ÑIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/settings/zoom-in.png0100664 0001751 0001751 00000003021 12471273273 030664 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR€€¤Çµ¿sBIT|dˆ pHYsÃÃpMBtEXtSoftwarewww.inkscape.org›î<ŽIDATxœí×Ái$@ÁëCþ»\²`õÑcaAÁ $}ÛþÐóÿõÞ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆ€(ˆúz=~rw{½á7¶Ýë ð‰ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ ê¶½Þvw€¬m÷zC™ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ J¢ ê¶½ÞÝÝŸþ ÛîõøÄ%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%Q%QuÛ^oà@”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @”D @Ô7²ó~ŽZãIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/settings/touchpad.png0100664 0001751 0001751 00000112175 12471273273 031116 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDRÀ€b ÞsBIT|dˆ pHYsÃÃpMBtEXtSoftwarewww.inkscape.org›î< IDATxœìÝwœTÕáþñÏÙ]ê"]P±‹-gÔX@¢ˆbEEQcLâ/Ñ|“hLâHQ”f±`%†¢‚3Š{! ˆQ©ÒÙßgVvgïݹ³;}ž÷ë5a¹çÜ™3ã†;Ï=Í·“‘H$#¯+""–1æöL¼®þý‘L)ÊtDDDDDDDÒAXDDDDDD ‚°ˆˆˆˆˆˆ`) À""""""RJjyÞvà»èÏ-¦ÉiŽˆˆd¹Ÿ€ïr`O Ef›#"""â]mðà‰H$²ÀÓ ð'µU""’m¶³"‘È’Ê1¿F7BEDD$$:ú3`bEø‘‚1#6üD"‘/ç2Б„%€?žŒD"Û1ÍSÕ É:«"‘ÈGÆ˜ÆÆ˜ÓŒ1=Œ1¥‘Häk`EF[("""âA"x€1æW@ÿÔ4GDD²Ðו~þpp"à«tü;DDDD²\"s€+SÓÉBí1g[#*ß»ÒÏ[ÒÛ$‘Ä%€JY+DD$›µ>bU¾ºgšÚ""""RkÚXDDjcð€1¦Ø7³Í‰OXDDµx*‰lþýx´ ’ˆˆˆä`IÄ.àñH$²ÀÓ8%³MñFXDD¼ŠÏF·=ÂÓÔËh«DDDDDD¤°ìŸéˆˆˆˆ$ƒ†@‹ˆˆˆˆˆHAP‘‚ ,""""""AXDDDDDD ‚°ˆˆˆˆˆˆ„’H$r{¦!""÷Y$y"ÓI%õ‹ˆˆˆˆˆHAP‘‚ ,""""""AXDDDDDD ‚°ˆˆˆˆˆˆ`) À""""""Rr)ÌtDDDDDD$wåR\ŸéFˆˆˆˆˆˆHnÊ¥ 0¸.Ó‘Ü“k`,pM¦!""""""¹%0À½ÀðL7BDDDDDDrG®`Ü\醈ˆˆˆˆˆHnÈÕ 6®ÊtCDDDâ0@½L7"Ã:fº"""¹€Á~¡¸–醈ˆˆ+}2Da‡àÉ@÷L7BDD [®`°_*®ÌtCDDÄQ Ð…à†v.f ,""”ì—ªñÀ™nˆˆˆ8jˆB0(7F!XDD2(_0Ø/U€¡™nˆˆˆ8R¶‚‚ED$Cò)ƒýR5’醈ˆˆ#…`K!؆àn™nˆˆˆ–œÀ»vír:\‚/OokDDÄ£Š\è‚á%àÔ ·CDD HNà#Fðý÷ß;“€ËÒÚ ñª!Ð"ÓÈ Á0…`I“œÀŸ}öÝ»wgÕªUNÅEØ-.Mo«DDÄ#…`«"—dº!RÑ|J¦"""ù/§0À§Ÿ~J·nÝj ÁƒÓÛ*ñH!Ø*ô\Ší >9Ó ‘ü–ólîÞ½;«W¯v*..Io«DDÄ#…`« Á³€“2ÝÉ_y€>ùäºuëVS~¸8­¯‚-…`…`I¡¼ À`CpœžàG€Aém•ˆˆx¤lzn‚ Á¿ÊtCDD$ÿäUøøã9í´Óøá‡œŠ+BðÀô¶JDD<*¨üÊ+¯¸D^°`Û·ow*jÌNLo‹DD$ßå]øè£èÞ½»[.¦¥·U""RÙo¼áVT0!øöÛoçÿø‡[qÞ‡àùóçÓ¯_¿x!ø„ô¶JDDòY^`°!ø´ÓNcÍš5NÅÅÀT‚ED2fÀ€Ì™3Ç­¸`Bð-·ÜRÐ!øå—_æœsÎq Á{/£,""I’·àÃ?¤{÷îñBð…ém•ˆˆlÛ¶³Ï>;^nžÆ&eÌ-·ÜÂ?ÿùO·â¼Á³gϦÿþñBðñém•ˆˆä£¼À`CpœžàG ÒÛ*Ý!øßÿþ·[•FHþíoË¿þõ/·âFÀäqž5kýû÷gÇŽNÅ!ØŸÞV‰ˆH¾Éû ðÁpÚi§ñã?:Ó€óÓÛ*Ý!øÕW_u«R0!øæ›oæî»ïv+nLa‡à¦À+€/½­‘|R<‡àém•ˆˆlݺ•³Î:‹ÿüç?nU &ÿæ7¿! ºW„àâôµ(½fΜɹçž[SžƒB°ˆˆÔRÁ`€%K–Ô‚K€Ç€óÒÛ*‚ýë_3wî\·*‚oºé&î¹ç·âÆØ9Áy‚_zé%Î;ï¼x=ÁÇ¥·U""’ *ƒ Á§Ÿ~:k×®u*.ÎMo«DDv‡àyóæ¹U)˜5j”[qÞ‡à3fÔ‚›a{‚™ÞV‰ˆH®+¸ ðþûïÇ ÁOýÓÛ*زe }ûöUn¼ñFFíV\!xÀ€ìܹөX!XDDVà½÷Þó‚ÏIo«DDv‡àùóç»U)˜|à 70fÌ·â¼Á/¾øbM!¸96›ÞV‰ˆH®*Ø 6÷èуuëÖ9מD!XD$#*Bð‚ ܪL9rd¼œ× c½ð œþù5…à]ÓÛ*ÉE€Þ}÷]N?ýôx!¸_z[%""›7o¦OŸ>¼öÚknU *;Ö­¸”<ÁÏ?ÿ¼—ü‹ô¶JDDrMÁ`°!¸G¬_¿Þ©¸08;½­°!øÌ3Ïäõ×_w«R0!øúë¯çÞ{ïu+.ˆ|Á¸…àÀ«(‹ˆH €£Þyçx!ø)à¬ô¶JDDÀsn–Æ&ë#¸ï¾ûÜŠó>?÷Üs\xá…ìڵ˩¸¶'ø˜ô¶JDDr…p%‹/ö‚ÞV‰ˆÀO?ýÄ™gžÉo¼áV¥1‚¯»î:ÆçVœ÷!øÙgŸ­)·ÄöÞV‰ˆH.PޱxñbÎ8ã 6lØàT\xè›ÞV‰ˆØÜ»wo.\èV¥ Bðý÷ßïVœ÷!ø™gž‰‚ÿƒB°ˆˆÄPvðöÛoÓ£GšBð3@Ÿô¶JDD`w^´h‘[•‚Á‘H„k¯½–xÀ­JA„à‹.º(^OðQém•ˆˆd3`o¿ýv¼žàgQɈM›6Ñ«W/…àH„k®¹&^>€<ÁO?ý4t Á­°=ÁG¦·U""’­€kðÖ[oѳgO6nÜèT\Ñ|fz[%""°;¿ùæ›nU *?Þ­Jò<?õÔS 4H!XDDâRŽ#sÆg¸…àØžàÞém•ˆˆÀî …ܪL>|8&Lp«R‚óöº?}út DYY™Sqkl>"½­‘l“·Âd ‡Ã5õ7žz¥·U""°qãFzöìI8v«R0!øê«¯fâĉnUš`ççíµßcî’ÞV‰ˆH6ÉÛ‹`²…B!zõêUS~è™ÞV‰ˆØ|Æg(G"\uÕULš4É­JÞ‡à'Ÿ|’‹/¾Ø-ï ÌE!XD¤`åí0Þ|óMzõêŦM›œŠ+Bðém•ˆˆÀîžà·ÞzË­JÁ„àaÆ1yòd·*y‚Ÿxâ .¹ä’x!øðô¶JDD²AÞ^üR%Nn¼€B°ˆHFlذ3Î8ƒ·ß~Û­JÁ„à+¯¼’|ЭJÞ‡àÇœÁƒ+‹ˆHy{áK¥E‹y Á=ÒÛ*Ý!xñâÅnUMÓØ¤ŒˆD"\qÅñBp^/ŒõØcÕ‚Û`Cðaém•ˆˆdRÞ^ôRmÑ¢EôîÝ›Ÿ~úÉ©¸"ŸžÞV‰ˆÀúõëéÑ£GM!¸” ÁW^y%=ô[•=(€|饗R^^îT\‚Mo«DD$Sòö‚— .¬)7^NKo«DDv‡àwÞyÇ­JA„àòòr®¸â ~øa·*y‚§M›VSn ÌC!XD¤ äíÅ.]Þxãx!xÐ=½­Ø‚ß}÷]·*‚‡Ê#<âV%ïCð£>Êe—]VSž ’ÞV‰ˆHºåí….Þxã Î<óL6oÞìTÜx …`‘ŒX·n§Ÿ~:ï½÷ž[•‚ ÁC† aÊ”)nUò>O:•Ë/¿Ü-ï…í >8½­‘tÊÛ‹\º½þúëñBð  [z[%"" \¡¼¼œË/¿¼ Cð”)S‚ED XÞ^à2áµ×^«)7ÆöŸšÖF‰ˆk×®åôÓOçý÷ßw«RP!xêÔ©nUö:“ÇߦL™Â!CÜBðÞØ|Pz[%""é··Lyíµ×èÓ§OM!x&pJz[%""°;/Y²Ä­JÁ„àË.»ŒG}Ô­JSò<?òÈ# :T!XD¤Àäí…-“,X@ß¾}Ù²e‹S±B°ˆHýøãœvÚi Áåå\zé¥L›6Í­JÞ‡à‡~˜+®¸Â-ïƒ Á¦·U""’Jy{QË´ùóçÓ§O·\Š Á'§·U""»Cð|àV¥ Bðc=æV%ïCðC=Ä•W^I$q*®Á¤·U""’*y{AËóçϯ©'¸˜œ”ÞV‰ˆìÁ~ø¡[•Rì|ؼVVVÆàÁƒ :?øàƒ5…àvÀ|‚EDòBI¦MºwïÎõ×_Ÿôçýî»ïèܹ³SQEî ¼‘ô‘­Y³†îÝ»3wî\Ž8â§*M¢nJc³Ò®"c¸è¢‹œªT„ीcJÌu“'O`âĉcb‹Ûa{‚Oþ›Ö†‰ˆHR)W²xñbºuëFÓ¦iõÖ˜B°ˆHF¬Y³†ÓN;¹sçÒ¥K§*‚/¹äŒ1\xá…NUšb{Aó:c˜0a‚Sní >…`‘œ•·Ã™jcãÆLœ81/]‚•‰)t?üðÝ»wçã?v«Ò„}ñÅóä“OºU©è ®–óŤI“¸êª«Ü†C·ÇöïŸÞV‰ˆH²d>è ÷] FÍ®]»ÒØšŸ)‹ˆdB°UVVÆ Aƒ˜>}º[•fäyž8q"W_}µ[î€í VÉA€{õêÅ;ï¼Ã‰'žèXþÍ7ßðÔSO¥¹U?SÉ Õ«WÓ½{w>ùä·* ÁVÞ‡à &0|øðšBð< Sz[%""uUPs€ÌäÉ“)))ᦛnbáÂ…Žõ‚Á Û" _3RÙÆ¨žÀb`[^KDD*©ÁóæÍãÐCuªRs‚wíÚÅ Aƒ0Æ0`À§*!ø¿äéœàñãÇcŒaܸqNs‚÷ÅöŸ,KsÓDD¤– &ÿþ÷¿çŽ;îøùïgŸ}6;wæ¿ÿ­¾ŽÅâÅ‹yýõ×9é¤j;µn6¤²­""’Y«V­¢[·n9‚Û·oOûöí“úœcÆŒáCq[%;«Bp³fÍ8þøã“úœË–-ãÙgŸåÜsÏu*®Á§¢,"’ò>1vìX®¹æšjÇo¸áFŒáxÞÝwß퀛À¦¢­""R{;v¤yóæI}Î@ À´iÓhÙ²¥SqÖ…àN:ñÚk¯¥ûe›açÃ~E†CðÖ­[y衇Øk¯½Òù²û¡,"’3ò:7jÔˆÇ{Œ~ýú9–_~ùåüéObݺuÕÊf̘ÁÒ¥K9à€jûÞî2²R–ˆˆ8ëÝ»7ãÆK÷ËfU~ýõ×yûí·ùå/™î—nN„à;vpß}÷ñ—¿ü%Ý/­,"’#òv¬–-[òꫯº†_€ÒÒR† æXV^^Î=÷ÜãTÔpœ %""™óÈ#°víÚL¼tvጻûî»3õÒ!8£ c=ðÀlݺ5/]‚;fâÅEDÄ›¼ À;vdáÂ…œp qëŽ1‚zõê9–=üðÃn_¦uk¡ˆˆ$Û–-[xà2õò{¦\ZZÊ´iÓØcçŨŸ~úi–/_žŽ¦8Iy6ÆpçwÒ­[7Çò5kÖ0eÊ”T½|< Á""Y.ïðÑGÍ›o¾É!‡â©~»ví¸à‚ ˶lÙÂøñ㊎N®u#ED¤VZ¶lÉØ±c]Ëï½÷^vìØ‘ÆU‘òܦMæÍ›ÇÀ:t¨c²²2ÆŒ“ÊfÄÓ»=PÒCpýúõ™:u*¿ûÝïøÍo~ãZoÔ¨QnÛE€í)~ì¼ $w52Iм À=zôàµ×^«iñ‹§cîºcÇŽuû2¥^`‘4ªÝsÝu×¹öþ}÷Ýw<ñÄinY) Áx o¾ùæÏó{GŽIqq±cÝI“&±qãF§¢ÍÀ{ix,#És›6mÊìÙ³4h`ç|vØaŽu?ûì3fÍšåT´;•©aЇ+ëø–ED$ò&_rÉ%Ìœ9ÓuHváªójããŽ9æ˜Ú|™ú5Pm…,I¾cŽ9¦Êèžšn\ƒA·¢­Àª4<¶Ôö}ºñù|,Z´ˆý÷ßÿçc;v¤ÿþŽõ7nÜȤI“œŠJF@yI³Ï>ûðúë¯Ó½{÷Ÿcjó{О̶‰ˆHnÉ‹ü»ßýŽGyÄm.oÛ[ˆþ|/P­K÷¦›nr}~—‹hpC-š+"" èÑ£ ,¨2º§OŸ>|ðÁŽõ—,YÂþ󧢆Ñ?s*üõíÛ—¹sçÒºuëje5ÀÑ£GSVVæTÔ6y­K½Ã;Œ7ß|“#<²ZÙÅ_L›6mÏ›;w.ï¿ÿ¾Sѵ@ƒ¤6RDDrFNࢢ"î½÷^î¼óNŒqœj´¸Ûû[á{àñØŠgžyfm¾L]8n)""uç6ºÇÃ7ÞèzžËK4NjSlذa<ÿüó4nìÜl¿ßïºàãŠ+xúé§Ššõ“ÖÈ:餓xã7Øwß}Ë4hÀµ×^ëz¾Ënm€‹“Ò@É99€ï»ï¾š.|ëžÀt‡²jߌjùeª1pUÜ†ŠˆHÂ~ÿûß3eÊוúìØ+ 0{öl>ýôS§¢R2¼MWþóŸ?~¼ë<ß µl°!0«{î¹Ì™3‡-ZÔXoøðá4lØÐ±ìñÇçÿûŸS‘û_DDòZNàVzþø°À¥üàÕØƒµü2uàüíLDDVTTÄ}÷ÝÇwÜQc½F1|¸ótÎH$âÖûWÄî¡ÐY©¤¤„É“'ó§?ýÉSý~ýúÑ©S'Dz·Þz‹7Þxé¨5Ps²Î #F0}út×`[Ùž{îÉàÁƒËvîÜɽ÷ÞëTt8ö&¹ˆˆ˜œÀ.>Ž>ŽS¯ÚmñZ~™Ú;ÌZDDê¨aÆ<óÌ3\sÍ5žê_{íµ4hà·›*G`?)ùÔüÐoáì¿Z—nÛ¶mÞc0Ö¶mÛ7nœSÑÑ@w§©= ÜÈŸå„'ûáŠíEÞf“¿×çÝ€ÀMÀ¿€iÀ<à3`“?~ßÿŸ?îš·""qì·ß~,\¸°¦ð» ø‡КæÀŽ7ŽmÛ¶9eU/ðÀ¢E‹j ¿[ÏÙ½•SMš4aذaŽ'–——3jÔ(§¢ú@ÍlÓ¤iӦ̚5«¦ð» 8˜ÝΰZ—nM¿£F¢¼Üqqnõ‹ˆ˜|éþ'pKœ:NˉY äã?¦K—.Ž'´iÓ†+V8 ¹› ôMðõ%†?Þû9ž ô 5+¶®^^懂¾jÛb‰‚lé>ú裙5k{ï½·Ûù;€uØðÛ¨¶á{Ïž=™3gŽãÉ'NäŠ+®p*ú¬ÓÁµ¸¼¼œ—^zÉitQ…MÀÊ{u!f+Ÿ•+WÒ©S'víªþ–JKKùæ›oœ”ÚŒ½Q˜Ž=À&L`Ö¬YuÔQnç} ô¾ªtlûùsÌ1n[ñ /pÖYgÅÞì‡ÃMÉO¹Þ\\ü–Ú áÚ†½ˆVqøá‡Ó³§óÚ«W¯æÑGu*:p]•Kjæ„ðÂ/`·©z€Sµ]I{lÏñ+Àj ü7 Ü4E¯%"58ýôÓyíµ×j ¿Û°Ã•+ºï¶àðï}M½÷Üs.7{3Þ Ü§OæÍ›WSø]‹ €±úV líÛ·çüóÏw|’Í›73~üx§¢Rb†S§Ó!‡›o¾YSø 'P5üŒ"Á߃»ï¾Ûépì¾À""R r¹x0x¶ŽÏ»'°‚˜UAçÌ™ã‚;ì0>úè#§7ÆW×±=Å·ÇeLfoȬÁÎ)»_=ÂR(2ÝõÂúáœØ'T$—;¶¦ð»çð 6WS‹Þ?CêFšÔÕ7¸‡_°¡½Ú×]»våä“Ov<áÛo¿åÉ'Ÿt*jNÌpê,ðvQËšBiµå­ëիLj#ÜOp^»5öú-"" Ó¡£6V'ŽÖÒ=$8”Êå"Ú‡9I²›?6þ@x$¶·á²o?ÎýÇ·ü°?Ó)@l¤cÈÚ…TÅ AƒhÛ¶­ã ¯¾ú*|ðSQ)+KgP9v¸ïjuWãpíºé¦›\Op¹vAÌZv+öZ;ì;ÖàÝØƒW]u¥¥Î£Û§OŸÎÊ•Ž÷n »~DD$Er-€Ýã÷Ó$?ï'À˱È^{íåx¿ÿýo>üðC§¢kɾ;éYÁ—Oa{Z[e¸9ñ Ì÷—eº!"$‚óêe(jµ€Ü Aƒ÷vÙ˽{ó2”aGŬóX'öóª¢oß¾xàŽ'¼÷Þ{Ì›7Ï©¨51é3`'p)pGçTëÚoÑ¢—_~¹cå]»v1vìX§¢C°kyˆˆHžË¥<8»·k*T»-Þ A®½Ö}m —;ém±ÛñH%þ@¸¶×þÜL·% €‡üðÝþ@8Ó_ Eò]v.¦×9ø;°©Šk®¹†† –<öØc|ÿý÷NE_ û~>ݳ¼ZOqQQ7Üpƒë .×®"ìš™² »À”Ï›ŽÃPñn¸¢"ç¯8&Là§Ÿ?fm‰$"Rri¬úxÿbT[K°‹ŠüìǤC‡lÝZ½C¢~ýú,_¾Ü©—ø#àˆ”µ2Çøá°óµÇ&&æ{ìðÀ•ÑÇ·Ñ?7{cWxnWéσIοW€ CAßú$<—HVÈô"X•ìÂödÆò«vþjW]u&Lp<áÖ[oå¯ý«SÑZì–8©RÓ"X[±=¿Õ½G³5Ô–-[èСk×Vë ÆçŸ~ÊÁ[´øÚíªà…Û"Xßc{_ß«åóþ¸+öà9çœÃóÏ?ïx˜1cÜæ 8ï£$YË·À~ØØ'úÛiò?à;à»PÐçut…ˆä±\ Àép)ðpìÁáÇóÀ8žPשž€ó¦”Ä_Š]».ÃÂß#ºoo(èKè ’?nœŽÝV©/à<¦Ý›Ï³BAßux‘¬‘%¸ò¿µÑ†˜¡t0£ IDAT»Ÿ}ö‡v˜ãÖG­ZµbÅŠ4n\í¾Øv†'‘[Þˆ½©—hø¯¬p@ìÁ[o½•;îpM|ÕUW¹]×¾&uŸƒSþ è ,«Ãó6Ç.Ve;§7Þxƒ“N:Éñ„ý÷ߟ/¿üÒ©—x*Z+«ùáC±×ô3€ŽØÐëu=‘mØ0¼ ûí…PЗìiu"’倫ªýG±Ê†”Ÿþ9‡zh¢_¦^ªí¹QHüðu€ãd+–c‡¥? úV$©=ûåk 0ŒÚ…ò(è[šŒ6‰dRàmدêr!jBLï'Øy°3gÎtöàqÇÇÛo¿íxÂ3Ïço¢¯±Êßï„;¦àuD$K¨¸º–Ø¡TUºtçÏŸO·nÝO8è ƒøì³Ï0¦Ú “+RÑÈlæ„ÂØÏÒ«íØÞâ;Ò9GÇüûE,/}CA_]†-ŠdT{€7Ró6G‰jŠCx=æ˜cxÿ}çéœ/¼ðguVìávQ©TôUî^þIxþ#€’Ê—,YÂÑGíxBÛ¶mY¾|9 T ³HÅ(—Šàû±!3ÙŸó3ؽƒ¶k×.:wîÌŠÎ÷RÃá0Çw\ìáu@’û;*ùá–Ø­°®ÃŽÌË„À½ÀßBA_*§FˆH¨¸ºµ@µ.ÝSO=•_üâŽ'|ñżôÒKNE“]{+¦œ?nŠF”Høý8<ôÝœî*BAß{¡ ¯p¶'É«^Øà,"ÞyÙã·6Ÿ¯{¹RÛ \ŽÝ=Ùá·â¹«=ïQGÅi§æxªU«˜6mšSQ3R³G{øÀ5¤æ&Cµ-‘JJJ¸þúj#£æò{и,i­OüpC |3öÿ#2~‰¾vø¯?¾9ºžˆˆä õ;;»0G•.ÝiÓ¦qñÅ;žpÊ)§0þ|§¢ÿnKrû²RtžÎKØM¼z¸$ôÅÝúÃÓ;7kÿè£Ð»°ÕzìüÜÊåÀœH$â)Tûá_DÛÓ!ö }%P_$k¤¹¸%¶g-U«ù· &´íܹ“Ž;ò¿ÿ9Oç\¼x1]»v=\ŽíNöÅÑ`G¥²W±¶¸ÊµkÖ¬YôéÓÇñ„.]º¸íiÿ¶§:™üØ^àTª¶ÐÖÆéС7n¬V¹¸¸˜¯¾úŠ}÷Ý7¶h)vªŒæƒ¦Xt}ŽK€¿ÕþCd‰À©¡ O_œErœz€}̈=xþùçÓ®];Ç,XÀ»ï¾ëT4œÔÜIÏFwá=üF°»þ5…_cL©1ærcÌ"l¨}x*úZW=°_øNúCÙ½%ÆÀjcÌcÌpcÌÞN¯Q!ô½ üXäñ=<à„O ¾H!*Çþÿ7•[ÙU –õêÕãºëܧsÖ°n£ä5ëgR?¤v'ös®¢wïÞz衎'|ôÑGÌ™ã¸aA+b†S'AªÃ/ØÅ«hÚ´)C‡u¬\VVÆèÑ£Š~ܦI, \ŠºþÙ~Á¶íà™h›E$‡)»«v­W¯^C©î¾»Úè+€=±w6óZt.­ûj#U•…‚¾?¹ÝI5ÆøŒ1°Û<Ô6d–`Cò8à[c̛Ƙ3Ý*‡‚¾U@7àqÏ_˜è„‹ãÖ)\eؽ~Si+8_uÕUN«ô0}útV®\éT”Ë_pWÇ0Æpã7ºžàrí*Â^¿rÍ3ØÑ?UŒ9’âbç¦'MšÄ¦M›œŠÜÇÐKùáý°+Ÿ“é¶$à`a´í"’£€Ý-Àö6V1lØ0š4iâP½Æ/S73$-Ý…÷÷xs(è{Ò©ÀÓÊó"¶§àJ’»©ÁÁ›iŒ™îÖ# úv`ç½áñyÇî!-"™Um4IË–-¹üòË+ïÚµ‹1cÆ8•»#w¶b«â’K.aÏ=óìœ9søè£œŠö$÷®]eØ¡«Øo¿ý8÷ÜsOظq#“&Mr*:86©­üð‰À[ÀQIzÊØ‹€éØNŒ`ôçEѲdmqvðVô=ˆHÒàš Än•SÅÈ‘#ݾ4ñÛßþ–»îºË©¨0+©­Ëþ@¸vCy/ }Cœ Œ1'`‡-'2·.6¿ˆ8üÁ·ÞÆÛ°¬•ÀA¡ okr›(’:iœœNm€*]}K—.åàƒ¦¼¼útÎæÍ›óÍ7ß8ÝØÜÃpâчíbn»í6þïÿþÏñ„!C†0yòd§¢eäÞç°öß䦕¾õÖ[ø|>Ç:vìÈÒ¥Kz‰Ç~$ñ—P·E®6³±ëv̾777:×x/àT씩ÞÔí&ûàj­"’{€kV‚ݧ±}åƒ_}õx`¢_¦þCâ[íd½èåvoïQ“…@÷hëÏŒÝ?êfào$Ι³€þ‘Hd{l?>Ûnç1”Uý.ô9ÞýÉFy€K‰ >ýúõã…^pÄÞ…]tñ›d7°ùá¿`W¯uØÝç¹±ß%jÑ–ú@wl>»^mü5ôý±.m‘ôÒèšíÂîM[ÅþûïO¿~ýOX¿~=>ø SÑi$o¨O6¹oáwvÁ+§ ÖìêL„_€3§Œ1õb BAßûxÞü»èþ…"’9[pXÁù¦›Ü—(5jeeŽ[zçò\àU±Ú´i㺓ÁöíÛ¹ï¾ûœŠápC!ŒÁaÞy-¶Æ*F$­U•cŠŒ1 1ÍŒ1-£–ccŠí½áüíù­MøÝŠýް(è»:ô½\×ð vºSô¹®ÆÞä¸+úZ‰úÑ÷&"9B=Àñ5ÇÞù­Ò¥»hÑ"N<ÑyúÇ¥—^ÊÃ?ìTôy´·`ôîégØíˆâ97ô={Ð3‡}—3äiàÂH$Rí›°?žôõðÁPÐçu10‘ŒÊÓ`°­Zxýå/ÉâÅ‹Oxúé§Ý戮ÆÎ+Í5»B~•{ü1]ºtq<¡uëÖ¬X±‚Fª-‚½ø2©³á®8ú0ØÅ˱7/Ê“ôÝä à‚ÊÊËË9è ƒøïÿëx¢E‹8þøjk.nÀŽ‹»]Ÿ›èH§æØ=–DÕnºÆˆ`Cülïç&§é:¹ :_v.‰ {.Ã.‚ùçPÐ÷mJ÷Ãn]9„˜©qìÀŽps^ "YE=Àñ­Çþ\Å 'œPe.Qqq1\p‹/v ¿`{KkÜŠ'Çœ‡·ðv ¿Ç`çe‹ó€)Æù¶ûïðö%x¸¶HÉ8Çí†jÑûÞ¦?d£v/ß*?üpzõêåxš5k˜2eŠSQSê°5”±UêéÜ;³ v»¥–@ëèßÛ{cÚcZcšcj;:¨ÚòÖEEEŒ9Òýç±›a·ØKXô}wŽÄö2¶ÂÞP~ÁÞ¨‡ýlŽ2Æt2Æ4×;ì„«eÏ”èŠÉÏ’Xø} 8<ô K4üc»\Çã }߆‚¾aØÅ-ßJàÔúÀ³ZZ$7({3{gºŠ›nº‰ÒÒR®¿þz–.]ÊOÓ"løÛØÓ³§1f§©*5x‡Õü‡ B‹ÎÓ=Ÿþy¾þúk§¢‘$Ð#më!عØmHΟbìÍ‚ÎÀ¡Æ˜šnÎüÎö‰|^I½!ü‰ýþ<œ ú>w«ýÝ:Õ3Ôsgtg‡w1°7Àvc~0Æ|fŒYhŒyÑ3Ás¾1Æy+J¢¯}J´-^µ^ÐMp‘ì§ìÍWÀs±û÷ïÏ7ß|ÃèÑ£éØ±£—çùx?ÉmË ÜpîF¨jv(è[àpü¼õ»ªW¯p§žz*Çw;w¦yóæÔòÆoewcœîžß†·ùAg×µ"RgÕzKJJjÜËÝ¥¸ˆÜíÞ…Ã Î=zôàˆ#Žp<áóÏ?gæÌ™NE­ðÖkIt^kkì6J¥$ç»F ¶ç´u´ÔësVûZZZʰaÃ+—••1ztµ]”À^¯œÿ¨$ÚÛ½p©CÞ8ÄÓÁãÌ p=ðº?ö²“AÒEÉœŠ÷õO"ÀBAß Pзͩ‚1æcÌØuEæ“°#´`×#©˜¯^Œí9?8ø5vkÅ'Œ13Œ1—cZ¹5&ôm }ƒ€?à°®€‹£€©Ñ÷."YJØ»jÑââb×»È1^Å.·ðT’Û•)§3/ÚÅbcZ×$ú‚ÅÅÅôéÓ‡gžy†åË—³mÛ6¾üòKæÍ›G8féÒ¥¬[·Ž 6ðä“O2pà@š5k–èË€í©øWìÁè0¬j‹¢9èë„™;$"É·;/¯Š+¯¼’=öpÞùäÙgŸeÙ²eNE¹Ü£³Úé`-†ƒl u M±«é¦²ç±¶W¸±‡ž/Õ&üŽ1‚zõœ›8yòd6lØàTäþ¡ƘúØà›®©NÛëxxôswâÞóÂg¦©M•]‚÷‘b›sBAß±ÑÅÂcæ_`·/¬ËgÜ»¦ÇƒÀ*cÌ£Æ×êh›ÎÁej…ƒs°ï]D²”°w‹€põwS€£ÀË©hTŽ|]E9Ö $¸÷^¿~ýX¾|9/½ôýû÷gß}÷¥¨Èù×w=öàüóÏgÚ´i,[¶ŒaÆզWøbcLw‡ãS=œÛ p^!MDÒ©ÚÖfÍš1dˆãVä5õþc¿4ç¢mØE”ª8p {íµ—ã óæÍã½÷Þs*Ú—ï Ñyº­HßÍ‚"ìÜÜÖqæ—£b¶k׎ .¸À¡:üôÓOLœ8Ñ©èÀïT`Œ)ÅÛád«`Œq»ãÛxÉÿ-]7gýpCà/«G€A¡ ¯Ú>eÑÞô¹À£Øý{“ݳZ >3Æ\á6w8Ú¶Axï þKô3‘,¤œ×UR*YüèˆÝ>gI*” þ@¸8ËCÕçcD/Ðîc«×gÔ¨Q<÷Üs´k×.VZÍ›7güøñ,X°€æÍ›'zú­±BAßGØ!ññh´HæmÃañº‘#GR\ìœjèýËå^àj["Õ¯_Ÿk¯½Öõ„¶ª¶Õ›1¦!v¸i&æ›ÖZº ®ðöÚ\EM½àcÆŒa×®j»(C/pt^rg2·•Ø`¸¿1Æíæ²ÁŽÈzÕ;ßùH®€×¡×·º„ß3€÷°sqS­0xÍs¸S…h«}/p±/)Ú>KDêN81ÏË]ʾ»Øá9ÿKW£2àx¼-hQ킆äy\ò­·ÞZãŠ^tÒI¼üòË®C]t3Ætv8îô¾b)‹d‡j½À:uâœsœGfnÚ´É­÷¯>™ xɰ »?rǯ¶åQII äÆot{®*ë#DÃ_s2»PX1Ъ†¼˜{ð˜cŽ¡[·nÕ*Ÿ~úéLœ8‘’Ç<Û{ƒøy{£ÎdÇïF¶'¸¦^èS±C¢OMU#üpK¦?¹x,vØstÈó߀ÙÔbñµ:ú6Æ8ŽâжÕëÂXˆ~"’e€SŒ‰9ö&vûœ±óC½ÎÉeÕ6Itð-à´áfO¯/rê©§òç?ÿÙs£âñù|<õTBS° p…Ãñj=Û:ûáö‰¼˜ˆ¤Ä<®„\¯^=.¾øbÎ8Ãu!÷L oM–j½À­ZµâÒK/ìÐð›o¾™¯¿þšiÓ¦ñ‹_üÂíy½‰]ˆªÙ±Jv¼<;5©ŠŠßƒzõê1xð`–,Y¿ÿýozöt½TcW„®°/Ù5: 80R¾«¦ñ½°=ÁHÑbM·boŠÄóÎÛK݉ ЙúŽZ Ì2ÆüÒ¥|(Þ¶HjŽ÷cI#àÄMÂΧz;èlÏpµ/Xylu^}UæÊD¿,õðú"ýë_]çùÖVÏž=4hP"§\î0¿l!°ÆÃ¹^>'I­½ŸÇ<Çoïå5oÞœ[n¹…¯¿þš©S§rä‘Gº=WCØ 'ˬÃ%Ž5Š•+Wòüƒöí=Ý·k*Ùœìú<Š±Ã¡BÝ·Ø€«èÓ§ÿûßY¶l<òHMÿí+ëŽíümúmŠvn]o¸S1ð7`¦?v] 9Qþ@¸#Þ¶{,Ç®ölŒ9øm²ÚSMWŒ1ÕV°Ž¶y0Ó+\ýLD$‹dr¾J®Úˆþ´>ÃíÈ$/Áî‡cÇbI‰ë”SNáÄS³ŽÔÝwßÍŒ3ظq£—êm±=Þ¯W}eþ@ø- Þªš À"Ùa ½··Ýv_|ñC† ¡´Ôs'^#à§d6.M"Ø¡«,¦pà&2ÍdCô96b{É$³IR±]Ò&‡² pqåÆn¹¥ÚVõnÞþ ]»v¥S§Nì¿ÿþ´oßžM›6±fÍÖ¬YÃÊ•+™={6K–Ôn‘È®íõ=Ví ¼ë„/}Ža'èìtxŒÝç×sveælÑxÕs|$YZ¹ ô}î„Än­T“úØÏdxŠÚ("µ \;…~!æ ”‹oŽçõÎ;ï<ï­IPÛ¶m4h÷ß¿×Sªà¨ï<œ—øª]"’ eØ=¼«LxíÙ³gMC]cmÇßj[+å°¡-‘¡5ì^«°‹ŠUÌ{­õpðcŽ9†#<’Î;Ó¹sgZ·nÍúõëY»v-k×®å‹/¾`æÌ™¬Yãe £RcÌÖH$»ŠÕ{ؽc«Oüufwó+oNÌïS<7æê«¯fèСvØaqëßyç|ýõ×<úè£Üu×]lÞœ²Vû¯ùá›CAŸã2è^DÈôrñÞ T™ß]Eû9²k89ØþñÀieÆÞP‰÷{pž?¾6ôÒHA‘¬¦,µá%Ø9-ÖÖᘣæà%ÅСC ÀNÛ^xYäLX${l&ÁÀ‚ ?[£ç:. œcʰÓ7¼,,´+Zw5Õ‡N7&Á)TM›6eÈ! :”.]ºÄohY .dòäÉL:•HÄëî3€“Ü Ücñ€·a·2 Ÿ;”'ÔûÛ¥K¦OŸÎ¡‡šÈitêÔ‰?þñ <˜k¯½–™3g&t~ê£üðIÀPÐçiˆTŒñ6$|L(苽I~ v1±ZiÒ¤ÉÏ7TößÊÊÊ~îM_½z5K–,q[ÕÛ‹îÆ˜‹#‘È£•†‚¾oýðloMZc?›Øé"’! ÀR^†ö:DO[/4nܘƒ:(±%¨k×®tìØ‘eË–y©î€½ôk´Hö؉í½õ2<³;lz3ù·¾ÃjjÀÛ£uÖàðÞkÓû{ì±Ç2}út:uêäùœââbN>ùdN>ùd†ÊÕW_ͧŸ~šÈËÖ7Æ4ŒD"ÛbŽÏÄÚƒ]Î[ Œ‹>~pª]e¹±×†yä‘,Z´(‘aöÕì·ß~̘1ƒ¡C‡òÐCÕúy<88ÊŸ úíe{ÄŠ­"ÝFË}O*Í›7gàÀ 2„®]»Öü¢ëÖñÒK/1}út^zé¥D_ àncÌÌH$»ÖßaØáÒ59 `‘¬¡E°$!ÑÅ2âÍûÚ úœî¼{ À{ï]ëiU ©Xǃ½1±½¹êÉ=ñæîîb÷<×Mä_øp¦ñlÆnç÷1öý»½÷F$ðÝ¡G,\¸0¡ðëä“O&sÜqžgÑTpJœà‡ãŸbƒÌ~Ø¡­Žá7ÊóV~-[¶ä¹çž«Sø­`ŒaÒ¤I 0 ÎÏÇ@È;í‚P/ÛÿM}±¿—áñûA…‹/¾˜+Vpß}÷Å ¿-Z´à’K.aƌ̙3‡ý÷ß?‘—{ÓèŽØƒÑ÷2ÝÃùÚQ$‹¨Xåeó÷.dz*ûý~üq¯ÕÛPu^³Û{¬ÌóoI‹íØ{íÛ €±½…ùj»·©Yý»×…½:lÚþÕnŒî½ÏÞÜz×x>úfÉøhÿ9þ9®y=_õµ×Sôë¿ooy`·*c_‡œÑîÉKOÛç¯ÆÐº¼œ?mÛ5jÈ=¿²zÃŽŠqÖŽï±B£VÚDÊvzZdê¦[ÿDý¦û°rMò~µþòÏû /ù/«W¯v¯ä¾”W ‰Ñ!ÑÃCA_µ•Ô+ó‡b·ƒŒ§Ê6‚ÑÝ!<¯B0qâD®¸"Ñl¾[=øè£¸ð yñÅ9õrcÌzŸ®Šsîþ@øÐPЗÐ0I `I”—U8ÜnugU>üðé»™½—!€…°'´H®ÙÌî¼mØàWm{ <÷ö†ÞzH¦ÑáÏŽÁïÐãSé-SWÖ¦}®v½™CãwøÕèÁ9ßòàœŸïgž}xvä`Ï7Nyq%¼xGíVr®ÉÞ}ÆÕ~ êÄ ºF‡DVC=/=œ›€¹1ÇŽÃî¬áI ¨Sø­Ð¨Q#žzê) Hn€]ôjlÌñ¹Ø÷o멳±# D$Ã4ZõvYMZûá*_’¢_œ<õˆîµWB#¡j­U«„¶>Œ À^¾{x&-"éµWã²7nøžÄ»eëa˜’Âr8ð¶?¾¨†:^V®œ úbWQï幇Î]wÝåµz\õë×ç‰'žHtxþÐØÑ÷4Ûù©]ÝSDÑw©ÐxÞÿhï¡þÿBA_ìú!ž{D{öì™HÛvæ™g&2ýê—ÑuM~}o^®ùêɺ˜ImxùG>¡ñD¹Äï…½°ˆä}gX¿¼,‹í4"ÊóŠ—Gu”çÕFII }ûöõZ½p¨Ãq/£¾€E²€.fRÿõPçä<êsñWBݬHC[DDR.Úã¥ï âÄËž‚NûayÀ{î¹§÷ÖÔÒ 'œHõ#ŽyÙó+ß§‡‰ä]̤6fÕ OÚ’ ^ö;üw(èÓ>À"’/ô}AÜ,÷P§Ö¸Y³f4húÈ?þøDª;%r`‘¡ š$,ô­z¨ê´RbNóÂM€ÓÎ }Ÿ9†‚¾uþ@xPS÷i=ìœß| €{aßcM¶…‚¾uéhŒˆÔLXj-ô}ê„§—Å©Zºçá}oÉå\ºç==Vý}J""’…Â÷ß±âçyóæqê©§¦ü5_~ùez÷îíµú‘Håž8cL}à/uï¿ÿ~®¾ú꺼œ'Ë–-£S§NÕŽ=ä™›4kwQ^b 0<ôm‰Sï; zCª:{ã<ê¡N¾†‘œ£,uõ'àBj¾óKq½ÆÓÓœÔ0E%¥ªÍ }¯¤¼1"""©µ  ú&y¬¿Œø¸ù€½lû¸,Õo4Xê$ô}ŒÍt;²@„<˜ë,""o)àO üÌñP§·?ö¼ÏP®ˆ¾'/C¼|F"’ À’ w…>¯åÉPÐ÷N¦!""RÏ]CAß’Ïó²ûÁ@÷Ä›”õºcß[<Ú!B$K(KEu¸3ÓíÈ ØÅÀDDDrÑ`d(è;/ômLôäPÐ÷)𥇪^† ç/ïéËèg$"Y@X’e,ðU¦‘!c£û"‹ˆˆäšåÀI¡ oLŸÇKçùþ@¸y_'kDßËùªª÷W$‹(KR„‚¾m@`s¦Û’f ÑÊÏ""’›fÇ„‚¾·’ð\/z¨Óø]^+[üûžâñòÙˆHš(KÒDç ]‚÷sÝ  (è«ÓÖ"""i¶ ¸8;‰{Ó.Öx¨w½?n—¤×̘è{¸ÞCÕ5ØÏFD²„°$U(è{»5R¾Û œ úVgº!""" X œ úþ ú’vÃ:ô•O{¨Ú¸-Y¯›A·aßK¾ŒD"ÛRÚjÉÆÔy”å@Yô±«ÒŸ;#‘ÈÎÌ´VD¤n€E²_àc̯"‘ÈÚŠƒÆ˜ú€è}ø±a7žrcÌrà³èã]à¹H$²9é-Ér6ûQÇ<Ê£HôÏ’h½Hô‘³Œ1%Ø šcGÕ«Åsl6D›#‘HN&"R8€EráÀ“@cÌ…ÀPàDlo¢Š€NÑGïè±Æ˜iÀ„H$ò~Ú+"’•Œ1E@ì Ãz؇Së-±áwgô±Û+š Š1m°¡· ÞÞsME{eÑ}ï7?* [Ñu<Úaod`°X‰D¶d²m"…LX$wœnŒùälÁ«)0nŒy ˜<¡^aÉEÆ˜ÆØÐÛ ŽÏUy¸p)¶wxG¥G¶j—Âç.ZDmŒ1+"‘ÈO)|½¬bŒiœœ´ÇÞ}}°ŸÓ9;°x0˜ ,Ð%‘ÔSI‚’’öÙg:tèÀ¾ûîKûöí‰D"¬[·îçǪU«øôÓO©ãñT„ßXÇE·cúE"‘wÒðš""©Ô"…Ï]„ Ö ±ø'l(.T€ƒ1k€•‘H$WzÈbŒiôÎÁަ*Mð)êm£#‘ÀVcÌ|àeà•H$òyÒ,"?S–‚µm[Ýn²vèÐþýûÓ¿N<ñDŠ‹ãïX´zõj^yå^yå^~ùe~üñÇ:µ!ÅÚ¯c.D"Ofº1""9 >ÐbõêÕ[€­™nL†µšc¾©¼~Ež8øZÌŽ£6L÷0Ƽ\‰DVþÿöî;>ª*ýøçLfÒ I„DŠ—ö)‹ ÐuE)®Ëî*tAŠmñ'Š‹ EªÝUY]Ä RWÔ¥)  R†RHŸ”9¿?f$äÜ™{§~ޯ׼BäÜ{ŸÄ¹Ï=ç<Î×! hlƒD«¸¸Ø©ã†ŽmÛ¶áèÑ£X¸p!¬”ü@«V­ð·¿ý ï¾û.²²²0eʘL^ýÏ0 ÀBˆ'UB‰ˆèÂD^^^Ä7ß|Ó"???Ð'Ì: !Úy:µþÉocFØ'„¸ß±Ÿ˜ˆtàÕwÞDÞ$11ï¿ÿ>V¯^~ýúÁÕ|0** /¼ð¶mÛ†îÝ»ë¥afXáØCGDDÍ(++3ïØ±£Å‘#G\ÝsìZ !Z{: `>€ï…ý< ‘?ô'“DJ°cÇ´k§ÿCì>}ú`ýúõèÙ³'rssu?¿ŽFÈ…½X)ÈÌÌŒ ‘mÚ´i´@VMMÈË˳äååYÊË˃jkk…Íf`³Ù„Ífµµµ ±…‡‡×FDDÔFFFÖDEEÕõçõÉBˆ*)ecE'a´¹À!ÄãRÊg< ‘/cL¤`úôé†$¿uZ·nÇ“&M2ì:¹G±TJ¹µ±¿B¤è  7ìaêÚ‹ˆ¶ø/€õT%”ˆמ={¢‚ƒƒÏÄÇÇ×ÔÔÔˆ‚‚s^^ž¥¨¨ÈRRR¢´”¶¦¦ÆTVVfÎËË«ÿo§Nìí˜¬Ž—··jïH~“À{Pg˜<-„È—R¾~¡BˆK`¯J üœWì{Ô÷ØàgG¤”\¼ù) 5ücÇŽÅÔ©SQ[ëÕô€ !zÃ^Uõ2ØÞ¾úÀ^ôDÅTUBˆoaoý°ZJ¹×€x‰ˆ<Îf³‰ŒŒŒ¨:”geeEX­V=g?-ŽWìIp%€WOj2™`³éžHÑû¤èe!DŽ”òÓºÿ „èà ÇkìÕ¥µ¨Bìð€÷¬eBLþŠ 0‘—†Ùlv:¾è¢‹põÕW㪫®Brr2BCC‚TWWcÿþýÈÌÌDff&222°sçNgCM°ÀïàÚ¶`W:^s„¿˜ à¾é‘¿©¨¨ÊÌÌŒ2ð¿µcªP…¾Ä¡¡¡¸æšk0hÐ ´k×®þ•””(**ªoç—““ƒM›6aÍš5ÈÌÌ4ðK¡fxOÑÀØ*'¹xÎP=¯¿8*„x Àb)å ÏMäU˜y‰Õ«WÃjµj:&44Ó¦MøqãбcÇ ŽMMM=ëóýû÷ã_ÿú–.]Š‚Í*zk=@A{/¸E1^Jù³× "?¡Çìdpp0®¼òJ 0à¬ä¯mÛ¶¨­­Eaa! PPP€Í›7ã7ÞÀéÓ§QUÕl^éøÝm†}ék¹ãuö_šÍ¸ùæ›1räH >M·°mÙ²%Z¶ümqÏ 7Ü€ àèÑ£X¶lž~úi”””¸ôuSÂì†ýÿ³:x ÀL!Ä^”Rn0èZDnŘÈKÔÔh[±6bÄ,X° ÙÄ·)]»vÅüùó1kÖ,<þøã˜3gŽSç1Àåv !¦6·Ç‰ˆ×?þèÔq!!!¸é¦›pã7âÚk¯EttÓùCddd}ý‡ÈÈHlÝj/P\\ŒcÇŽ!;;RznÛ­ÂC€pØïõJàØúè#—Îç ôx°!„@zz::vìxÖC•°°°ú™ôÂÂBäççcë֭صk—+?CF%¿ ¸À Bˆ…’Rzõ^-¢æ0&òAÓ¦MÃìÙ³u9Whh(žþy\z饸óÎ;QYY©Ëy] àU!D¥”òmOCDÞÇ™í"×\s ^yåtîÜÙ¥kGGG#55ÉÉÉØ»w¯Ó}åÝ$@ €â—_~Ù6qâD—Ûø5”œœŒ+V`Ñ¢E¸ë®»t;¯'œ>}Ú©ãÌf3† ‚‘#GâÆoD›6m”ÍÍÍÅW_}…•+WbÅŠ} ¢à>©Bˆ?I)‹< ‘³X‚žÈÝ~ûíºŸsìØ±xàt?¯ €·„c<ù6‹Å‚ÿüç?X»v­ËÉoC111èß¿¿¡]tbþãÿØbܸq&=“߆î¼óNŒ9Òs»‹3Ëêo»í6;v ë֭Ä 4%¿ÐªU+üíoÃòåËñí·ß¢{÷îšcp³¡¶ !ôû‡DäfL€‰¨Þå—_îéÎà]GuK""§Ìš5 cÇŽ5äÜB¤¦¦^p­7èܹ³iÏž=‘*ckjjDÃWee¥©îUSSÓdýÐCé°—ëÚµ+¾þúk¼ýöÛHLÔZp¹q—_~9víÚ…GyD—ó¨+€íBˆÁž„È\MDõÚ·oïé `€ž„ˆ|ÓÀ =¿)))Ø»×»»¹;v,¸mÛ¶õ›]ëú™‹ŠŠ,eeeææÚ4EEEUÇÇÇW'$$TÅÇÇ×°èß¿?ÂÂÂPQQaä—áqqqqX»v­!³fÍ©S§°xñbÝϯ£°W¢î&¥,õt0DZ0&¢z^šöâWJ)7z:"¢Æ´jÕJ—822ÁÁÁ0™L BPPjjjPVV†òòr—÷ˆ8p 2((¨4'''¸¸¸ØRQQ¤õ%%%–’’Ë‘#GÂCBBl-Z´¨JHH¨ÊÊʪò÷äþøÇ?þ~ùä“ObéÒ¥N·Ft“dOxðÜ¿B´Ð@_Ç+ö­M&ÇǺ?KÛ| `#“ir&ÀDT/&&ÑÑѺt1›Í°X,0™L0™LB ¼¼\sµëžÐÏåÀˆˆ ——çÔq¡¡¡hÕªâãã‹ÅÒäX)%ÊÊÊPRR‚ãÇ;UÌÊjµš~üñGݪ[­VÓ©S§BO:ºy󿥰÷!&$''£{÷îøé§Ÿt9ŸÉdBHHBBB`2™œiØ”)BˆˆpìÉnØ“cU¿p€*!Ä7VXÍvˆd&ÀDt–víÚižÅ0›ÍèÞ½;úõë‡~ýú¡oß¾èÑ£‚‚ΞX(..ƺuë°zõj|ùå—8yò¤–ËôBt”Rfi ŽˆÈ ÊËÏk·{A&“ ]t:vìxÞïʦ!‰ÈÈÈúbK:uBVV–.}‘]eµZͰ/-PGë¥Æ!¼½â±GÕÖÖ"77שcãââ0dÈ\qŸòÊ+Ñ¥K˜Ígßò!33™™™Ø·o>ÿüsdff:s93€¯a¯4îª`W9^s…ÛŒ—Rêó€È 0¥}ûöÊ ðW\qãÆaäÈ‘ˆŒl¾¶Jtt4F…Q£F¡ªª 3fÌÀóÏ?¯%¼k¼ªå""oÓ¢E ¤§§#<<Ü¥ó„……¡sçÎHHHÀ®]»`µZuŠÐeuý‡›\Nd6›Q]]í¾ˆ|Lvv6rrr4Y³fá®»î‚Étá:·-Z´Àe—]†Ë.» 0{öl|óÍ7xã7ðá‡jýYÒ#ùmL?ß !žðŒ”ÒõFÍD`h":‡ê¾&“É„µk×âÖ[oUJ~όٳgã¶ÛnÓrصš/DDäE„èÝ»·ËÉoC-Z´@ÿþýªÛ9u ª©¿4ªS ºçž{pàÀÜ}÷ÝÍ&¿M4hÞyçlß¾)))úè< €Ç|#„hòç‰H &ÀDtÕØf³áøñã._oîܹZè+„ü½ED>KqÁ}¾Î EëÖ­u?¯‹BhBJšÜ{ï½xýõ׫ËùÒÓÓñý÷ߣÿþºœO'ý¬BxÕSòM¼‘$¢³´k×Ny쯿þêòõZ¶l‰¡C‡ªÐÁå‹ù¡°°0O‡Ð˜PØgñÈ }úôÑýœñññ˜;w®îçuÑï|(„àϹ„ 0EKk‡ììl]®©!€nº\”ˆÈÏxi Ø÷“éÕ«—§ChÌõðtäÛ˜ÑY<‘§¥¥i~‰.%"ò3^¶¸! ì{‚ɇ„††"!!ÁÓa4fº¢¥§ƒ ßÅ*ÐDt–¶mÛÂd2)µÔÐc 4\r‰¦œ–3ÀDDÐ# CLL ¢¢¢ê‹)Õ¬*))ÁéÓ§­6€U|}L»víœîqm hÿ0ÙÓobLDg±X,HLLTêÑ«× pll,qêÔ)•áL€‰ˆŒ   ÔÖÖ*‰Ö­[#&&111n~¢öÌ™3ÈÉÉÁÑ£Gµô6þòÐó ‹IYûöí±sçN—Îa±Xн{wÄÆÆÂd2A“É„’’ìØ±555ΜöïBˆYRÊæoVˆÎÁ˜ˆÎÓ¾}{¥X¯`èÖ­`""…††¢¬¬¬Ùq!!!èÕ«bb´·p­K–[µj…;vhI‚-¼¦Y15O˶¨:)))0`úõë‡~ýúáÒK/mru™3g°nÝ:|ñÅxï½÷PU¥¼HÀ {kÄÅš¤€Ç=ÀDtÕ7<½f€MË „qº]˜ˆÈ¨Â2™LN%¿ ÅÆÆâ¢‹.Òr÷û-!n¹å|ûí·ÈÊÊ»ヒ)S¦à²Ë.»àÒü˜˜Œ=K–,ÁöíÛѲ¥¦­½×kLT‡ 0G5.((PšiPÑ­›¦‰]Î5Bupee%¤”._/%%¥~¯°¶¯ñ1Zf€'Mš„8}­ž={âƒ>ÐrÈ5Bˆ§/H‹ 0GË_½fY‹ˆÈuª °”ÒÙbVg1›Íˆ‹S^”c \¾(¹–û=¶E]uÕU¸æškT‡GèáòE)à0&—!¢=éÏ­8LDä:-½€+++u¹fll¬–áAº\”ÜBËý€^uAn¼ñF-Ãy?@š±5KÑÀ•:hßàc{aBˆÓvžó:,õX[Eá‰7¼víÚ!""BuI5ßðˆˆ¡¥’^ pDD„–áœ|ñ!mÚ´ÙlVªÔ¬×ý€†`€÷ä&ÀÔ$!Ä•î0~bÛÀPÇ«Î!ÄZ³¤”»‹’Œà‰`!ºvíªÚnAÓzi"¢@¡%®¨¨Ðåš`3ØØg˜L&$''ãèѣ͎Õ+îÔ©ÂÂÂT>y?@šñ)G‘.„Ø`€‘pn¹R €1v !Vè¯cˆd°„„å›(U‚î(„`5Q"¢sxb tDD„PÞÚË%Ð>ÆÝ!L&ºté¢:œ 0iÆ˜ê »©¶¸TÇSÿÀÇ:žÜ@µð…Þ½€ÐÔ{ƒˆ(˜L&«=Ô+6™LZo&À>Æ÷ˆ_,„àŠVÒ„ 0„ŸX€%åIù ¯¨¨H·kj,„•¤Û…‰ˆüˆ–VHz‰ŒŒTÊØÇ¨Î¢´´T—kjH€ƒÁâ¤`‚°¯[Zû^_"@TT”Ò8½öÚöHÖíÂDD~Du6VÏßßö ðþÓ§x¢0¦Æâ\MšpÉÀ<ñtîf±X<‚WS½Òs!9YSNË`"Ò¤¸¸XdddX>œ““cÉÏÏ.--5ÇÇÇWuîܹbÀ€)))µžŽÓUª3ÀUUU°Ùl0™\ÏG(„E>Bk/àÔÔT—¯©a°'ÀŸ¹|Q üà„p¿§ãð„ÄÄDtéÒ999(..öt8^Ç3‰‰‰0™L°Ùl*Ã9L¤£HQPƒ—ÉñªP  ÚO»×EÌš5«%€ˆêêêFï{<ˆmÛ¶áÝwßEhhhulllEbbbÅ€Н¿þzý~ɹ‰ÖVHááá._SÃh€Ë }Š':CtéÒEËý[!‘&L€˜"ÀsžŽÃSâââ°ÿ~ö€œœœ:uªÑ­[·öp´îç‰`³ÙŒÄÄDœUmÜÇ  µBÍÈËË‹Z¼xqTtt´ÏÌÙ xÆŒ©Ÿ~úéïÏœ9“PRRÒª¼¼¼UUUUô9êp°Áë~ëÿËØ‡ÄÆÆ"22R©À•Þ!`Γ&L€”¢-€iZ F‡œœŒäädãÇDFFªªôék¯¡— È3À€}ð?ü 4T× ù!D€°WFuùt°'Òá°Ïû‚hØìê~S\\¬>­êaz÷þõ×_ƒ§L™rÅ×_ýÇ¢¢¢‹N  ‹ãU§Àç°·EÔ¯}¹E»ví°oß¾fÇéÝ é‹/¾P-„H’RžÐíâäט®¿Â>; Äd2ᡇ´iÓÞßWWW###‹-¢E‹\JŠütZ³FމÌÌÌ .Å>uêN:å–xTo ¤”°Z­ ѧ{VR’òÄn!„IJ©´AˆÈß !ÂÄÀž¸êͪFèc¾~Ÿ !„Òû©Õj½àß?öØcÝ,XðòòrW÷…¸À¾´Ùll¹èCÚ·oï‘XËpL€I àÀõ'Õ!!!øì³Ï0lذ&ÇX,üîw¿ÃË/¿Œ3f`Á‚xå•Wt낃ƒÑµkWtíÚõ‚㤔ªE!\¢uA¯XC%h3€îy"@äÅ5b<‡…ƒÉo=!‚ƒƒ›Mn ¶¶ñ­º………æaÆýåûï¿ÿ“”R϶E¡FÖÔÔèxJ2šj!¬cÇŽévM­.°A·‹“_c¶$„èàRÕñ‹/¾`ò{®ÄÄD<÷Üsضm:tèàLˆtB¿}JK¬ç>`­¸ šž"üHAÞÓÄÄ4ý¿]õ}¢±ª………æÔÔÔ'vìØñg“_òQª«³ªªªP]­Ïމ.]º4?è7¼ eœL£U<cÇŽuê"©©©Ø²e þð‡?`×®]Nƒ` K ûÞNÝ.Näc„Ø«œN:Õý1z?“˜˜ˆÇ{ EEE(((@QQ QXXˆ ;;»ÙúçÎ×ÔÔˆÞ½{?”““ÓÛÈØÉ·h©^QQ‹Åõ]-[¶DHHˆÒJ0& ò ƒÐWuàôéÓ]ºP›6m°iÓ&tíÚÕm{WI:u´i×I«©©Annn}B¬Ú^¥!× IDATD…Æ`V‚¦€å(غì÷šk®ÁÝwßùõ×_GY­VX­VTUU¡îÏzÍBy3‹Å‚4úw7ß|3Μ9ƒòòr5™Àœ;Ü»wﻳ²²†è«/GII‰§ÃðJZ{KGGŸ[Ü9IIIÈÊÊRªË) 0L©*ƒZ¶l‰«®ºÊå‹ÅÄÄà™gžÁ]wÝåò¹È;˜Íf$%%i­U¢ñœ|ã@Ž~ŽŽ………I;vlôïm6[}BìjŸ[_T·:<<ü‚_ÃxÊ”)½~úé§› ÎKÝyç8pàÀYE'áAŠ #[k]Hrr²jÌ`R°oœJ {¡€f >f³>?"·ß~;^yå.…¦fÅÅÅ!((¨ÉÂ,çä}À„}úW—>·¾ê_ÿúWLIII“wå&“ ¡¡¡gÝ¸ÇÆÆâºë®C§N€„„˜ÍfDDœß°gÏžèÙ³' ¸¸ü1V®\‰òòråóÇÄÄ 11V«¡¡¡ZäB5®›3fÌŠ‹‹Y¹²!âââ§ë=‘/òTaL «Ââ…ÁRÊ oz'à@¤\ROcõ½fýþ÷¿Ç¨Q£ðÑGéz^ò?‘‘‘ª °r/k"?£~7 `âĉ˜7o^£³8]ºtÁ-·ÜøùçŸñì³Ïâý÷ß÷hòÖœ¼¼}:–,Y‚œœCÎO¾3ÀD¢:ð¹çžsº˜app0Þ}÷]$&&:•D©¨¨HénÜd2éÒÍ`È!ؼy3¶lÙâò¹Ü¥K—.èܹsýìs—b×}Î]Ò{w«V­ð‡?üÁ¥ !0{öl¬Zµ pé\z©©©Aii©ÒC€ž={궬.yþùç5U¤ÀÀ`¢&)¿oë§¥¥aüøñºžÓZÚÑ~høðá¸è¢‹t?¯'•––¶Q›ššªëµï¿ÿ~CÚ-’ÿa]ÒàÀS¢:ðàÁƒFÆèС|ðAïC¾Eà pˆcO$È¥ºO=õ”Ç—›L&c—'5C»ï¾Û“!èfãÆqRJ¥u=÷Sö6MsæÌÑõœäŸ¸*ŒôÆ8ð(—ñtG <òˆÇûÛ‘w±X,Z†³˜Q#ŒØÆÒ²eK̘1C÷ój¡eبZ={öÄå—_nȹݩººZù›©±’Ñ£GcРAºŸ—ü g€IoL€Œ”2€ÒÚfw%À˜5k–[®E¾¡ªªJu¨”RZŒ…ÈW•üMž<]º(Ö–*ÄFÖ²?~¼Ö‡u^GË÷Ò¨Ÿ§… º\Yšüg€Ioü˜~Qä®n½õVôéÓÇm×#說‡ r¦Lä'”3:£’?‹Å‚yóærnž^]§uëÖ¸ñÆ=†K‚‚‚”¿—F%À½zõÂm·ÝfȹÉ?p˜ôÆ80)-ƒ>|ø°á• ë!ð /¸åZäý¬VåI]Îþ5ÁÈßß×_=®¹æÃÎ!Z–@ýöç?ÿÙã{¢]¡åa‚‘­gÍš¥%É¡£aØ"„ðíeäL€“Ò pee%Ž;ft,õ.¿ürÜrË-n»y/&ÀD®3:ùóT[$-Ëekkk Œ øqã ½†‘¼%nݺ5yäÃÎO¾MãÃ>I¡f1L^W«ÎìÙ³ i[A¾EÃh&ÀDM0:ùëÞ½;î¹çC¯Ñ˜àà`åL,77×ÈPC‡E§N ¿Ž¼éaÂ<À¶HÔ( 3À—A“&ÀIivïÞmdçiß¾=ÆŽëÖk’÷á 0Q“”gì~ýõW#ã<ù䓈5ü: uìØQyïÿñãÇ €}I¶'èÁ[f€ 44Ï?ÿ¼¡× ߤq˜ 05‹ p`Ržþú믌£QÜD`Á¢@£œ…8 ü«Þiñññøç?ÿiøuêÝ»·ò¿û'NJ½´´4 0À-×Ò“ÅbQN€ 0fÌ 8Ððëoá 0é p’RPÚÜûÍ7߸­Q.&jRê@w$À0qâDtíÚÕ-×€èèh¦”»c¸Î]wÝåsm‘ú÷ïÆd2)ýL¹ëçiáÂ…š ‘ÿã 0é pàÚ¬2¨  {öì1:¢³p 4Q“¼.¶X,˜?¾[®U'22R)v× 0`/ä4räH·]Oááᶈˆˆ“*cùEy÷”Kz÷îͶHt>!½1\Êk›7oVÊ•‰tبqÒ¾$Gi´»`¸îºë0|øp·]/&&Féß~~~¾–%.ûÓŸþäö=Ñ®ŠŽŽVzJàΟ'¶E¢†4ܼ/ L€—rVë‰}ÀØ***T‡òŽ‘W-Y­3þ|˜Íf·\«eË–ÊÿöÝ9 ì‹m‘âãã•Ö‰!//ÏèpmÚ´a[$ªÇ˜ôÆ8@I)3œRË`r·“'•Vä€ñUYˆ¼Rœ­õÆÑ%—\r &L˜à–k%''+Ârg?{¸æškpñÅ»õš®HJJR~Bà®eЀ½-R‡Üv=ò^Wq0¦f1lJ™mnn.öíÛgt,D€šš- °{ïl‰¼ƒRCV›Íæö^î3gÎD\\œá×éܹ³òM®»ß¿„n- æªÎ;+'Àî\UÀ¶HT‡3À¤7&ÀË Éëœ32¢zÙÙÙZ†s˜‘rì¯[X"##ett´R±€Ã‡£¬¬Ìè|Öˆ#rU[!ýôÓOF‡sžÛo¿Ýí×$ïÂ%Ф7&À-@ÊÀ 6 ¨¨Èàpˆ˜)¨ 4ûéÏ«w’““•²Z)%öîÝkt8>ËÑ IiøÛo¿…ͦT„\7&oUg€Ioü­Àí4”««9 Ln¡1æh 8ö_ÝP*õ믿âèÑ£Æä!©©©ÊÓºF†âóZ·n©2îÌ™3ؽ{·Ñá… 0é 0}¢:pÅŠFÆA@S,¸¯¿ ‘wQ®‚ì¯Ë ¯½öÚ2Õ}ÀžXºëKzô衼Qú¿ÿý¯‘OÃh)¥¬62òL€és(ÞH­[·ÅÅŇCNCœË7: `Ê °¿.ƒNJJªŽŽVº3>xð Ö}„e̘1Ê °¿þ<‘÷Ò0ÌÙ_RÂ8ÀI)ÏX§2ÖjµbåÊ•GDNCÏNîÿ¥@VÅ}Àþ: III¥*ãjkkÙÎïþüç?ç„……婌ݼy³Û÷S`ÓPÄŽ 0)aL ¼¶™Ë Éhf€¹ÿ—––}ÀÐÒ[Û§hÙ¼sçN#CñyIIIJ³ÀEEE\RNnuü¸òÛ½ÒC"&ÀŸPZJºfÍ”–*=p'Ò¬  999ªÃ9LNyôúõëŒÃc† ¦œûí·F†âóRSS•³Zî&wÒð`œ÷¤„ 0AJY`ƒÊØÊÊJ.ƒ&ÃìØ±CËðFÅAä#”`]½Ó®]»ZÕ~À9998p€¿6šrã7*ïÞ°Aé–H¶Fqe)aLu>T¸dÉ#ã ¦1Ö4˜ÈUCqðÚµkQRRbp8žÑ¶m[åeIœnÚwÜq<$$¤Peìºuëüö牼‹ÕjE^žòÊfΓ&ÀTç35*ׯ_Ç¢íÛ·«­°ËÀPˆ¼ž£—»Ò,pee%V­ZepDž1xðà3ªc™_X›6m”–A[­V¿ýy"ï¢aö`LŠ˜@J™`£âX¼õÖ[GDHà ð>)e¹‘±ùåÞ>þº zĈå¡¡¡Ju,Nœ8Á¸––¦ü`Ñ_žÈ»hL€¹š”0¦†>R¸dÉÔÔ(M)ÉÎÎÖRë{#c!ò!Ê ðêÕ«µ´ñ))))ʳÀþÜÊU=ôÐ!„R£5kÖ ¼œÏ!ÉX `œ&EL€©¡(Ýåää°éJÃòg€ 0@JiƒbïËŠŠ |ùå—Gä,RËeÐM38"Ï0`€r¼~ýzØl¼WnÌðáà Z¶lù³ÊØÒÒR|õÕWF‡DJJ‰~øAu8÷ÿ’2&ÀÔåYàÅ‹£¶¶ÖÈX(h,¦ÂåÏDçSžÖ}óÍ7ŒÃcFŽY¬T1??_ë¶‹€Ò³gOåÒË—/72 `‡FQ‘òöþ½FÆBþ… 05æ]JÙÈñãÇñÉ'Ÿù;ûÈ4UË" Ê«"6mÚ„ƒ‹G˜ÍfMË YÀ©i&LøNuìÇŒÂÂB#áõý÷šžwó‰)cLç‘Rž üHwöìÙFCþ®ªª «V­R.ð®•èZ–Aûs/÷+®¸B9ûþûŸod8>kÔ¨Q¹±±±¿¨Œ­¬¬Ä»ï¾ktH€˜“Q˜SS”—Aÿý÷ظq£‘±Û°aΜQž´Ù*¥Lyô’%KP]­T4Ù§ÜtÓMåQQQJ³á6›ûW/ --M¹4»B4ìÿµPjßE0¦&H)·@Ã/Γ³>þøc-ÃÙsƒ¨iP¬Þ›››ë·Å°ÒÓÓ•§u¿úê+H©ô- 8>øà&!„R¥°ŒŒ lݺÕè(€H)±sçNÕụ”þ÷D Ø.äeÕk׮Ů]»ŒŒ…üPmm­Ö›p&ÀDMöLNyØ_gín»í¶"“ɤ”¸åææj™e (7ÜpÃ餤$åš þZ\<ãàÁƒZV‡qù3i˜.d)€SªƒŸþyC!ôÍ7ß //OuøN)åÃ!òÊåÔׯ_¬¬,#cñˆvíÚÕ¶k×NùÎyÍš5F†ãÓ®»îºÕªc—-[†ââb#áÂý¿d$&ÀÔ$)¥À‹ªã?üðC>|ØÀˆÈ߬X±BËpÎþ5ñ Pi) ?úúê« TÇnݺ•Űš°pá¡¡¡§UÆ–••á½÷Þ3:$ Û·kjøÀ˜4aLÍy@©ÊÀÚÚZÌ›7ÏàpÈ_áßÿþ·–C˜©Qžö×bX£G.‹ŒŒT*†U[[Ëv~M·õìÙS¹R—A“^>ÿüsÕ¡%ö ù!&ÀtARÊ"ÊïhK–,Ann®‘¿xå•WPRR¢:|¯”’opDj”‹aù¤‘±«©©ÑºJ`­”r¯Qñù#-Űÿ­â?zôhå'mEEEX¿~½‘áø¬… n QZR^SSÃY`rÚ/¿ü‚½{•ßòs|k`8䧘“)eå'À‹-¡C‡ Œˆ|Õûï¿£Gj9ä9£b!òsÊ›Z¿ùælÙ²ÅÈX<âꫯ®LLLT.6°bÅ ¿\îªèèèÚôôtåbX‹-bemrŠÆåÏŸpÿ/9ƒ 0i1Ku`MM üq#c!$¥ÄìÙ³µ²MJ¹É¨xˆü\€ZÕÁþ: <|øpåÊŒ'NœÀwß}gd8>kæÌ™«L&SÊØòòr¼òÊ+F‡D~HcÌú ä&À¤LJù-4ÌðÁسg‘¯Y¶l™–¥Mg‰œæ˜ÉTžþì³Ï°¿ÿ[¿å–[ÊZ´h¡¼'zùr–hÌu×]—ñÅoPÿÒK/¡¢B¹~ýõWìØ¡ÜÒ·“S˜“VB±½†Ífã>jp8ä+ pß}÷i9d€Ï ‡(P”PZ"(¥Äœ9s Ç3 ¤¼øÀصk—‘áø¬ûï¿ÿ#(Þœ>}‹/68"ò'[²}&¥TZ‘@t.&À¤‰”r€ªãW®\é—ûÊH»ûî»§NÒrÈó’›ñˆ\âø7¤<ûùÎ;ïàøñãFäwß}÷™ððð*ÕñK—.52Ÿõ÷¿ÿý×¶mÛnU?þ|ÔÖ*¯Â§fµZñâ‹/j9„ËŸÉiL€É3 a_Ùc=f`(ä V¯^wÞyGË!ÙþcP8D¦ гvUUU~YÅßl6£OŸ>ʳÀû÷ïçÃÛ&Œ7îCÕ±‡ÆŠÊÏÌ)€ýûßÿÆÉ“'U‡—Xg`8ä瘓fRÊý”oܸŸ}Æ•¬ª¤¤÷ÜsÖÃæ9Ú¸‘‹UR•7c.^¼00"Ϙ8qbApp°ò’É¥K—²"t#žz꩟cbb”‹9<ùä“°ÙX¨—šf³Ù´¶G\%¥´ù?&Àä¬'(ÿò¹ÿþûQYYi`8ä­~øadggk9$À[…C¨JUúkÿ˜˜Ù£GÓªã=Š ”k>ŒS§N¡ÿþʳÀ?ÿü3Þ}÷]#C"÷É'Ÿà—_~ÑrÈ¿Š…`rŠ”òW¯«ŽÏÊÊÂsϱ o Ù¼y3^]ùǤÎ$)¥råZ"jž”²€òSÈeË–áÇ40"Ϙ0aB¾ÙlVÞÂóŸÿü55¬³ÓPVV&Mš´-<<üWÕcfΜ‰êj.ê¡Æil¸[J¹Æ¨X(00&WÌ‚†Y…Ù³g#++ËÀpÈ›äääàÖ[oÕº„ð#)% [Cù÷µ”Ò/«ø·mÛ¶öÒK/Uî œ““ƒÕ«•»ÿù½‚‚!((H^zé¥Ê³ÀYYYxóÍ7 |ÔÆµ´>MÙ2Qc˜“Ó¤”¹ªŽ¯¬¬ÄÔ©S Œˆ¼EII ®»î:=zTËa…&QÀsì«WÞ üÅ_à»ï¾30"ϸï¾ûòCBB”§#ßÿ}X­Ün(¥DfffýçS¦LÙd±X Tê©§P^®\œ„ÆÙßÔ¼5… 0¹j6åž+W®Ä—_~i`8äiÕÕÕ5j”3}4RæÕSž€éÓ§‡ÇÄÇÇÛúöí«< \XXÈBްÏâ×SÓ³gO寭999x饗 ‰|ÓÆ±víZ-‡Ìslç r `r‰”²À?´3eÊ>M÷SRJÜyçX·Nsw‚µRÊ· ‰ˆRÖ@C_àÍ›7cÍÿÛnwÿý÷„……)÷^¾|9ŠŠŠŒ É«•••áàÁƒçý÷‡~xMhh¨rïšÙ³gãÌ™3ºÆF¾©¼¼ãÇ×rH.€%…C† 0¹LJù>€¯UÇ:t<ð€‘§LŸ>]k¿_À>#u·áQãJ¡Ø¦M›†ÚZÿšt —<¥:¾¬¬ ‹/62$¯%¥Äž={meYÓ§Oå_ú………˜3gŽ®ñ‘oš1c>¬å¥”Ê[8ˆ.„ 0ée2å;¤W_}K–ðAž?yùå—­ô=]J©i³09ϱ„PyxÏž=xå•W ŒÈ3¦L™R©\{ýúõØ·oŸ‘!y¥£G^pö{Ú´i›ÂÃèžoáÂ…8~\yçù¡íÛ·cáBå2€ý¡Ý«…Cˆ 0éBJ¹€¦;¤ &h­üG^ê©§žÂ”)Sœ9t#4þÜ‘.4Í?þøãÈÍUÞ6ëÌf3®ºê*åY`)%^}õU­•í}Zyy98pÁ1‹E0@¹/kYY|ðA—c#ßTUU…;Ñð/)e¡Q1QàaLzzö=J¬V+FŽÉ'Á>¬¢¢·Ür üqgn 3Œ”Rjz$"×9þÝ)Ä:sæ þßÿûFäÿûßÏÄÄÄ(φãøñãÍîû=WXXXm×®]+î¼ó΄ÊßÇ™3g"'‡íßý]ii)þð‡? ++KËaeî2($ pL€IwRÊýþ VÇaÆŒ¸úê«ýöÆÂ—:u wÜqþüç?£¢Â©NÀ­RJå–YDd,ÇÞ}M,}ôQœ<é_+»wï^ݧOM±žyæäçç–Û>}ÚŸO^rÉ%¥f³YΙ3gJJŠòr®ââbL›6MóõÈwÔÔÔ`ôèÑÎlu{TJ©)c&RŘ !¥\ àŸÎ»aÃôìÙ“'OFa!‹þyZuu5æÍ›‡.]º¸ÚºêA)år½â""}H)«(?ÕÊÏÏÇÈ‘#QUUe`Tî7mÚ´ÓVÕñ………xæ™güfkqq1vïÞ­¹ abbbe«V­êW,^¼ø5³Ù¬üæýÎ;ï`óæÍš®I¾ã®»îÂW_}¥õ°ÿàúx2 `2ÒÓ>qæÀÚÚZ¼üòËèܹ3^{í5¥*”¤¿5kÖ -- =ô«E_ž•R.Ð+."Ò] 4¬ÚÙºu+&Nœh`8î.o¼ñFMm öíÛ‡W_õýö¤¥¥¥øá‡4'ó!!!¶´´´²†ÿmÈ!%ýû÷YËyÆŽ‹S§”'àÉG<öØcXºt©Öìîd‡2`2Œc?ð­~vöùùù˜8q"ÒÒÒðú믣´T¹^ ¹ààÁƒ¸á†píµ×bÿþý®œª Àx)åtB#"H)kaO‚•-Z´È/’¿†þú׿–&''kªN¿zõj,^¼ØgûçååaëÖ­°Z•'¿&“I¦§§›Íæó¾ð 6|¥¼—éøñã¸ùæ›ýf6€W_}Ï<óŒ3‡>!¥Tn©Eä &Àd()e)€¡vºrž}ûöa„ HNNÆ”)S™ÉßFؽ{7î¹çtïÞ+W®tõty®–R¾¥ChDd¼2š2ûî»ï¼óŽAáxÆÄ‰O˜L&M³O~ø!ž~úiTVV–!²²²°sçN§Ï´´´’øøøF´X,òöÛoŸ m¶6oÞŒ‡zHsä]¤”xì±Çpï½÷:søÌÑ7"¢ó _}bI¾Eà#õ:ç•W^‰{ï½#FŒ@PP^§ 8•••X¶l^ýu={|þà)åQ½NHúBÌlði¦”òOÅBÞA Nëq>ú(žzê)! ˆÊýþùÏ&lß¾½Öã:uê„™3g¢eË–F„¥›Í†ŒŒ g«ù£[·n%)))ÍN·oßþ±ìì최ûÅ_ÄäɓЋ<«´´ûÛßðé§Ÿ:sx€RJM+0ˆœÁ˜ÜFØïŒžð¨žçMHHÀ°aÃ0lØ0 :­ZµÒóô~ë—_~Áë¯¿Ž¥K—¢  @ÏS à¯RʲfG’G1¦Æ!ZÓzܘ1c°téR„…i>ÔëØl6Œ?¾Ã‰'b´‡‡~éééF„æ2«ÕŠ]»v¡¨¨È©ãSRRÊ»uëV®2vÕªU-nºé¦kjj¢µ\còäÉX°`nû£Gâ†nÀO?ýäÌáÇ\&¥<¦sXDbLn'„ø €EB 87zõê…aÆaøðáèß¿?Ìf³Þ—ñY'OžÄúõë±téRlܸш=kOx\ò‹O`Lq<¬l @ó/Ͼ}ûâí·ßFjjªþ¹Ù©S§L“&Mº¸´´4Ô™ãŒ»îº z‡æ´ÂÂBüøãN/ÕNLL¬üÝï~§©GÿþýÇlݺõa­×>|8–-[†èhM¹3yÀ·ß~‹‘#G"//Ï™ÃÏ$¥Ü£sXDMbL!„¸ À‡Úyèèh\uÕU4hÒÒÒžžP3ÄEEEøïÿ‹7bÆ øùg§ë‘5§öbWËŒºé 05Ea'j…˜ÍfLš4 3gÎDLŒæ T¯òí·ß†Ìž=ûâšš§¦"CBBpóÍ7côèÑÖ;|¸Ñ×ñãÇa³¹­uÞN“¥”ÿs×I_L€©9BˆHQ®œ£OŸ>˜?> ¤STî÷ÄO´Üºuk’«ç‰ÅðáÃ1|øpÃW#åææ"33ååJÛu›UÝ»wï’ÐÐP—Þ\ž}öÙ3fÌøwmmm„3ÇwèÐsçÎÅèÑ£] ƒ\tæÌ<ñÄx饗\m[õ°”òy½â"Ò‚ 0y !ÄÕèîéX#„@llìY‰qBBBBB\ÿ²X,Í~n±XPUU…ÊÊÊú—Õj=ëó†¯'NàСCÈÊÊò†^ȧa/döÕû6&À¤ÂÙ¢XçúÝï~‡‰'bìØ±×!2÷zýõ×cV®\ÙÎf³¹<-„@ß¾}qýõ×£OŸ>ºVÏ.))Áþýûqúôi—ÏåÌžß 5jÔ ?þx>§¿à+®¸/¾ø"zôè¡WX¤@J‰%K–à‘GAnn®«§{ZJ9C¸ˆœÁ˜¼ŠcßÙØ¬D‡Cg«ðìE® = ¹Ž 0©p$gQ"õ8_LL n»í6L˜0ݺuÓã”nóÅ_„½ùæ›)V«U·å@­ZµÂµ×^‹Ë/¿íÛ·wê6› 999ÈÎÎFa¡>¿ž;wî\Ú©S'ݧ¥¥Ý™‘‘1Á•s˜L& 6 &LÀu×]ÇjÑÛ¾};&OžŒíÛ·»zªJwrÙ3y`òJB ì=ƒÿ`ˆg£!ë<È*þ… 0i!„fïÎuÅW`üøñ6lââœÞnìV{öì±<ýôÓ)ÅÅźoHMHH@ï޽ѧO\z饈ˆ¸ðjáòòrdggãøñãÎ!:Ùl¶¥§§—´jÕªZ—6¢OŸ>cøá‡ûàD‘µsµoßãÇÇ]wÝ…Ö­[ë@mm-V®\‰×^{ ëÖ­Ó£kÄ 7J)wè‘K˜“×B\à·háápI!€x]J™éé`HL€I+!D0€Xè¸4d2™Ð»wo :C‡Eÿþý½¢æBSòóóMÓ¦MkçLŸ`UAAAèÖ­úôéƒÞ½{ãâ‹/†RJäææ";;[—eÎ EDDÔôêÕ«8""Âðí-cÆŒðÉ'Ÿ<[[[«Ëzx‹Å‚#F`„ 2dˆO^ó¤“'Oâ­·ÞÂo¼cÇtkË»öä×ù2äD:bL>Ã1ûp ìK¤ûz8¶ö¥Îˤ”ž†ŒÃ˜œ!„‚½:´a匣¢¢0dÈú„¸K—.F]Êi6› Ï?ÿ|Üÿþ÷¿ÖÕÕÕ†–v®¨¨@ii)"##¤¤$tìØ;vÔ­On\\\U¯^½Jœ­ô쌙3g^ôÜsÏ-´Z­.k¨eË–6l†Ž¡C‡TûCgmÚ´ ¯½ö>ýôSTWë:ùÿ>€;¤”º/§'r`òIBˆn†9^C C–Wà=¯I)wy:r&Àä,aßÀ©Š¾ZµmÛ={öDZZÒÒÒУGtëÖÍ£½uëdggÍ;7ñÀñRJ]–‡WUU¡  ùùùÈÏÏ¿`%çØØØúd¸cÇŽhÛ¶-Z´hØØX¥ïÉd’)))å]ºtñÈÏ?ü0öŽ;î˜SZZz©çB W¯^õÕ·û÷ïÏ=ìV+¶lÙ‚M›6aùòåÈÌÔ}¡—ð˜”r–Þ'&r`òyBˆPƒ ‡=!öÊ*Ò^¨À+¼+¥,öp<äfL€ÉUŽÂ…Ñpc/÷:f³]»vE=êãN:!11ñññºVVV±~ýúÐ¥K—&>}Z¹X˜ÍfCee%ÊËËQ^^޲²2 ¤¤D—˜ÂÃÃ[Ÿ7|µhÑ_|qe¿~ýÊãââlu ,‹K½Š¥”õ] ***ê»”––"++ Ä¡C‡ê?ž8qRJ €ÉþÀÐì´E‹8pàYTºtéâÖþÌžP]]mÛ¶aÓ¦MØ´i¶lÙ‚ÊJÃ&esŒ—R®2êD®`L~GÑöDx8€Úx6"¯Q ûòæM6ØÂ%I 0éÅñ 2'/ª,K}»ºÆ^mÚ´AëÖ­X,Ýf_}õÕ˜uëÖµ)//¶Ùl¨­­=+É­¨¨¨ÿXYY©Gq!gÔ(u|<¢¾e_ÃÄøÜ$ùÜö}°Z­®Äu€‡ôså$Z…„„à’K.Azzz}RœžžŽ6m|÷ö¡°°™™™øïÿ‹M›6á»ï¾s¹'´‚j/xRJ©ÏS"0&¿'„ˆæx¥;>ö€›–îyP-€]°'»›|#¥,ólHäM˜“žË¢#`o—äÞéW˜L&å~î&“ V«µÉþíV«Õ$¥Œ ÔÓ_[6Ø·¼¸”¥ºÁ•î ëÞ`­bccë–Ô}lìå‰ÕÕÕ8zô(>Œ¬¬,>|ø¬WQQ‘[ãð€©RÊýî¾0‘VL€) 9nÔ:â·„8Íñy+­x~c™ºjGjðúÀ·RÊ3Œ‹¼`2‚£HV¸ãÅR¼ö‡q°w1ðäƒ å°ïÍôÁ°w,±×¢nÅAbb"BCCÏš1oîÕplSÎûoyyy8vìjkk=ý¥ÀaH)?ót Dª˜5B {"œèøØº‘Ïa¿© †175U°ïÓ­„ýi}~Kp6øs¶”Ò+ÞÉ·0 fÄBaO„½:q“ºêÙñpßCV ûûGì«‚|Qkwþµ‰?GÞ£À³æH)½}EÑY˜éÀ1ãa¹À+øœÏÍ8;Á=÷e•RÞ‡‘`rG±¬pØ+ösVؾL<Öñш†Ç5øíá©¿ÜèE¸À(<K «€½kÄRÊlOCä &ÀDDŠ 0¹›cûI(쉰Q«g|M(ì-¥êÚJ9û=©›í­DÅ­üH£aoƒèß国Ç>ÿ°TJéö ÆDzâ/ """r iê^ Â‘ þ¬5{OR·ò'öÙñHü–«,•®Åo³½²rh€õ°?H àVØëx¾ª|à5)åמ†H/úfCDDDäH†­ŽWÝV’¯@œ¶(v¼{û,qÝË {ñú—¿ÏööïK)€Ç«\þ¶„1ÀL!DWØÛƒ}f8Ìqú‹#°Ïö.–Ræz8"Ý1&"""só+Pî( UW;ÁÜàc %ÅU°? ¨v|¬‚}Æ7¬‘—Wô`ÖIÝ×[—ð–Êföì9Úïìð‚"À`Ø“áẮ_Ø{ÛÄUÖ² ù3î&" PÜL¾Ä‘×l˜ûÛÃ| û¬n]Â[Õ\òB ì3Äç\<÷soÐpö¿ªÁŸ­0 ¤¢-€¡°Ï §èV”>`ìIï&)eއã!roùEHDDDÔ$GX‹sÚù8ö›aOŽƒ`ßG[÷jø¹7Í×}5çþÙÙäOJY·$ºI ¾W “ãºïMÝëÜÏÏ}5ü{ìÉlÃúoõ3Ù‘R°Øñª«JÞ@€ >^ÿ­R~ ŽdÀF)å¯އÈc˜‘ÏrÌŽª& â ü–;óQ6x¡™ÏëÁZØ“\,¿Sý^ù;)e ìK~÷X^÷ß…áRaOˆ»ÃÞƒ¸á+Þõ å\g`ŸÙmø:à[ý† 0ù=Gò£ÈIRÊrß;^çq,3o…óãDÇÇhØgÕƒñÛösÿ|îkx/^׿YåUà0$»RÊ|—¿ D€ 0Q3ËÌ;^ºp¬L°À¾2 ¶¹ñDä:&ÀDDDDDàX™Påé8ˆ‰¿nô'"""""": `"""""" L€‰ˆˆˆˆˆ( 0&"""""¢€À˜ˆˆˆˆˆˆ`"""""" L€‰ˆˆˆˆˆ( 0&"""""¢€À˜ˆˆˆˆˆˆ`"""""" L€‰ˆˆˆˆˆ( 0&"""""¢€À˜ˆˆˆˆˆˆ`"""""" ÿ¨ùy=—N×ùIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/progress.png0100664 0001751 0001751 00000000731 12440135707 027300 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞbKGDÿÿÿ ½§“ pHYs  šœtIMEÝ&2ÕL‡itEXtCommentCreated with GIMPWAIDATxÚåÛ» Ã0 EÑk¥ö^ û/â5’RŒÀN$G’½ŠsvÔÜ)Ÿx`gf`¹ò0)㯅/ _ $>7@X|N€Ðø_Âã¿ÀŸÁÂÃïHânÀS0akºâuRÆIoe†á-Š`8~døQÌàG0…ïÀ¾g“ø^Ìâ{0oÀ<¾eøVÜà[p…¯À¾f—øZÜâkpÿ7€{ü?B௃¿ ¾4@8|I€øÜañ9Bãÿ@@@ ÿ@¿ ‰ßÈâá}%&‹椌–¤ŒßV@ß#€ù Ô¤ŒoÀÍíqRÆ·àîê<)ãkpûß )ãkpÿÓ$)ã^yÓ·Ùö`IEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/circle-arrows.png0100664 0001751 0001751 00000001570 12645510255 030214 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@`¹UbKGDÿ‡Ì¿ pHYs × ×B(›xtIMEß $+ç– IDAThÞí˜MHTQÇÏ3µ4%'KRÉ µÓÊj‘jÙ*!\’‹`ÂW®B("W1P‹Ú¶j[àDAh„’ág¥Ž–_˜Î¯Eƒ:3÷ïŽ3O‚ùoæsÏÿw>¯H 1ÄÃÃW‘x9(éâ$™Œ?m"@œ”Jµ”ÈqIö{0,Ä-ÏŒ‘(º‹}Üe”PXá uÄEÃø05ôQióWùŽ^’)ãÛyB8˜âr$Ì;è$\,sc“Y€CÜRhòpFúÅ# ’&™’/ÛL¤š{›q¾úí»pr˜uäÙI9™TÈz©_«¤éxª8ÐM™©| ·$9#"Â>qQ/òñ›F6úh™¼Ò ß÷2笛Og<à˜QŠ-i´á Ðýãû=e@N¦×1Lކ÷œ&yqL/2p± À/Š4¸CI O?NÐ 4jëå0¡ ^:V¢Õ5Ià&3Jì¶aÄ €AÓÚ˜lË”CÝJóÞ¨4j“T¬¡?ˆÀ‚­Óñ40äGÀcûÈI"××M£[2÷’B‹/'­)´’…®Ez­ ÷±„‹Ìˆ“È¥ÕšàWæh×ìÞc:¶4“´d?_ê Ñ@¼½‚ H?5v™ &þ¡› {$›6’A ì ®4>K;4»Aex²•;Îísš€÷œÖ'§ô@‡æ)ÅÌúv#zª…&àÔ8ãß?“ëÊ%«{]¬4aø¡áJ}ËuŠ•;sÃÐkò9=ªu>Á%>S%"ÂYáØBŠ©n9oï±^ ¤­^ê•Ei •¤ú¯£4óQ;¯HÜÌz~KÌ–k¯|‘q™’dI—#’j"Õ#ç™Í¦k,…}Añš]‘¨Uʽc< í| ª•;4ƸéQK¯EãsÜÊ Fµ¸Y i|ˆ;z¥Wÿª6[j¤LNÊ~¿¿ç¤Gºä¹¼3¼v]Vï•,qˆCæÅ#òÍX‘bˆ!†ÿ?†É—Á!¥½IEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/logo-64.png0100664 0001751 0001751 00000011732 12612226251 026622 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYsttÞfxtEXtSoftwarewww.inkscape.org›î<WIDATxœí[y”TÕ™ÿÝ·¿Z{•^€î¦h$‚&Š "£39Ñx$3=‰'3Ç89ñ8ã4`–‰Ž˜ÉÄ]”C 0ÚjZ@Ö–n¶†¶é½«ºªkyû½óÇ«.ªº«7TH2ó;çªwß}ßû¾ï~÷ûî÷Ý÷c ÿ—Á]h.4þ_š ªBH!„ÿ˜iÇÕÿ|:ABÈtQ¾ÍÜf[4OTxË2¨ Ê|!hHÆÍ?€bcìè9ÐVÊËŠWU•úÚ;BšiZÛΜéùWÆXïˆ÷BjT¯°<·à²Å•òìëÊÅ‚ ^€Œ1ôu&ÑÝÒæƒ=Úñ½]–ž°© ñ!˜ 0p\‹Û{)¥v0ÆôÁϨ¬,y|ͪ•ß^píÁçS±ã{éC?ÓÙÓyýÃÖî0Æ:?Q¤Lù3²*ü½ ñÓVêØ(r,'_9²à¶éêÜ*8Ž'£ÒJD  ¶åÀ¶(b!Ý-1»åH(Ùq"’44ûkŒ±mÏž²àš9ïl}mUI}ý\uÕÌ4­ú÷ްûñoÚOî|ýÌ™ž»cÚÇ®^âïá9ò`ñ$¿3óÓ¥y…e>âÍ“á Êð%Rîiž° $E²£«ÅEÛ±>l~ü@,Ùo¾¦'¬o0Æ’UU¥ï¼¶é߯®ª*ué&4PÊà÷{Ò÷mý}½ùýy¢µ­½w™eÙ»Ú?’!ùŠWx!¿Ô7ÿÖï_æ*›QPF!qˆ÷÷1Ä,U¾¢q=—: ;~sD;ôFkØÔœg¾º|ñ7¶þ»ùŽCAeÙH&uäçû¸ éî‰"à÷àëßüIWCég;»Â÷1ÆìsV!¤RV…º+ošR|Í—§Ê„$m=F ý–Ž o¢šîÏ%)8Í](žÓó„°ªp ž³ÖÔq2Šºßœt^ü¯ñS§NDss;xGÅä Y÷jº‰[º0}ú$À3ÏíH<øÐ¿ßÞZxNaò IŽ|þ›³'^{Û4™×€û- QSclÈè‹! R§9*í¶dztÇÊV@Ÿ ±;»­°Ü‹Â ?uêD@UUi–ð[~÷.@U¤´ððËyþñ—OœXüĘ@ñBV{ür² Ìûäí?¼B­½¶<›IŽG<‚y¬bf¹ БŸÓoéhOF2Yízµ £RÉj;úNî¸}qúüøñ3Y×oþÂÕCè¿·»M-X²ø •Rº`äI €RÈóüÈñK¾<…»þŽL½lBξ…’E²/ç5ªp Êèúˆ* jAäF_ÞÛ‡›p LÓ†$ ˜6ÍåwÿÜÆ€O_] 8uºU•%€“Íí¸ôScùŠU½Ñhbå°>€"B¾+ÂC’Â+7ÞU‹iW” Ë G:wBÈ<ê:Õ½AºG¾å±û–µ½ò©ß=ÞÖÖóÜCBY¢ªjsUUÕCO›¢,¾sæˆÂ@M€G‘ìú2Œ&Za0¸×Á´a¬ñoâŸÄk[ßM ßÞñð°ò®ÇúžÚ°uK{{ïý@Ž\@Åû%IÚ²zõê‰O?ý´Õ{1ó3eéë&uУÇ`³ìÉÜÔï d0ˆ]&Ô’9ÈŒ 1‹¡!âÀÎÐC·K+i0¦x”{\–«/-Æ’ïMÇÏ~ûÞª;èé‰ ¹§îíCèé‰àÔ©|fÁwmÛß»·õL÷WXÊôÓS€¢ªªúŒßï_ºmÛ6eΜ9()+Æ‚œ‚W—¦ &'cݨðf 4â0ƒf…«±‚2†Ó‰^Tz‹À‘lÓð S:’ ]úYåÇû ¼ût vný)¢Q$G^žë‹(eøÏ [±ùµ}ˆÇM£±±±CÓ´ëc'”B8UU_Ÿ5kÖÒC‡)sçÎÅÜys!óK³ñðª}åO†/Žàû8‚K:îB0ÅWœ%|‘L ðe8ÑO6³-Ï—/còAÜtëÐÙ^ÀŸë?Àg?V­}«W?†Ý»wËË–-›èñx¶B<@ÊA¸¿¸¸øþ¦¦&9 âW¿úîù§ï`åºe Ê×Q†a u(fçñ8ÖïÀ pÃ`†B„>BØ‚^[¡>‘Àp¬Áá“™ëçžÖö>߆7¶­GGg÷þð—™}¸ò– 4èÁé:ûß?Û¶1þ|½¡¡a“¦iË€KAØýæ›o W_íÆM¯ÏƒOß:WÞT“Ú ŒAá]GSéã'œŠSDÍlD- ÝZ?Ê=ùðRº]á LÊ žÒÀ˜(g ÂtL!rÄfð|@b¶/K™û·Ý& #Ü‹+oŒ U÷”ᱯíÀ©ãb„ hkkÃŒ3ŒX,v Çóüí·Ür ¾··¶caVÊñujQ4Ç{@S¾¢Ks‡>WR55èÔBÜ6²Úu‡¹#XEƒÂÁ˜„gŒ!i›ˆZÂTƒY"apuÉõeð×7¿6-<Ž`B…›7o”——cùòå¼(Š7q禥K—¦¹Z·n|y2|ùî(É~TzÏ&+šÃÐu6\…ˆ½„°»D-Vü˜,ÅEŠ?§f™œ%,e 3 '3¢0@mJBˆØCu:Ñ‹–x Û€5a¨x‘CÍU¹ÃõÄéxáÅÒçŸûÜçI’sñx|ú¼yóÎ2išpì³ ùùmRßY†ŒÌùÈ.5T^„0Ž GZ“athѳrRë"TåR}\9 J” |¢ŒÉ;æg 5A246gÎ$‰ÉÆqgGeåÊ•Ðâ&Lݘïw ôÙàã¹ñV±èšã°¨3ìµ€¨¦ý àF»@•9É—äóð¤VK²S|ÅYþ%#­©Z>èÅŠ+Òç”RpG9BH–'«®®/ðh|×­ Ùùô*fɹ¥±šc¡[ïOŸD.ƒÓ oáÙü2Èêé„©×pCŸ6†å²ÂÌÌã!çp'}IÄÂ:n¿ýöt›eYà8ÎáA05-«J„»V~¯ÿº±°&8ALʦ6hI„²„ó ù’· ò"Ê=ùéó¤Í†Í@H–gÓŸï·¡œÔÒ¾Bw>ˆ8ÙÓ®ó|yÍ,¹a)xþìô4M„‡SåTSSSÖMk×®EA^1^ùÉ>XFnóÈýŒdªD%¨ðqˆÃcæÒwðÔ`<1IÎjsÅáÈ™!õB>æ€K ?½`×o› G(¶lÙ’Õ~àÀ(Šr’Ó4mËÏþsmðû÷íG´ËÄS÷Ö!R„EìÅdoA–ÇoŽQ;èÖ‡ú‹\«Á^#Ž¢íhM„‡€'\ÎÒš' Yë…UœÛP‡aóú8¸³ õÞ3äúöíÛÍD"ñPÀq\Occ#7mÚ´¬Nº®czÍt„"]øâ÷.Eåì‘kwÄb 6K{py ÚÇCà€C}Ù£55´$Bð.ö_”E‡3)¯k² ,íÁÇ’FGº“øŸu …šOœF Ò§¨¨H…B_àcaUU;Žº¡( NŸ:Å oÄÆ5ïãÙ½‡p{bH?ÀM‚ÔãIHíÙ‹ Ÿ@ ñ¹kABž¤"_òdµKm¤3géd†¯€HPáͽp24;~}O~ï-TÏ@OW(§ð…B €ýBšŽ;V>¤§{ ¯¼ò zzz°ðú…xêÞ:L¿¢—/­DÙÔ¼t?Æ0BÀÄf`©ÐÕg2DLgHÞ@lQà1Ù[8T eØD#j1ô[Ù–ÔÒqà­Ø½µ%Jq¤¡ ÕÕÕ¹ Ø¿?¼^og< H$455-0l‰¬¸¸‡¾}û°bÅ <÷ãÝ=._Z‰ÚkÊáË—¡W«`R¶Ì5òÊ L$® ¹@0bPg,ÃÁÉýݨßr =Æ0yR¶lÚŠE‹ c ûöí€=@J`ÆØÑÇA˜;w.<ÇqðÈ#`Ýã?Å[/…¤Š˜2§Ï+FÅÌBxóäai˜å2˜<¾RmQ´ëCó^ÛÓ‰¾®$A?–å«X³f Dqìë”úúz=‘H¼¤ÒaBȵ……hlîP. ŒÁC)ÅÆñ‹_ü‡ ™t+BÁbÅ“ü(™@A©’*@VÈ’"@Rp<‘´aê6LÍ‘´`ê¢=ºN÷£»¥}]IX†¯OÅ´‹gàί߉+VŒKèh&EUE¹ÖÕÙyclç€Jt¼u¸³ª QàÕFEKK vìØ;wâpÃaô„:aY&ÇãPP‡º&pAàÁó<ADa~1fÔÌÄu×]‡n¸555™'ͤxÿX®™= +`Œõ1ÆdYîúå?°½Íý¬/a±¿5DÛw:ÆV­ßÀ¼^ïi–’;O!›Þøý«p¢KÉ. ú2Ì_t‹âD—†ã]ÊðÊsôD"ñÄÀõÌ¢èÂ`0oëë{OÉ\jÍÌ‚‹"Jó$ðÜ9Ö¯/l‡¡#b¢»ßLGÔöÖ|ñ³s(¥tc¬È.‹¿ašFòàž·Ó ”1tFM4œI¢'fý¼RÇt¥xîʶoz–©ªú§á 0ƨmÛÏîØòRvÖÀr(ZzuisáŒg è<ÁrRƒÕš@kØ€M³§/¥/?³ÁH$¿ÌlÏÚ#„\áñxêÞ:tFb#ìí>E~>™K×ãÏ7(cèׄb6"IkÄ*õûoïÀ=ßX3 £1–ä,)c»½^oû›¿{¦âº¿[Aè0&Ï„âBq ÿüó¸ãŽ; ˲¾ `/€(c,g)k8À˜7䫪ª>_[[{É®]»†(a”1ÄuŠHÒF$iô?ÆiÂ\ózäyxGX{lÚ´ ·Ýv›eYÖro¥€.ÆØ¦†S€  £Iñx<ÏÕÔÔ|jãÆrUUÕ¨ø -,,ÔEépË(ü–Ç£ d D/ç8îQÇÓå÷û»ï¾ÛÞ»wï'"´ã8l÷îÝlùòå¦(ж×ëÝàN“ÆÀg Rþnð1RìGp•¢(_qçÆ¼¼ú†W£€t€4¤¤)  Méi HH»u˜Ñúý>UÕsçÚZUKã¼Wãþ/= Ø:Ì8âÐ3üý+ €t€4¤¤=:Ìx7ï1øsü¼¤¤)  Méi HHS@:ÀhO{ÆÜÞ.IEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/protocol-icons/guac-monitor.png0100664 0001751 0001751 00000001263 12440135707 033013 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<0IDATxœí›AK•A†Ÿs½¡"D&\ ‚ •Ð"£ð-ܹlí/h§{ÿƒÂEˆ­ƒp¡ Áe›¨ .tq‘š÷´˜«Inœ?ßk3Ìò ïy¿ïœo>fÆÜ3žø¿ùì¹û/,=À =`ÙÝ1à9ðhåØz é #ÀðX,F3|:b1*ºF¨‰b)­î/Q P PS P PÓNŒû l'ר%‡;ÀK`:%8v¹´Ýa„‡¹›Oì:à˜u÷ç›ÆÌ«ÆÄö€ƒaM` í &¦ø&X P PS P PS P PS P PS P PS P PS P PS P PS P PS P PS P PS¼±;C? ;C½†ôdaf÷ ;CãW‰}ÆU3Œkœ¦U"’‡ø7àŒïÀ[†kwx˜Š ¬‡¤ÔÔTÔÔTÔÔ´€®Z„n ØU«²kÀ3`‡òÊ¡¼8;b¶D(õMŽ›]`Éݱ w†ÚÀʸ3´ïî¿¿”Jiu‰Ô³Â瘙ªßã¾»gÍq6÷°M8>«lh×@+)Ääº›_k7iÀÊ$üïøŒÄæ’Úçãšdx”jÀ—ĸ&9%\äˆ"Õ€M ¯û^?oÜý(:*ã+ðŠPwêÚwàÐIÉ#k%hfSÀ<0™°n~ÚÞ<1 Ò8¼¿¾y†Ú(A6æ­h|ÇM­Û qß¡«ÂOe²I 8]âœõ·ÜLômrÀ!`ÓÀú¬¡è%‡Æî ¥À*ð3ï0럟ï(ðØŠ|¸Þs]@DÀ ͶÏÔ.9BvOˆõ(WWW¶\jq#ÀC ôSã7°Ç1J)ãår™0 ÙÝÝåòòrFKÏ#,8¥”R©TdooO’$1.v&E0Ípm[ˆõõuÙßߟ™éëðGॎRÓ`ss“R©d4G´Z-ªÕ*kkkœ”€_yÏ¿f=¿èÑl6'Ê€tøœ§ OdŸà°ØP죔2à;㯳ˆív›Á` ýLZa¶AEœŸŸkÛžß0YÀù 8::Ò¶õ0ò» D…Fc™I;Á¨ ðµÐˆ,#"t:’$Ѳwv¾ÄqÌéé©–í8îTXÈ ýN0N€/ ¯ÅNc’€o O†N2I'Èz'xÈü~:ÏED899A$ÿ?ÌÀù:$ qçÚ-¬€ÖèÛ¶€ó˜ðèÍ 0Ï¥”±N×1*‚àxè²° Ëm‡ rl\áÃî?Üöy\€7À‹iDd‰kà=‹÷x<Çãñxî0S§“Ä«ý¢ÅIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/folder-closed.png0100664 0001751 0001751 00000000747 12612226251 030161 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<dIDATxœíÚ1NƒpÅñ¯MãfMÔô&‡Ö 08z‚²x¯à ºéèœÜ꤃gaR›Ö¡'ø7PxXß'aú~ïÿ 0´`ffffö/í©è7ÀiÍ묀gà®v¢–¥¬Ãoãø,Ôo$~}Óé”4M+_$Ë2ò<ïêK=«ù|¾ªc8þ<…Bíg7±Džû<ïߙΣ("ŽãŠ£7*à 8i<ÅÚ pźôƒ$I  pÜx‚_ ð <L&“V†öëm&k€ñxÜÊÀξ!g³£Ñˆ^/tŠ-‹à9¡;\úýC ÷Y½ÞáÔÔ\€:€š PPsêj.@@ͨ¨¹u5  æÔÔ\€:€š PPsêj.@@ͨ¨¹€Àú.ü<¾,[ ýAâ¸Ü^–Ö-{u3333³úItŃÜç/IEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/checkmark.png0100664 0001751 0001751 00000001071 12612226251 027356 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<¶IDATxœíÙ1(„aÇñ/…ä¦SJÇÊŒ”U™•A&ìJI‰RÊ®ŒJ)“Á¤”\Ù”UÉp%¥¸ár^ÃëºË½ÏsGïóü¹çÿ©·Þ~¿ß{]÷¾J)¥”RJ)¥”úc2Àñ×k¶ÈàÀñAŽpHµ|p#Œeêf„<Éå#àÈÊEsosùX’‹æ^'p‡¹ü-Ð!–΃ìWZ.š{Yà sù ¹h~ìb._Æä¢¹7 ”0°/ÍSÌå‹À \4÷&±ñmÊEs¯¸Æ\þèKçÁ"ö«¿ ͽ ðˆ¹ü ñ'ä_jæ¼g ûÕŸrеyâë†ó9àsùé TË$p€¹| ò’Ô‘ êKÕŽ0‚ù^?v|†M[ðBr±Êç†ó•{ý^¿‘Óµ‡ý‹í¨Áùeÿ‘Ó“Þ±´÷@—÷Ô)kt?o;fò:ñ›®ˆ;´ŒŸŒðŒËÄtk渔 èC3Ÿ„ ±tžØv´ôÕ¯•4Bè— åÛ÷Zþ9_’ÊEâ?A‚t¬J‡PJ)¥”RJ)¥ö DUÏ]IEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/magnifier.png0100664 0001751 0001751 00000002042 12612226251 027366 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<ŸIDATxœíÚYoEÀñŸï+¶9r*‰‘ñý¿/" â0'qbâÄØñÉCí†Ùž‰Ý³†ùKûÐ=»µµÕÕUÕÕKGGGGGGGGGÇÿ’‰ úži\Å V1×›ŸÂ.^÷^O±sA:¡}Ìãn‰{^âWHµo°• «‘lLáK±×KŽñb½ï_Ä2®a¦ás‡ø›Éº"×øBóÊ?ûú$wžÄu±mf‹g‡øJ ò¬é,ÜÅíbnñ›Xñ“8™`W°Py6)²Ç“ÞûÒÈ2À<>7ðöñµóïß#<[c©2?×{örx5ë ¡K>2hÌc±òÛCÊ;Â÷ø«˜¿-Og’„M‰½[e]»Q8ÄwÅÜ,nŽ(w€ \U_ýŸäÂ+õè_{$2 ð^1ÞÄ›¹}ãe‰Ù+ÃKÅ8;_—AoJd‰2 °XŒ‡ |ïbOÝ£Ê:ah2 Pºãiù~ŽŠqZ&ÈÔšr'0V1à°Ï'Ȭ2Ù 3턘a€WÅx5Af•+ê+^HC“a€²Ôý0InŸÅxGbœÉPôE1άÖfÔ ð4I6r °­žûïÉIU÷Eƒ¥Êz‚Ü·d¹êZ1žÅ£E뛢—Xå™äž@ÖqxG”¨Õ¢h¡7·áügøê<í±2ëŒDfCdS¬ZUæ¢8,m;Ûù`Ÿàcuïy¤…¶X¦ŽDJ¼fpkÍ W^ž°g°xšì=»‹O5÷×ðK¢®oi£+¼*zƒeðª²ïŸT6çäX´!š¢­é}ÞˆKUïÎS"ÅÍ8}ŽðGšv Š´Á¾Pú@¸ô(ÙfVõu‚^5Ú2@Ÿ-ü.²Ä”p÷¦?åí¦è/”ïY-^#sQ—£Õï[ÙaBüð±ºý˜ð@s%¹&2A*m{@ûbµû¯]ƒ+»%²F¹mVD!´Ÿ©Ì¿a€Óè:ïóâXœzGõÄuõ»ÂEá!iåð¸àXDþ¦ø²z§xhÆÕD|X1xGHxŤ+ó‹èßÂ#Í©{š¯ÒÏÍ8{ñgÕÏ“¢Ôùo4ãîð“æÔwKÂɸ{Q#‰^c•~Qõdá—Áˆ¨ßtã´<ªàËb€cñ±*øaTÁ—a ôÙoI¸ýC-þ{¬£££££££££ã?ÎßE²§Uh"ýIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/mouse/dot.gif0100664 0001751 0001751 00000000110 12440135707 027322 0ustar00mjumpermjumper0000000 0000000 GIF89a €ÿÿÿ!ù , DŽ©Ëí£œ´Ú‹³Þ¼û†âH–扦êʶî Çf;guacamole-client-0.9.9/guacamole/src/main/webapp/images/mouse/blank.gif0100664 0001751 0001751 00000000107 12440135707 027631 0ustar00mjumpermjumper0000000 0000000 GIF89a €ÿÿÿÿÿÿ!ù , Œ©Ëí£œ´Ú‹³Þ¼û†âH–扦êÊ¶î ›;guacamole-client-0.9.9/guacamole/src/main/webapp/images/mouse/blank.cur0100664 0001751 0001751 00000000506 12440135707 027660 0ustar00mjumpermjumper0000000 0000000  0( @€ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿguacamole-client-0.9.9/guacamole/src/main/webapp/images/guac-tricolor.png0100664 0001751 0001751 00000035052 12612226251 030206 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR\r¨fbKGDÿÿÿ ½§“ pHYs  CMÝõtIMEÞ vÞ­$ IDATxÚíy|TåÕø¿w¶ìûNHH {0²Ë"ʦ‚bë+Z¥*tÁ¥ö­í[kUôUª?«E©õ­BŤàȦ e•%@@–B²}›ÉÌÜß÷&3“Ìrgò|?Ÿó™ˆ³ÜûÜsÎsží ¯¡ €¾Ý«HR€ ^•Ö¿c@UTiýÀ 4«¯­7€RUÊT)  °Öv¯vñˆ|I4æ‰hgÐñ@o ª| ç¥k+WA!p8œnç,Ê€ñ…8N p0Èü^í#÷Pù½ˆáð °[uá*aÀTU&Q€I Ïu~rvuhaª€-ÀzUê„ Ð]h{§³Û€¾Ý¼MŽï«Õè u.B €_ ãÔ^>]4Ë%ÉW£ƒíÀaàˆêÂøÙÀMªÑ@™¥8N pTuk€C¢IZ'ø­ª¬õ(Ëb².‰UmËCjÛ†5h)‚ Q{ûåÂX=&ËÕ6Q¬x‹ÁÀX”‰¼q¢9¼Âv” Ä@®h'¸xeã‹èµ!gÕgr­PO»˜ ì„ÁiVÔg4U¨«À×…qùœTŸ]°Pc³D?Uǖ˜|[v¨Ï2R¨µÀæ_£œnäbSŸé|¡Þ‚Ë18‰² UŠY}Æ3„º @9KŸ¬ÆÑíd•úìõ º'éÀŸQöš ƒèžÒ¬ê€8›ÑÍxåš0!²ª  ³ðzûÄŸËLîSuDàg%â < êŒÀlJ-ÄIÙ€8ãáÓ€E@…Pf!” U‡ œ|‹TÄf!®ÝD”*ÌÊ7zýëPM åâJ©SuKD%xI(ª7ËKª® 4D¶¦ â ùZÕ9˜Ô ¥âa©Uuϧñå}Ððð/¾«q'xŠà”úˆ;Dsx–X”\p¢'¢yOÕIèHÔ!D›‰GzøbíKô¾Ä{Õp‚£¸8æ+ìKÅ''„ñ 4Lœª£“Å$ k¹x:&Ð8”¯gðZZv­Ã“ûň’PßÁ„’𼨭塶Ö#€…À3jƒ ¾„eŸ€Ø*€ó<<Ž(_&ð]$`¼úºM8Çù³Úû þÀÀ޲‚%@cþ'„ñ ü‰í"I8€Ëùº"ðãH@§¥9-9€ûQ&üĘ_àÏŒªPV„P¹e©OÌö üÖ‰AMìÐBo;ø7b_нhf›º³臲uRçtGÌÀxñì€7@ÄÞ~ ÔfÄf“1[l´XeÌ;v»ìÉË(W@qwr±jØ?F¨¡À+ƤçÕ§®`ÊØÌ-vÎoâèÉ:vî¯$÷x GOÖQ×`õÔå|¥*ºƒ€wÿj(ðJ¯'Á£ úqû=‘ÕÎ^§“Ðé@¯“¨©³’wª–­;+øpc1ª,ž¸¬À”Üë 5ÔaqD%‡` 2Ð\×Ò-îyÌ•1üîÞLôúïú?Y»l6ƒA¢G| ó£¸ej2½SC8üm- M6w^Ö Àl÷ç` °^˜gÐtè z£Ž°Ø"ƒ‰H ""1ˆÈÄ ‚£.êo$ jJš8sð{/ÐXmÁÚbGöì˜Ø­„Xºh(CE`µ:v_½„^/ñÞÇçøÛ²“TÖ´`s_›LE)Kæw %mR˜0Mw¾Dbf)C¢‰M !²G0“»]Fnå*±¤“Ð$$* ê(:RMÙÉ:jJiªõýaöÔd^ús6µõÎßK`€Žº+ËVå£-%œ:ÓàŽK¬Æ‡üÉÄ£Lúæé^B »9žÙQL:ÅÐ;Ù[I: N¢¥ÙFõùFªÎ5PœWCEAÍõVŸk“IÇŽWfìôL¿$A¯ãLq#›¿*géÊJ/˜]}©ÿA™,ó`^@Ùê+p#iWÆ0tf/B îÑl-vì6™†j uÔ•›©«h¦¡ÊLK“ ›ÅN‹EyÕ˜ŸÉ½·¦9úwäìv¨®máõ X¶êŒ«/÷eà·€Õ×ÀužÓt7ôFaq »)„Ì0ì6×%$ I}ERÿ­½fÙ%¬-;6³‹–f;͵šêZh®µÐXÓÂ…³ 4Tš±ÛìXÝä8bùäõQ„¹~Þ;À¤cûž üé…£—5»rÁ`£/;€Tà*LÕµO-<>ˆÈ¤`Ò†Æ:$ °[eM^«ÔÚe¶¾´s¨¯M5-T5P°·‚¢#U.b<òë¾ÜqcJëe¸Å Ô7Úxé“ü{S ®Y:¬g}ÑP ˆq¿“¡å÷çæ zB¢M„ÇLd`Âニ TBQ›ì÷-é$¡,¿ŽCŸœ£ôT­K¾;#5„מɡG| ²ìÞ{0èuì?ZÍ«ïå³ùërWÍ\í®¡€;À"à÷øVêq“BLj(Q=ƒ‰N!(ÜÔF·•šh µAÒ+r:ƒò*Ë~Ü8’Í|³ö '¾êÚ<˜N'q÷-©üñ—}±´xf^¢u~à­µgYôo»úuvà9à¾äÆk€Ñ£Kè!Q$Œ"©ñ½ÃЛtÈ6YV—ãdß!M:N~]Îî•§±urxbà½ÅÃÉJ ÅæáHÉdÔ±ëP÷=~Êš. .7á†MBîpÀZ”É¿nMdR0=E‘<(ЏôжpÝBvO¡ÓK”žªc着KŰbñpj½´li0Hä6òä’cìØ{¡+_µ˜ 4»òúܱø×À}Ý]q3FÆ1ü–4Ò®Œ!0ÌØfø²°}§e‹  GÿHd»Ì…³Îm¾ùÛŸ²‰Ž4a÷Òª¤Ý1Q&Æ_‹É¤cÏ¡ªN«ÊÉÁ]ZŽzÝ]i³Æ&0tfª2©%pÙ¸Úf“©(¨ç«7O:tn!»_8k^é±±‡ó2¼ÿÉ9¾Ô¥ãÿi(Ù„4l’º³¢öÏÈ9~µ^Kó)¡1$Œ¤8¯K‡sžx°?½’ƒ5uIÀ ¬p‚ô|½¿²³_s5ðO— ±\x¡$6èžcUÄÀÉ=}GV³MX«»†v™ú s‡çz%3bH”§“{8 ÜqS ÓÆ'tvOª­iʤ¿¤›.ùé 9³z1dz -ÂøÝ;¦¶ÉœË­ÂÖAXËÔµ§Ž² !Az~3·¡Á†ÎÚì/U›Ó„ÐwYÝ3.…«nI§ïØ„Kž®¸––f§÷üø›Èp#WŠºè¼¿–°Zef…Ôé3YªÍéµàzêŽÊh0é{W&™cã…ñ{"ÒÒKäï©èp5+=”¬´Í®¸ôÛwWtêHr;þ¤Úž×À‹tì¾±ia\÷›¤ Áj¶ ëôÐø?o[É+´NbØà(b¢´«’f‹·×ž¥±¹KÃÅÕöºæŒºøùÀìýÆ'2prBŽE.êý …‡ª:\þ 0é˜>>«6Ÿ‹$Á¦¯ËÙ¸Ã%çf«6ø‰·Àâ¦ =£nÏ GÿHK}ž ›zò¶”tø¾>½BÈîAC£U“Æî|¾xÔ•j±·À| ¥[~°ÌQñdO“öï9ÃÑI\(¬§º¸ã­À·ß˜‚Uƒ½¿N'QUÛœ‡öRïÚtã)ª-þÓ“ ˜‹kùåä䟟Ouuu—¿+46ĬpúOL"2)«Ù&z}o^âìJ¬ ·LF7^“¤‰í1t|›_ǯ?H™ëÓ‡™T[ü¨ö”¸¡¥FNMMåÑGÅn·³`Á‚.}WtJ#âˆÏ'2)YFlîñ"æúJOÔvè|o»¡'1‘&jê[<~òïÇŒOn<„3Eîú™ªM~à  <€†6ý<üðÃ<üðÃDGGƒÍÖ9cKeÈŒ¢{† 7ê”séâäž—ã¨9ßLÍù¦ߺòÓ"êê­üü–Tf†cör^ÂÀ=ÿþ¼˜ÿùGÜ}-:Õ&?ÝíÆ¢’^z½ž1cưlÙ2222xúé§©©©qî{Œ:¢z“sc*ñ}ÂÚÎèbœ¯ìpál=ͬ™76Ûøpc1n,æ–i=xøÞ," ß $‘yæÇyíýOýìÕ6Ê!Ø™­RQrü{Þ½{ó‡?üùóç·ý[ss3éééœ?Þáqe|F}¯N$uH´8§¯Ql-v¶þó8å§ëœþì°A‘,ýËPL:9ƒA"ÿ\#yõ[¶ü§ÜÓÍuâlèà S½müóçÏgÆ ?ÀŠ+¨¬tì„•¤“<%™«ï΢çà(¬»0~†ÿ •–N?ÀÞÃÕ<ÿúI‚ n¿T$l`Õúbæ=²ßÆj›SÝ9Xä-]ˆ‹‹cÅŠŒ7£ÑøƒÿÿñÇc±8–v©µç×uṲ́+Pí_¢¾²k pÖm.áî[Ré‘ä¶“½D]ƒ•_>v€{+hñ®N-‰ò{ÎD×â…?’$1iÒ$Ž9¤I“.iüGåÀŽÝ°A"íÊXLAaaG–e’FqÝožÔ©ã³M6¶î¬Àà†ƒA’6»Ì®CULŸ÷¶î,÷¶ñ£Úèµîp?GYð<øàƒlذ¸¸¸Ë*ÉÖ­[9uê”Cßi 2–ƒÝ&¶ðú„°Ëħ‡1å7é7!‰àHç¶ž´Xílß{³ ÷H’²å¸ ¨‘—Þ<Í¿ÝËù²f­4Y°j«áèqÂÁ(i‰#Ÿta$>=Œ>£ˆN ¡¡ÊBS)·[¬2›¾.£´ÂÌØa1 ::ãLFß40ïÑý¬ý¼„ª‹V›*Èö9bÜ|†’ëßí 0€/¾ø‚ØØØß{àÀrrrþîØ^¡LûÝ ·ÕŸxI'ai´²úÏß8¼E;$ÈÀ¢‡0yt¼Ss Í6þï½þ¹"ßWšg;0 hèjÐÍþ?ž7Óq=‘ÊÊJ®¹æ§öüš“áôR å@Y)h¨6;tP¨uNà³/JÑI0lpÔ:£AÂl±³q{¿[”ë’R_½{÷Æb±ÐÒÒâîÖé¼”þØ›™üoO<Ë›o¾™Õ«WÕá{ív;øÃÈÏwܧ ‰&6=Täå÷3t‰žƒ¢œNÁþò[§ùbOÅ%Ó†éta!v¬bÁ‡øýs‡É?×õ}üwß}7Ÿ|ò o¿ývÛîU7Ó¡ívÔj!(JÝÊäÉ“YµjŽÍ1®]»–Ÿüä'{Ñ€ãîÎ".M)öGšj-|ñú·T9yØ&:ÒÄÎUã/:8dÐ+Çvÿ÷ïÇÙüŸr—ä0,Y²„;3ÀÀ@JKK™}:¹¹¹îhš`4ÊA!§"€›P*ýº…§žzŠ3f8üþúúzæÍ›çÔoŒ½+“haüÝYnÈ+S°c{Üìv™üsnI 2wî\ìT%íÙ³'ÿûß pK&ã@Õ–/‰þG>ôG”Ф.gðàÁ,Z´È¡ÿVž|òI>ûì3‡óïgŒŒ#kL§Ë/ |5ÐID&Ó{x•çh®mñZ7³ÙLUUÓ¦Mëð½½zõÂn·³mÛ6W_†°¡d ²: ¸£Qt:óæÍ#--ÍáÏœ9s†?üÐáL?!ÑôŸ„Þ ¬¿;b·É†¹îŒº½·’ÝÉ…˜‚ôç{ï½çð„õc=ÆW¸¥¼æÕ¦ ÂMU~‡ Æý÷ßïÔg¶mÛF^^žÃ=@¿ñ‰D$‰5ÿî<$°ËX-6R²£Éï²ïÍ“À„ùýˆMulI¹¾¾žµk×:üýË–-sGs$©6íÀÛ~Ÿ}öY§ÞßÔÔÄš5k~x\ý&$‰q¿@í 5'† ˆ®íM eúï3tf*ña$õ@çÀæ£ææf6nt¿0µ'"ð| @dR0Q=œ?É.If䪟¤s݃‰H B§—°Ûd’úG¢796 8{ö¬ÃÀÀÀ@n¿ýö=ÛI&q‰IýKýJ .*=ü}þû¿ßUœ››ëÐL*@X\ iÃb±YÅaÁwõ$ô s¨Çn%8ÂDß«™ñûÁôŸxQ1Ù.Û+Ôᨢ´´Ôá\•C‡eÈ!®n†t.QÈçRÀmµþ¦OŸîôg6mÚäð{³Æ&ˆ ?‚KÎD&£spR8kLãçõåÊ›zb¸dMI‚˜ǶÈTVV:œ¯ 99™ÌÌLw4ÅlGÀmîøåþýû;¼Ý·=§OŸvø½ñ½ÃÝ–÷MàÃ@V¢ÃŽ Å¥‡1ãÙ •Jd ìíSÄ_©†úýÆÆF;§0f̘K¦¿ë"?°íï'Æ úºã! 0 S7äLè*}.åd‚£L—HÀ×§64Fé@d:Ô#Y† PÇô¹©©‰¦¦&§®¹OŸ>FWî«ÚxÝåÀTw=„øøxôz½ÓŸ«¯wü0bs} AFáß³V6*“ví:â p©WD3èÚGšœJ#IÐTç˜qZ­V¬Vç%&&vÊ^`*°Òã <<¼S3›Î쬯h&ºgˆÃ»Ý+ ¢±ZIã•6,–¾c”6œÎ%é$ê/8–Ôh4:ýÆÄĸc% C0É]Àd2!ub_îå²_Šš’&¤.=fêL幯ü,ƒ„>áH:©Óu!$I¢ºÄ±°>((ˆ   §¾ß`0tÊ^à"oïbR€(w=ƒ¡syøY9µ»Ü]&ðql-2}FÆ3û©¡$õ‹t:ƒÐ÷{ÿ²Óu4Õ:6h-Z«¢h·ØÞ\…RkÜm 3Æ9|øp‡ßÛXm¡,¿ÎéÄ‚î1‹ l‹º‚N/q.· ›ƒµââ✊dÝŒIµõ8€¡\b« ·#€Aƒ95~:¼¡½A'4^pÉa€+hªµpþxÃß—‘‘Add¤Vš!@µõ8€Lœ/êxfë\–•ÆŒq¼yÙ©ZάQ€Àm­¡¼À±‚¥&“‰Ñ£Gkéòuª­_ä"€wþjKKK§f烂‚œÊdµØ9öE -.Jë$´ÇÒhåèæ"‡ßê”þ¶é±ÕêÎÕ¬ÕæÛ@¼*n£³7¤Óé7nœSc¨Ò“uäï­@œ ¸´ëÔKÝRBM©ãuÇGR’ó'ë›››ÝéÚìÝc +;šrrr5j”Sc½ýå™§€—èçèæò¶–8õ¹E‹:WS§©©Éã Z‹(ˆóæÍ»(­r‡¿g¶³íµc˜­]Zòôgö_àðÆ"§>÷ÓŸþ”þýûwê7ÝD·w: ·»±«7týõ×;µ$¨Œ×l|þÒQÌõ-"7  sHPq¦ž½ž¡Åìø¼’^¯w:ùªÍët(»{¹»ÏŸ?ßé•€VÞ|óM§3§Ö”6±íµo©»`N@àtÏáL=;–Ÿ ¡ÒìÔgyäRRR:ýÛn Ú¼A‡’8ÕÝYRRÒe––Æ«¯¾êôç.œ­gÇ¿NP_i{Žùƒ œ;\Åö þ‚sÆŸ““ ºt˜§ªªÊáD8$зF)înЂ‚‚.;€9sæðÀ8ߠŬÿëaŠUc0 ' ¸<:½ÄÑMÅ|õÖI‡·û¶Èk¯½FBB×VÕ‹‹‹]b/?BJûÀí ¨¨ˆººº.Édâü##FŒpú³–&+[þqŒÃ‹¼–+^ áá¾-f߬=ËÞ :µ—äå—_æÊ+¯ìòµ;}„¸@¯Œ€G6*ïܹÓ%ß“˜˜È‹/¾Øéû?*d÷ùÔW˜]ž3^à›†¯ÓKT5òÕ›§œ^êkå·¿ý-÷Þ{¯KÆîgΜq÷@`Ôá¦üÿ—â½÷ÞsÙw5ŠwÞyÇ©¥ÁöœÞSÁK¿åè–$$¶wãpßÚbçà'çøâµo)9VÝ©ï¹÷Þ{yæ™gT‡Ò5]ª¬¬¤´´Ô·Ÿ$3€=Õ൵µ„…¹®T÷¾}û¸úê«Î¹Ö^bRB9'ƒðø@QO ¡7ê¨È¯ãëwNS[ÞÔéDsæÌaéҥOnn.7ÜpgΜqw\¯G)0ÓcW§cÒ$×åéÑ£cÆŒá“O>é”eåñ‰¥ D&#é$$12ð[dYÙÓà£Bv½Ÿ¹¡ócío¼‘•+Wvú´ë¥øòË/yýõ×=Ñ›õÀ`²§÷îÝŒ?ž^½\·õ --Aƒ±}ûvjkk;ý=%Çk8óÍtz‰ p“H2êOã|„^¯£º¤‘S;ËùúS”ªíÒwÞ}÷Ý.Ö‚²eþ…^àÀžh–zàf`”§„ÍfãüùóÌž=Û¥^333“‰'râÄ òóó;ÿšm”«áüñšëZˆJ!0Ô(ê ø°áôÔ–5qè³"o,¦ðP¥ÃÉ<.EPPÏ?ÿÉ®]»<ÕLùÜœàR444ÐÐÐà–ˆà¥—^"99™§Ÿ~ºK{d»Œ¹ÞÊñíç9¾ý<éÃb0)‰èeùPB 4eõÊó°YìT5pøóbJŽ×¸ìë‡Ê–-[:UàÆQöîÝëÉK€ýÀžüÕ””Ö­[ç®Zèm|úé§nñ¨‰}#èuE QÉÁD$a Ô+UdÄæ"¯„ø:½„¹ÞJÍù&.ÖS°¯‚ g]×ÁDGGsï½÷vép£„„„tzE«0p‰Š¡¾´gúôéŒ1‚7ß|“… RSãšÞàüqeŽ 8ÒDdbñ}ÂIMT`ì6»ÕŽ)¸×èõIÉÎ{ª–s¹Õ”å×Q{¾‰æz—VÒaæÌ™üéOrú$jg¨¯¯÷¤ñJ@>æÉ_5™L¬[·Ž)S¦xä÷dY¦°°»ï¾›Í›7»üûu: A"29˜ÞÃãHÍŽ&(¨81yè²1½²<«ä8³¯’S{Êi¬¶`k±»<ú gùòåL›6Íé¨å믿v*ÿ¥ (0àÆLÀ—Ãb±x$øNy$RSSÙ´io½õ¿ûÝ﨨¨pÙi+»]Æn‘©È¯§"¿žÝä•L¯œRGf@oÐ)» żCcy e½Þfµc·ÊÔU4Sx¨’³*>çl>yòd^ýubcc=zÛ‡òtKxÅTWW{E·~ö³Ÿ1sæLþú׿²zõjŽ9â–ß©*j¤ª¨‘Hbf8Ñ©!„Fa$8Ò„)È€,+“²,wKÇÐÖ³KŠÕ7×µÐXc¡©¦…ºŠf*òë(=Yëô©ùäþú׿RPPà¶ß«+o¦®¼¾C€žàHÁáFBã‰îBtÏ"“‚1 Û•ˆB¶ËßUªõÃ0^§—$h¨²P]ÒDea•E 4Vši¬i¡±Ú‚Ýf÷ÈuÝqÇÜsÏ=Œ?Þ«•¥<´ùçsV”#Áå—¿ü%‹/Æd2yU)eY¦²²’eË–ñØcÑÜÜìÑß×”j’N"8ÒHTÏbRC‰N !&%ƒI‡lW®S–•¸XóŒ’2ì’¤ïþÞt *«IDATÖé$k,\(lPŒ½°žªÂF,Í6u®Äîñù’)S¦ðüóÏÓ·oßN•®w5 ”••yò'm^s×]wkÖ¬!88XSºû÷¿ÿ_|‘sçÎa6›½~=Á&¢z†@hl ᱄Æ` зõ¦’jdб)½lë8M9´}—ÌEލuè¢D,êÞ‰k[ÔSWÑLmY3•ç°š½[«A¯×Î5×\âE‹èÓ§fô.??Ÿœœ—­T9ã[aFFûöís릊®°jÕ*Þ}÷]víÚEqq±Ãi%bŽ Ô€)H1È@@cSƒI‡Þ¨L>¶p’¤ö¯ßý­ÌC¨ó—øÛfµc³Øi±Ø±4Yii²ai´bQ_›ë”°ÝÕËp®2ü¡C‡2mÚ4æÎKzzºæ®qÍš5Ü~ûí455yòg @³7À©S§Üó¬KÜrË-Ìž=›o¾ù†;v°f;øâ Í\Ÿ,Ë4TYh¨²ü¸£h]3o5ü¶¨¡#Ô!†첺¡ÉN›ñÛí2v«ìsÛ “““™5kÓ¦McĈŸÕw†ýû÷c±X<ý³ÍÅj”èq9räˆì Øív¹¡¡A.((ÿüç?Ë ²·ÚLHÇ2mÚ4yãÆree¥l6›}BÇfÏží¶*e#WÔŠ+d_eóæÍò 7Ü GFFÊAAA²$IÂø¼ &“I •û÷ï/?÷Üsrmm­ÏéRYY™|˜[o½_dÒ¤ILš4‰¦¦&6mÚÄæÍ›9tè'NœàܹsÜGhh(™™™dff2~üx®¹æúöíë³÷S\\ì­y¦f¯:€­[·ú¼2¶V/¾á†¨««ãÔ©S?~œíÛ·³}ûvolîðKâãã=z4“&M";;›ŒŒ zöìé÷VRRBQQ‘WÀ&o†p?ü°ì˜Íf¹¶¶V.--•?úè#yÁ‚rFF†Û”ùÆo”—,Y"çååÉÕÕÕrcc£l·ÛýNW–,Yâ­vÞ$ïs¼¹DóÒK/ñë_ÿº[ôdMMMlذ 6°}ûvJJJhiii7ƒÐ ’$a4Û$,,Œœœ&OžÌÔ©SÉÌÌìí°oß>Æïѳ1íxW^òf#„„„°xñbî¹çžnÚZ­Vòòò8vìyyy?~œÒÒRÊÊʨ¬¬¤ººÚ[Êá2L&DEEG||<éééôë×~ýúÑ¿M/ѹ‹mÛ¶1cÆ OnÏ‹ ÌÛ ÑÐÐÀ‚ (**â±ÇëVJ`0aöìÙŒ5ÊW{Wò½#™@~rŒô¦›n’?ýôSY p%ï¿ÿ¾+W®äƒ>`Ïž=œ9sÆŸoµ ø°úûÿ#¨¦›äœ“$I ‘GŽ)& »)ÍÍÍòÿþïÿÊ©©©r@@@wÉ·X­Úú"Pj¤uÇ 11‘x€¹sçE`` èý «ÕJcc#ÇŽãå—_æí·ßîŽÍP¤_Î,EÉÔ­™5k7ß|3Æ £_¿~"«SQQAnn.[¶láÃ?äèѣݹ9–÷\Îü¤ý aw'##ƒ2aÂfΜIïÞ½E£ø‹…Í›7³~ýzvíÚEnn®fò'z™Ÿ+/ç€ZÑFc4  oß¾üüç?çg?ûááá¢a4Ȇ xã7غu+uuu455ù\QS7Ô]Î4Qg),, »Ý®É´Ø&L`Μ9LŸ>ˆˆ0B½<„ÍfÃl6ÓÔÔÄÁƒyã7xï½÷°Z­¢q.Ïq _û¸”xxB+WüÇ?þ‘7Þxƒ’’ͶjNN'NdÔ¨QôéÓ‡ŒŒ „º¹˜ææfòóóÉÏÏgÿþý|ùå—ìØ±C„öŽó°°#0ÈÕÊ:”E‹1gÎ.\¸ ùNII¡wïÞdee1lØ0†NNNŽP½NròäIöíÛÇ®]»8|ø0ùùùh¾§ Ãf³iÍ9 wä"€ý´[*ð&,]º”ììlÆGMMÏ(¯ÉdjË­Ÿ““Ô)S˜2e  –}¾ýö[6mÚĦM›Ø¹s' X,Ìf³ÏŒåÃÃù뮻X¶l™–†¯ù@PÓ‘þN»¥-Œ·7lØ@MM £GæäÉ“>­äF£‘Q£F1bÄFŽINNQQQ L&F£Ñ/—ív{[ D«ÕJ]]û÷ïgÏž=ìÚµ‹­[·úü>&&†—_~™5kÖ°råJ-]ÚR``îÈܼ¡¥žôý÷ßgÖ¬YÔ××3þ|>øà¿*¤Å€Ú$%%…ÈÈÈ‹$** NûG5ZZZ¨©©i+kVSSCee%§OŸæØ±c?~œ¼¼<ªªªüÊÁ9’¿ýío4440cÆ ­%¨™ ,ÿþ?^Î\ |h¦4ϤI“ذaƒ¡-ëê}÷Ýçó…3Œààà8˜˜RSSINN&66–èèh¢££‰‰‰!::šÐÐP·_Wuu5•••mRUUÅùóç),,¤¨¨¨ÍðÛ;~N>ø >ú(111Œ1‚={öhéòJ€€}Ž:€@”ra×hé.Þ~ûmn¿ýö¶ÿ.,,dæÌ™ìß¿¿[“õz=z½¾­ŸN§»èïàà`’’’ˆŽŽ&((ˆÀÀÀËŠÑhl«Ø^Úÿ[}}=çÏŸ§¼¼œ––ìv{›´Ö'´Ùlmw'yóÍ7™={6:ŽW_}•_ýêWZ»ÌÍÀõ€S!Éã( C5sÁ`0ÈÕÕÕ?8ÔñÔSOÉáááÝå0‡ ˆN§“'L˜ WUUµéaii©,I’Ö®ÕªÚ²ÓdõZkøë¯¿þ’'»öìÙ#_ýõ²N§ *Ä­Ò·o_ù•W^¹Hÿ¬V«444ÈsçΕƒ‚‚´ê…hXL&“œ••%öÙg—Õ±úúzyÆŒZ¾®Î .×âI’$þùçe~9|ø°|ï½÷Ê)))B±…84Á7zôhùõ×_ïP·žþy-ßËrW,!dkõcccå‚‚‡S@zô¨¼|ùrù†nãG’œœ,?øàƒòÆ劊ŠNU ’eY~òÉ'}á~ßuÇ~âk-ßxdd¤\\\ì’œñMMMraa¡üÄOÈÉÉɈ|T¦M›&öÙgrUU•l±Xº¤ùË_|ážT[u ûµÞ)))ryy¹Ë‹HìܹS¾óÎ;åèèh¨ë—b0ä°°0¹ÿþòÓO?-×ÔÔ¸ìù¿ñƲ^¯÷…vpëɸ©¾ W_}µ\VVæ–j2V«U^½zµòõ×_/?÷Üsrnn®ËŸùºuë䈈_i©ÎÎð;ËAW­/º“™3gòᇺ5Æ… 8yò$GeÛ¶mlÚ´‰ââbîgôèÑL˜0#F••EVV–KŸµ,ËH’ÄW_}ŬY³¨¨¨ð…f9 q÷\ç+=äI“pà€¼páByøðá¢wv¡„„„È·Þz«üÁÈ¥¥¥r}}½ÜÒÒâÖgºuëV900ЗÚé:Ox™``‡¯4ʰaÃäÚÚZ¯Õ™_¿~½|ß}÷É™™™rdd¤, aÔ—ÙÓa2™ä°°09>>^ž6mšüâ‹/Êyyyvk×®õ•1«ìPmÓéM>á§À{€Îâ¢Q£Fñî»ï’––æÕë(..æàÁƒœ+®¸Âk×õæ›oòë_ÿÚ—Ò˜ÙÛèDY¿Î:€HàS`”¯´Pvv6ÿþ÷¿IOO×Ì5ÕÕÕQ\\LII çÏŸçôéÓœ8q‚“'OrâÄ JKKýÆØCCCéÝ»7éééddd0`ÀzöìÙfü‰‰‰šÈ„üÜsÏñÄOÐÔÔäKÍû`:Jéo8€ùÀË€ÉWZ)&&†õë×3lØ0mºñï¥Í6›Í;vŒÃ‡sòäINŸ>Í©S§8uê”Ö2ÎJ®ÂÔÔTÒÓÓéÝ»7ýû÷gðàÁôë×ÐÐP C›h±ŒÚ¼yóX¶l™¯å5´÷ÿì̇»êrO¾ÔZ’$ñÏþ“{î¹Ç§sïÛívòóó9uêÅÅÅ”——S^^NYYÙE¯­a¬,Ëm…5Ú¿¶Îv·¶EûWI’Ðét„……KLLLÛkLL ñññôêÕ‹´´4ÒÓÓ}¶6bYY?ÿùÏùôÓO}ñòO}:m]üñ(Ùƒ}Ž|Ç{Œèèèn3æ¶X,X­V,K[¤a³ÙÚzäïKw`çÎ<øàƒìÞ½ÛWoázào^À*|tÖy̘1NŸøK–,‘}yåd•

yĈòÂ… Ý–pDà{ö¬ˆá ¢¢B~öÙgå~ýúÉaaa¾–šËçrü¹ðRwuß—ôôtùÑG•:$WUUɲÕjíÖcúºº:¹¨¨Hþ׿þ%;Vè‰"/©¶ãV<•#øwwšp„ääd† ÆðáÃ8p )))mi²ü™òòròóó9}ú4ÿùÏøòË/9pà€Pˆ‹Çý³€2q ÔØ„‰ç{izôèAÏž=III¡OŸ>ôíÛ·Mbbb|ö¾òòò8tè‡âرcQPPàW9]H0%Ç?þä¦ëÅ3îN×–™Ç`0Æ Aƒ¸òÊ+éׯddd§™k®©©!//¼¼<Ž=ÊîÝ»9rä---X,,‹¯åÛóSQ6ýàààiñœ]Gbb"ôéÓ‡=zK\\ÜE¯¡¡¡?Èý÷ý<€ðÃÜ­[,***Ú2·JQQQ›Áûøùz-ð'àOþ 7€„R¾ø¿ÄóönTÑþÕh4b·Û±X,˜Íæ¶¼‹E¶gXÌÁà >¼•7eRpŒxî_¡Lúy|w“7ób÷qâù º1åÀ€WªÊz;1~?Õ =tC̪ñóÚ°ÐË p %¯yƒÐA7£AÕýcÞ¼½â4peùÃ(ôBÐ h~¬õö…è5Ò ¹( ¦h`X"¸;ð;àu-\Œ^C ³[½žñBG~ÌSÀ³Z¹-9 ت¾Nz"ðCOh)ÊÕk°‘¶©aÒD¡/?â1”j>šB¯ÑÆúRD?ëùŸÔâ…é5ÜhÛP–)Ç!&¾‰]ó?¡Õ Ô²h¨B™K„_¢e¶ÿYÑuÛQò£‰L1B|AêUÕ<¾ä™&#¶ ´e‡ß&á\O?” Bq€H EÊQRxó• ÖùXC9<ñ•Ð5Æø /ìé zlè:`JÍABï`p ’zxøòbÂIˆwåÄ,¿W™Ô Eâa©UuO ²¯…R ñ|­êœÏ£÷P |„R?}„ð‡7ò2ðk”<a@©§V'z)!.–:U· ÂÌ´Oª¦Ù„â 風T]JõGCÑû©¨–£ìì Ÿ(èÔÿN”3)dJ©%Ñ› qF6¨º#ð„R qPPuFàgôö‰¹!—ëïSuDàç<J/D•ãªNºéÀŸfaÝVšUHæÐ=Ñ)À*a ÝNV©Ï^/Ì@08‰’ÐAˆŠY}Æ3„º .Ç|Ä&"ÝÌ3_¨·À"Ÿ;„ñø¼ìPŸe¤Pk³£ìÿ>( Éçä úìÄ.PK˜ ìG)ë, L›Ò >£©B]îâZà]à¬08ÍÈYõ™\+ÔÓ9D*£Î3 ܆Ø3î-¶ï©ãü\Ño8Дì0ËEOì1Y®¶yˆèÄZ"ø-p¥:ŒUk—Ū¶å!µmC„š‰!€/ ܤI¢Iœ¢8ª†ùkT Àç¢Ô0LBì=¿ùÀÕèGPöë „ð T‡ÌF™@ìÛÍÛä8ÊDÞj P5x³PẠa(ëÖSÕè 0©ÎBç'÷hWÚ‚’Zk °^•:¡Â¾#¸  d @¼*Ñ>r•@™*¥À à`·ÚË „8HD;ôFÉZ“ª:‹¼W-¹\5èB”Í8gPòå—µ“ñ…¸Jnz}»W#Ê*CÊ÷"†Ö¿cÕ¹‡@uhÐîoÔð¼uìÝúw3JVÜRUÚ÷è…(³ô-('í¬í^íâùÿ8`òT"á•IEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/noguacamole-logo-24.png0100664 0001751 0001751 00000002335 12440135707 031111 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDRàw=øsBIT|dˆ pHYsììu85tEXtSoftwarewww.inkscape.org›î<ZIDATH‰½•MLeÇÿï;³Ë×vaåc+PbL ¥„/9 MÜô@Io„ƒ‘€‰iO&š&ƃñdzQcC̃ÄXè‰öÄ—àFhÓJWwØ¥ÓewÙÙμóx`f³»Åz0u’'ÏæýÿþÏûÌû¼Œˆð<þ\Õÿ€üo0ÆJx^:uêT‹"µ¹¹yooooÀ.ež¹¾°Œ±Òöööᦦ¦‹Dt®¸¸X©««+õxdŒ­ƒÁÙµµµïˆ(q"@Q”úúú®¹\®×š››k˜ÏçƒÛí†iš8Á8çˆÇ㘛›ÃÖÖ‚Áàn4]ºyóæåd2¹ŸôööŽú|¾êëë_îííÅÙ³g!„€¦i>À9‡$Iàœ# aaaûûûܸqã‹¥¥¥ÏX[[ÛàÕ«Wý¡PHÁ0Œ¬¸m1ö@’$Ȳ Î9&''ÑÕÕ•ž˜˜‘»ºº.µ´´(š¦Á4M†‘äVADYH.@’¤¼*eYFSS<OIww÷e¹±±ñtYYt]‡aÙ,„€aÙŹUl÷¶ MÓ ( ¼^oÇU·ÛݘH$F¡(Jä¤>Ø€B÷Ddÿa¨®®F:~$‡Ãá;Ú‡‡‡1== EQÐÓÓ]ןª"À9‡"ÏýÊÊ TUÅèè(@UÕ=9/ïîî¾Y[[‹‘‘lll`vv©T ^¯gΜàœ#‰ ‰Àápàüùóhmm" ®2uþo¾½ýÖÈU…§p}}@ ¯ YqY–ÑÙÙ‰ŽŽŽ§Nðõë×Õ¡¡¡6ÞyÿãÛU„0É4ÿ{e ºôÞG«Dt<‹¢ÛÁûT­u?žA•â@¥"Ã!±“‡Ë3žŒaâQÒ@4¡c+tÿ` »HðîW¿¯þüzsÇ«•T E2G±ƒ£ØÁPâ<~·¡Â$è"?šÀQæx ø~zû·_æ>ÌÀ¬Ïç»öé积Tx^tŸO)t(LÀ \SBøýþ½™™™ùåååw< ‘idÕJðšššîžžž·;/\¸Pît:OÜ’T*…………é©©_oݺõI:Þ`ÈØ#¢'6€¨ X}aPUUuÎëõö¹\.Ûí.¯¨¨¨p8NUUÅb±ƒX,ö×ööö|2™¼k1 €p@D"·§UEÉ ns²-F–ca…àÀ"2òšl7ÚaÈ•r–¨a‰Ú6(Gôo¤ËØE|Ú½qIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/drive.png0100664 0001751 0001751 00000001360 12612226251 026540 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<mIDATxœí™?‹A‡Ÿü3q.98­m-D ‡ñ°î:‹ëùB>JHRí„|€´~k9®³+±8P1ÉIÆ"^%;;YsÉŽy˜jyó¾¿ygvAAA„Ý#ZA{x ¼J7“Î?a€wÀ+àÇMüÁ`Ø )ðv•¢|g€¾z•à[b ì¿|ÄÊ3èC`ž6£ c€G¾b_@.U:›§Ä2_/| xAÓ  ¯;èG¶ßàVŸ| ói‚Eà;ÙÜúâ˜w€ŸIBŸ%ð˜åöSà‰ÐÇ€aÝ}Xög>BŽÿ ‰p²®`ŸÙ~SK3¾ø—44p×'PÙ’DI²l(!bXæï$É€á­ÿk4p”$J2à„ðv€kòxœ“B_ ·Àò vß%pÍ€Â9ÿÇ‘î¹.ÀÕZÓÙФQY­µÎSU_­Vgý~ßclˆcl¯×³ÕjÕù&+v(‹v2™ ”ï;“ì±X,(—Ëcb댭ÎZkC.@)…µÖ:5Žk‹5ç³-œuˆ›Ê"«ì¼ùm'†‹‹ †ÃaìõV«E½^÷Ф£Ñˆn·{½P(x°óK H¢h•¯ún‚\Íf“|>>õÓÓSïX®£ðt<—r¹P¾‰þÍ|>§R©\cnÅibmÔZŸw:Ãv»s¹Uf³ƒÁ`®µþpyy™*ƃZ­v¦”š‘ÇÛU‡RjV«Õ΀û©ªAAAþc~²INakIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/group-icons/guac-open.png0100664 0001751 0001751 00000001315 12440135707 031556 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<JIDATxœíšÁŽš@‡¿iʱ6\ÔC_ƒâ“ì•ðÄÓøÜöA4fõ@ö ö€=`ªG’*Jk;#,ÿÝe¾dTòç7:JkÍù$@'@:€4N€tiœéÒ ^Àç¶”R_90¾´NdÆØZë­*i­ojÀÏ€jÏ— w7÷ㆎ{ÁN_k÷Àض?ÊæY@)5€oÕ{žçQ1Nkµ!Ïs¶Û-Y–Q–eý£'`®µþn\Ìòìÿvæã8ÖEQh)Š¢ÐqÿñM0éKÕlÇüËÒ4íµ³ÿ"MÓ¦ãk‚€¬~æßoB¦»¼\¦º# <Ïãp8àû¾ñ0ëƒãñÈd2©® ðµÁiz#4@o®ó¾ïAõRñ+ó10«6¢(²KÖ#l³kûÕ1ðr‡××Tw lFw¥ƒp¤HãHÆ  Íà´þI `·Û±\.»(eÌjµ" ÃÖu:p:Øl6]”²:f ~8Ò¤q¤HÓÉ,0X,]”²:ft" CÖëu¥zgðCÀ   @'@:€4N€á~çj#ÏóWŠÒžF¶óµýê˜ ØWÛíÖ"R¿4²í¯íWÇý=nRüRè ,K’$i“õUH’¤¾^èѤó€["3øERn™œ­‹¬µPÒZ@MćX*k5þÆ{_,ÝZÀ{Ç= HÆ   @'@:€4?Dn¥…8ÄèIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/group-icons/guac-closed.png0100664 0001751 0001751 00000001513 12440135707 032066 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<ÈIDATxœíšOOA‡ŸiêAC³ö`BmÚPJ}ò@ød­ÈŒG  t´Ö?¬"i­—*À!phár.ljo§ÿAâ“åØŽšŠ²PJ}Ú@:øn}}|>Ïþþ>étzvç¹»»£ÕjÑétxzzú« ø¦µþi,â¯BÈõJ¥¢ƒ–b0èJ¥29NLr J”äÂ5•&;F£1iÂ~®‚Êåòj34 \.‡ ¸Òqþ(¥<à ÖÖÖ¸¿¿Çó<ãe¶ úý>[[[ŒÇcx6á³Öº¿¨Ÿé}€(€l6k•|³ÙäááajÝææ&Åbq©¸žç‘Íf¹¸¸€g­>ð}aG“ió2½J¥’ÕT- 3/e…BÁ*v©T Ç;6ÉÍôV8|ØÙÙ1ì²z&´¥fµ “ø½€3@Z€4ÎiÒ8¤H“x¬‰œŸŸsttdÔ¶×ëͬ»¼¼dooÏ(N½^Ç÷}£¶óˆÅ€ápH·ÛµŽ3Œã ‡CëñÀ-g€3@Z€4‰7 –«@*•"—˵íõzŒF£©ud2ã1ã |ßE-¤X,rvv6µnww—f³‡$c¿œÒ¤qH Æ -@g€a»?›ïÛÛÛW’bÏ„6£¦w‚àC»ÝŽ é_jµÚÜ—£6LhëÌj&ñ¯Ç–ÀK k€ñxLµZµÑú*T«Õ y€k“äwD&ñ‡¤Ü1¹(n½˜õ®JF6 dÄ»8*i Lã­–¶6à­ãöÒ¤qH Æ -@g€´i~ÑÁSh«xèIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/x.png0100664 0001751 0001751 00000001117 12612226251 025676 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞbKGDÿÿÿ ½§“ pHYs × ×B(›xtIMEß+7׎øÜIDATxÚí›MjÄ0 F5Þr$¯JOÔdÙÓ”B×¹Ñf©.F¡]LÚØúlÙX‚0³‰í÷Èœ 3ÓÈhðp.ÀdÇBD±†(kÉ fÎ9Þù7fŽ™c Ž(k`YSòx6–ðžs%há­$<ƒÏ’€€¯-á/ød (øZÎÀ'I@—–Z¾”„øSJÀ£%hàÿ•p4é˜ÐJ@Àï±<›ã(ü ¢ ¥ÍDô™™1F9w¬c¦¤Li?õJ¨6w3 ±š“[Puá-]’&·5r_š=o.‰EQô“ùEþ£Ç\Ïž*-á.¿“|®´²‚×”ÄV™pë^[lA‚ Q¶” †G•Å-$@à‘}š`ðèÆH Pø¡’àðš<àLžðJpö„銆÷Þ`!{–8Çœ•¥ªJ¦Ès ¡øbBGðE$„ÎàáB‡ðP ¡Sx˜„Ð1v"GLÓiG±((ŠŒbQHq9韟‡bQ:TŸ` È5ÝÊü‚sÏ™ŠÇãç˯¾ç•W?á³ek8ã¼™\xÙ9dçfvÓ7Òq~’ò¸½|ðîxç Ò&ž2¼Áíßá!¿ÊžÍµìú®ŠÝßÕ°o;j9AèÓ7›Ã†ä3hp>§Ï:‰û¶yì…}‹²E,ù÷C|÷ý.؇´4'Û¶•ðØoóï%«8ùŒi\|Å,úôËéøÅw1?JÕ×6ðÍ—k)Ù]N]m=µ5õÔÖÔÑPçÆ]ï!VE±30ýâ¤eÛ;}®P@%ÐPU-yùCT—z¨.óPSê¡r—›P@åâËgñó«ÎÃjµ´:NyI%¿šó[V.ÿ3#Fä³äÃ&5ŠÜÜôåvWòøSÿäþ—§Ç57^†+5¥ÓõO”€|^?K}ÊWŸÿ- €ô'Ît+öTgš»Ë‚Ã¥`Oµ0`DYý“óŇƒ+ÞÞÎÿ–‘“›ÉïœÇFµ(sÏ-0eÒþ0ÿª¸ŽYQYÃw¿ÌŠ/¾ã¶{¯çÈ)㻣êíò£Жï yè÷OQ[]ÏЉ9Œ˜Ü‡áGæbwµ¾ÓãÅÀ`·Ÿ¢¯=tKâ#°}Å|øü÷”ï¨gî//fö•³¯¿XÍs¾Ì†5ÃélÙUàósø ¨Çü×{+™wë³Ì8u*×Ì»,jëÖÒ2tƒ·¾Ï+/¼MÞ4fÝ2‘Œ>ýЄ E”:t쀦Ü_'d°+—lë’:ëšÁçonåë÷w2îÈ1œ7ûLÞ^ø7\wWÌ9­Uù`0LCƒ·Õ£,!I"Š"SQYÃu×?Å®â}Ü1ÿ†Ò%u‡CV@÷ß®ZÏ”s†r⥣%¡y¿O QôàQ¸ùŽè#A¼1LªA]Å*v½µc׆*–½²•½Å Ølö½Ãn`éGX- 'Ï<2æç KùÓãoñÒ ·7oûÛKK¹oþB.½æ.¸ôÿº¼ÎÑ8$T_ÛÀ]7=Ìî]%œëD†Õ§Åþ=ÞZêC?Ør²l)ô·§|D¯†­8H¸B8«cCîx°–Â:j¦‚š]„‹ŸÙÀ¤AcxæÏóš·ù!lVAZ”õú8m·„Û·—pÚY¿eöUpÎ…­[´®æselݸƒë.¹²²2æ<0¥•xBºÚâkŒÇ—ä×B:ru¸Ãõ(ñÕRâ­¥&ä!¬kQˈ> É­!Õ©Q÷«akª¸öê–­…Ýfi%]7¸îú'ۭ׈ù,~ÿ!þñÂ;¬_½)Ϋé<‡Œ€>ÿÏ*.Ÿu#7ν C så#ÇÓoXëV@Z^–C²F-§9%@Ð „pÇbo8D]ÈG™·ž]žª¨e‚ƒløG; Œ~þ­_W0dp_Æý¡Ï¢ª‹þõE«²¢(ðÚ+wÅU·#Æ æ×ןÃòVÆU>zµ+#òîk‹y÷¥xÜ^\™6ÎøÅX&œ”¤ÄÖ¾¼¿ÅQD‰t‹‡}d¢ÛE¼ã;7”—Dö÷Ÿt#ºøtkÛ÷ç÷Ë˹éê‹Zl“e‰ ΟÞáú¬úz#÷Î_Èï=„ÃnÅã à÷û;|œŽÒ+´¯²š¿?÷Ÿº UUI˲sÖ/Ç1þ¤üåXô·§Óß‘Ž@ûe;‹,Jˆ‚Šn1Ôµå^ÊvÔqÑÏf´Yî…¿-&-ÍÉì‹Oj±}ÙòµÌ5Øîyt‡D`¨ÿgLñ@ä9X<í"@ÐÐÐÐÛ-šºßÀŽQ4«_ ÃÊãþÆuj—ËÁÏ.8¿¿ò1ÓN¸™†ãó¸÷¶GYöa÷Û}&©Ú½«„__~'Û6íà–[nañ⍦¦¢ª*Æ E Ž=whÔÏ ôwfà-ØÅø<ÎZŠ„.w}F$Ʀ÷§_÷@Ó)E l€G3ðª±Åv✑¼ýîç<ñä;„B-­Ö7µøÕ×9ú¸_ò»»^䕼Æ'Ÿ|ʲeËHOOç‘{Ÿá‹e ][GIš€‚ Üñ$u5õ¼ôÒK<ùä“HR¤Ï1uêT §\5†´œØÁ]NÙÊ0W.N%ºe7^l’@Š,ÏàI êØ·ûƒí·6ýì"#R%"vÆ]:;Üz›ÄÔ,?¿çh^~s1§œy;UUõÍûš†ï%¥U\zÅ8ï¢{ÉïÀ°iSÄU1}út ¾!33ƒ'xžòÒ½qÕµ+Hš€ž}t%»Ë¸çž{¸úê«›·¿þúë¬^ó-C&ä0é”I©‹CæqÄ3Â’¿Ž¥2×±Ýaƒr¿NGŸŠý†§sÙCS0ÒC\~õ›·ûüA|øU&}-åÁJ~ùÌt¦]4œq3òyèᇚ­Íƒ báÂWðz}üáwOuܿפËqáýÝ}’ÿþç+þþ×·˜>}: ,@#ºõûýsÌ1È‘KÕ»OßÇ&’gȲ‰¨:´× xÃAyi3éÙN¸l£¦äE-÷÷;¿Âªf°sçÎæmáp˜ãŽ;޵kװ賤¤vï5uû#lÓú­x½>nºéæâxë­·P,NþáÑåQì ¸)÷Õ·ð1¹CFóP¸ ?*ÁýáºaÐnÛ¡h ÛDBýëWµy ††O á&ˆæ’Ð2cÇ1­?Ó.Î5OS<ý†¦QV^Öb›¢(Ì;]7ذvs—]C,ºÝ´îÛœtÒI­ö•——3`DF‹G—,H8d Q&¨«Ø¤¾èJ¿Ž]ØëoÙúA,bópÝ.Y°KJ›žø& ›ˆTçâE¿ŽäÕÐì"º3ºÕ|·§÷"v*V\CÛvM(V‰ã΋>=¼!i|ûÑnHKûaJÒ 'œÀ†Õ™:ãèx.¥Ót{ ´ö›ïp:Lž<¹Õ>U “~P s†ÅÁ0W.™ØÅ–wi}Ƞ¯·°¾X*B8¶ø~þuåÐϑޮx ¶@3tvyª¨zÐŒÖL¥&Œ¥4ˆ\=` heðìªØj€œ.¾þúëÛÇŒCff&ë¾ý”íÞY„ P”–b‡ÃèšAM¹·u¥¼¶]~,¥v¯ÛE@ò¶Ñsí$"^5H¹¯¾EˆlZŠD°¿5j绉{ NÙŠ$ˆH‚H–µëú$ û~ˆ L˜0=E%]v®Xtû#Ld¹õiEÁápP±3Ò×i á=ö‘/F··¯o5]FMï¦ù]H‚ˆjèµÖ­ÌÁqÎÙV‘4 èyôý‡êŠ<² Œ.Q*+¬C’EFu¿ØÁ™(¡ûí@mXëFŒA8¨QSæiÞf8¥æb@ož5Ñ•¸ÃþæŽv{äÚRÉwf’mm_¤\²³“x¬R$,%#gé¶:\)Ñ}qªª"IÝÿóvûD1ö71kÖ,¾_ñÃH 8ÈN`¨ßhGÌé6Æ€r=UÆ›eR£„d[SȰ8°H´6:({Ã(ãïñêlqkmZÛc°Sb¤K$¥Lm…—òõŒ;6êþp8µåïjº]@‚ Ä ù¼å–[e…Uï爐¨¡y»–*¡¹$ KÕ3"6£º`ë¾IÛ‚‘®<ú;Zú±²€ghª êX*‚¡ÖÍcP‹X£ÛC® #zµV-ìŸÆ·Nc[Ç0àƒgÖ¯¿þzÔ"ápIþ<Â2³Ò)..ŽºÏårñÚk¯!ðÁÓÐ㌽)÷׳©¡œB÷^ö[fÇÜ"ƒRD†¤D¡ÕãÄ6ðÆ÷Tƒl„ûDåi†sªOÖ=Aì…~l…-oŸÚ~ Ò·í¦t[·Þr+ùùÑóåGÐ64Ÿ={öàóEo).ºè"Ž;v*U%n>}es«ÈÀX4 «‚ @†E ]HUº%”ZPÓeŒ¯ð7°¥¡Oa'¥¼°žeÿØL^^=öXÌr{öìIJú—nÐa1 ƒ%K–Ä,óñÇc·;øviÿzj-Z¬à™ý8åˆÕØ.)d4,ÖbŸÎnoäH ¢pkC…î½TúZMVl‹¦ÈÆ Û¡<ÌF8WAsÅßJlû_%¯Ü½ õë×Ç,·wï^Ün7ùƒúÇ,ÓUt»€Îºàl6+÷Üsš½Yw:”––’——Ç–U¼z~Oì/ß%ÛÞ—á©}ZŽ ê‚õ¡È+–~¬’@¦UÄÙÆïgeüZ˜}FŠ<Õí^ëŸK·:Ú4ª2¡~VBýâ‹múßÒ"Þ}t5ÅÊλèÓ§õŒÜ&Þxã ÆŒw;KRú@g_x*Û·ogåÊØs”””pôÑGS¾½Žw¬d_qôì_¢ 4ßåÑôÈ´âX J‘*2Ð!Ö†cí@KvÌ †x3Ãê` #³åÈ­¹î1O•pPãã7òÉ‚Mäåõ¥¾¾žÛ{Y°`v‡§NíØÉ:ARâš’)mß¾½Ír²,óÍ7ßpÕUWÓPåçÅÛ¾`é_¿Ã[_BëØ }XK£Çï¤`7„ظCAôÉ5!úu[üØwøÛ 8“E‘©ƒœñ}å†a°á¿%<{ýrÖüg7S§N¥¼¼‹¥í«±±‘M›6qä”ñØÛIÄÐ$E@öÚP/¾ø"Ÿ¾‚¬¬l6,/áÙ_-ç«E;PÛéXÊBˆ~}bƒÖý»: A5v“"[Éwfr˜3‹,[kC¢aÑœ"†EŒ8tÛ@Õ Œv㘊7Õðâm+Yüì Uâõ×ßàË/¿lÿƒÀºuë0 ƒ£’“#()³2ú èƒ(ŠÆý™iÓ¦QUUÅK/½Ä¼yóøüÍ­¬þx7“ÿo0ãN€3=F„ ¹Ù¹)תh©-/ѧÁ†Úø:Ä’ ’ÑFf2C€à@m;e›ØîŽ­C7رv«?.fçº}(ŠÌwÞÉÃ?W]›X»v-@Ò’L%E@²"Ó§oN‡ÔÄ5×\ÕW^ÉwÜÁ3Ï<ÃòW·°üµ­Œ8ªfæ3lR. -UFÍQÓåVâiA7PÊBH5þà2!nËCTj+¼¬_VÂúå{ð5„e‰sÏ=—wß}·S–ä5kÖ0|T|S¡%isãïš÷0VoÆï÷7‡´vÃ0xûí·¹ë®»(ÞSŒ®é8R-Lr0«¾Äïóaw8(¬ô30ËJ¶«ëSìvAHMM%55•aÆõtuÚ¥ÚfOuÝ0ظ~5;wröÏNMj’š\á„SÃçóñÕòȼ­ÝÕ¶Uø©½&Ó^¯'¤l¯ô³»*МàóÃEËN;çĤÖ%©šqêqˆ’ÈòµØÞÐØXêeŸ;¾ùç?U Ã`oCˆ¥^Üþ¬éÁ€Ÿ?ø'ƒ†æ3òðöç“u%IPFfŽÃçË>Á]_×bŸnì© ²¹ÌGƒ¿ë§èêÔûT¶”û)© ¶J+üù'Kht»9íìI¯WÒLxúñ„ÃaV~úAÔý¾Fa¥-å¾wÙO•:¯Ê¦2/;öúñ…¢ßXþëu$Iâä3;ž_:Q’. ãOœŒbQøtÉ»m–ó5¶WúÙVáÿÉ É Òâl*õ²sŸ”àý&*ËK)øòs&OØ#kª&=GbŠËÉäã&ðõ+©ÙWIVnÛ&üÆ€Jc¥ŠUÉv)d¥ÈXäO.Û-øÃ:5aj<*a-¾ q¿÷º®sú¹Éí<7Ñ#¿Ä‰§®ë,ûwÜŸ ª:euA¾/ñQXé§Î«Æ\bàPBÓ¡ª1Ìæ2/›J½T6„â®i,Yôé™iLžÚý¹€¢Ñ#YZ§L?»ÃÆ»o¾Âù—]G°}fƒ¿Jƒ_EERí©‰T›tÈ´LþNƒ_Åí×ð´Nß‹ß|âÝ»™ó‹ ‘“0,=" «Õ™³f²è¥|ö¯œrÁ5ÚHB UשõêÔz#>(›,’êHs(¸¬b›³b“IHÕq4Ü> ·_EM4•>¬¯à©?=H¿yÌž;« jÙ9zl­ ŸÏÏÕ?»• ?ÄÆM[ðJx;ÒµƒØ-6EÄn‰¼lŠˆUépR߸Ñtƒ@ØÀÖ„tü!X'Ô‰›£-òÒ,\ÅÏX²d ?}WÒ“‹Heªw8ìüêö¹Ê¨1Ãxü…û9÷¢ÓÙ¼yS§Nåºë®£®®®ÍÏJ¼ú꫌7–•+W2}æþôÜ=íŠÚÿî’A·@žF/Á@|³1Jv—óÒ3¯±móNrssxüñ'˜3gN7×°ûøþûï™?>‹-"ÅåäÚy—uh‘8«ÍBŠ«û×8k‹P ÄÛ¿]Ã0 –ÿçKÞ\ðžF/“&MâÊ+¯ä’K.!33³kÚ5†ÁÒ¥KùóŸÿ̲eË7ép®¿õ 2²¢/â §ËÍÖ}ëœÅC HS5êë¢'Ól w}#o.|U+V ±X,œ}öÙÌ;—ÓO?=)iþ;‚ÇãaáÂ…<ýôÓ"Š"GNϳNâð±Ë¦šž‘š”lômÑãht{;·H¬ß à‹5¬øìk¶nŒd@Ëí“ËœËæpùå—3nܸ®¬j‡ðù|°téR^~ù%Ü8SœxÚñœvö rúduúØ«‚+µ›V)ê½B@†nP_×€ž y_e5+>ýš/–Pµ7’×9-=I'1qâĿ׍x¿31@IDATQ£š—ïJª««ùꫯX¹r%+W®díÚµ¨jd„Ù??ÓÏ=‰é3§`Mð±#ŠéKžÕ]ô Alƒbg0 ƒ-ßòÍWk)*,¦xW)Áàu»ÝÎØ±c™8q"ƒ "--ôôtÒÒÒš_Mÿ§¤¤D¬åuuÔÔÔD}UTTPPPÀÖ­[›-ê6»á£3zÌp7‚‘c†u™Õ¸7›è5‚Hœ´ßÛþ*…Å0 ÊK+)Ú±‡¢{ؽ³„Ý;KârJŠ¢ˆaíæ JËHeÔ˜aŒ3Œ‘c†1hh~§sA¶…ÝiÃáèYãáô*ƒ!¼^’aâØWYMõ¾Zü>?>_¿×±/°ÿåÇç$MIuâJM!Åå$ÅåÄ•êÄérâr¥àJK!=#úÂo]… €ÓåÄjíyêô®¡ ‘)?’$ÒØàI¸OÔ¹yÙäæewë9ºQp¥¥ôº‘%ôKt4dY&-#‹µw<ç{‹U!-#µWŠza Ô„(ЏRSPÃ*^¯µ ÙY‘q:íÈIX4.zwíˆ|‘ié.BÁ>¯íG>ÝG’DN;–^Ö׉E¯P«‹ÕB(& …[c 7!€Å¢`µY{Íð<^5a±(X, †n †‚¨ê¡™ÑL–%¬6+V«¥W;Ã!' &QÀf·b³[ÑTP8ŒV ‡UŒ^-ˆŠ"#+2Eéq?VWpÈ è@$YÂ.K°ß¾¦©á°Jx¿¨ºÛ QEQPùG!˜ƒùQè`$YB’%löˆÏÉ0 4MG×44MGÛÿ®kZÂâEQ’$iÿ{Óÿ=ðž ~”:A" ˜b´WÅþeö»-t_‚Ðü¢ùo~iŸ„€Ú£IQ—ð1i“^i‰69t0d’¦€LÂIB˜2IS@& a È$!L™$„) “„0d’¦€LÂIB˜2IS@& a È$!L™$„) “„0d’¦€LÂIB˜2IS@& a È$!L™$„) “„0d’ÿf×¶Ú½Id½IEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/folder-up.png0100664 0001751 0001751 00000001463 12612226251 027330 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<°IDATxœí™1LQ€¿3(Øj#mXt'MÌuÐnºÃFgYº#CÂf ‹3ƒI›¨±›‹u©“ Ivã`Jõþ”š¦︃óz÷¾äM¼{ïß;ÿÿF£Ñh4&’O€ 1à=ðÒã8|>xŽÈgòôƒŸDû9j¢ØTÂ÷΀­­-òù¼ëAJ¥···A]ãH¾rqq!^˜ŸŸ¼¶¨ì<^¹\Dˆ;ìÛ>?~Çô:³¼¼ìrjç¨|^L=Š>߀·ô¥§MÓ$‘HL}R•€ÌÔ#xÄZÀ'€ÕÕU_&Q|î÷Ïä3àÀÊÊŠ/ö„l6›d³YffT{dÏýý½²j‡Gž ÿ¶ët¯7$ø& Z­’L&ýšnbxNE ÃÝÝ]I¥R“JoÇmO#`nnNþdf‘N§åôôô¯Ô42¤Õjý“›GBÀââ¢t»Ý¡ÅIèär9¹¼¼´­ÎB-`mmMnnnF–§¡P,åîînäâC+`{{[”‹ª×µ€aÔj5jµšbˆG:–e9î¯âüüœr¹ì¤«ëªv¨ÑX,&ÍfÓÑ®O“““ÏoÀØåp*•âøø˜q $cCår94‹vvv¨×ëÓˆ%Ø~¯*•ŠãÓ?TgÀ€½½=®¯¯ÙßßgvvÖÑ3GGGŽ®©œÒn·'6–J»ëëëÊ p@óÏ1MS®®®¢+¥¥%Û*0Éd2Cï"#ú7AgggÑý»ÀÃÃÃè €þmp£Ñˆ®€A«T*bYVt²¹¹)½^/º) ’H$'@ÿ9êgAD% oÀÈ;8U5Ø “‹Åw,àãS¡Ñh4F£Ñ_Üʶ‡Ûö@IEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/action-icons/guac-back.png0100664 0001751 0001751 00000001112 12471273273 031636 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞbKGDÿÿÿ ½§“ pHYs × ×B(›xtIMEÝ 7¡H׌×IDATxÚí›;N1@ß®èØ„;p ¯£áS¦¡à|4ô„Ä'Ü… ˆ"R(XK(A›±gbE)œì{ñŽ×ö¸Z.—ä5™GPE€õh€³\4À¤}5~`ÇüIûù¶}¿ËA@ÜÇ+×*.¡v¿*a¼­kàU$ÔÆà'ÿÀ‹K¨ŒL†ºÀÿŽ/`øðœC·?êØnÜô·p ô¿– êœáS 0Ÿ* €à05|Š` >¶sð1 -ÂÇ0lž9øI0üó#‹ðÚ=À<¼¦ðZÜÀkp/‡À#pà^²¸„—°ç^B@¸ç]ÂKXsG_sà xÞ ùŠ.o§Ì®%Hn%H>¹” ý$$¼x‘ 1˜—^$hÍÝHÐ\p!A{I,Hxµ*!Æš i ±V…gV%ÄÜ0)!öÎ9 )6Gƒ„7 RíÏ€ RÖ˜ºB$¹+ER»üIvl×»HÊJ™Ü 8¦ÚlE‘Ô¦¶¢Hj]N˜Æ€·(àsñåt«ç‚„wMxK£À_Î ŒPÚH©œlZO_^•“£™GPyÇ7 û§·…’!LIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/action-icons/guac-show-pass.png0100664 0001751 0001751 00000001305 12612226251 032655 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<BIDATxœíš=kA†Ÿ#Á XEP/"X) ‚ T,,,líô7(b#V–’`kç°H¡ØZ(Á¤°0ÅŒ`¡ü:Þ"ˆ3ÎÎîæ½ñž¦ÈìÝsÞy˜ÌÞ…kîÎ8³E@MPPÔÔ„u5!@@MPP3ÑU!3›NgýÀ˜¾o€à.0ïî»êÛwo=€sÀ;À Æ7`˜ì¢wëì->Ü.\øŸc˜Ý´€3ÀëÊÅ¯ß ×ÛT€ƒÀZËůUl¸ b†‡Ý`¶ÑyÖ€#îþ¼ÃšEÔ<®‘_ü}`x¼v3À1à|¢ç6à–™p÷™êi¸õ§ùýû·mü¸ðûg€…Äý\é3¸™þ8ZXc;°œ¨³<ê^%‚Ï5¬s:³ o¤€â¯Âf¶Ø“¸|£´€»ß%.oR«-MÞ‰ù—î¾ZÑûab~oE­jºð´²÷ãÄ|j—õB[óŸ+{JÌOVÖ«"^‡sÍlþy(ñ±fvª¢wI½÷î¾XQ»œÌ£êð•î¾ï׎;}>“ïföØÕêVL»ûB…sgÀ¨,`w_…Çþ êjB€:€š  &¨¨ êjB€:€š  &¨¨ êjB€:€š  &¨¨É x¶a)òü–ú*žp½„U઻¯ôÕ ñ%ÿ7â PPÔÔŒ½€_£û[Ú…îIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/action-icons/guac-next-page.png0100664 0001751 0001751 00000001162 12440135707 032626 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<ïIDATxœí›=N1FŸ5g!% Ôi(8 }¨øK9-à§i(’)DY=3Ê~’Ëû½õ:Š×›D„MΖõ¬Ó °€u:Ö°N1)¥“”Ò~©úZ)" ¥4†ÀлusøK`èã\‚ª€ø&}àÚ«5Kà›ãU‚ˆd7`|²¢žFŸZ­&¼K )çÏPJéxvÖ¼tœŠÈkëΕ’µˆÈ p|­yi³&ôrúWIå5`±M0~ô • [, ý‚í%õ‹#—cIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/action-icons/guac-monitor-add.png0100664 0001751 0001751 00000001060 12471273273 033155 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞbKGDÿÿÿ ½§“ pHYs × ×B(›xtIMEÝ ) ÿl!¿½IDATxÚíš1,A†¿D( "$4‰\\§Ö*(ô¢Ôˆè% ŽJ¯Q)ŽBÉÊ+h4§9ÍEAr{r…U\®Ü gvv&û¿j“—Ù™ývÞûçÍLEy¶>rn oë÷h¬SÀpŒ¯ ¼õòÒÀ#,1¾G ¤¿eð!Á7ŒÆøZÀKBÛµ¸u‚kÒÌðªp¼¾Jð­Ó1¾ ’Ð6T9¬€X6 ÇR.€¦m;À8ðCŽðn³~ŠÍÊ}àÔf(:–%›I¤ÉUÌîÔŒË>éìÃ5RèwÏf¢ë5ª)}<Àµ9 Í21ò€T@@@@@ Ÿ&RìwÒ‡ý€%฾ ö9¸ ˆÜ;=¶•À€/ÇÆÔ²  ì€KÛ*p ë‡DƒÎå­mè>f  mXnÿ —ö›[b[À®¡Õ[8±çÿ™³@ ³×éšÀ ðéÃ: €ù»„#Àœ/ ¡;:wðLZxö%ºV æ€{:û6]\Œe·IEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/action-icons/guac-config.png0100664 0001751 0001751 00000002316 12471273273 032212 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞbKGDÿÿÿ ½§“ pHYs × ×B(›xtIMEÝ *ªðÁ[IDATxÚí›ÏkTWÇ?J¦!4."d”‚›@‹ŠÁ»1)Ý™:‹.ÜèÂ`þ!nº d™¥;ÛBܤ«fá"¤è.R#µIA¡DËèÂØ„˜‘sÃpßÜïÝäef¾póÞ¹çžsÞ½÷ü¸wzD„NFG×n€/aÌ 'á«íl€2ðÈBsxÜ®KàVF4™¡çÝ`?ðøÊB÷8lµÛ ¸æ 8Ÿsåp>Æ(ï4 Iƒ·Às`CÀI ~ÎÇSòßYç‡ÀJÏ¿iœ¿×bF–uŒ›Æxèª|ˆœôtaÛ"2#".j@ûn{Ž9é3ޝPC"²ç(È9g4¶SÊË{ ±ˆSóÿS?oü¦µ¯,tÇ´µÂ+å5ï0î3•1Z0ük ræ ïNkèz ¸ ëóÀ`ECà¿úß®[‚¥¯u³Ž²L9Lû>C¿^™‘šÃ4®)m¯OŸÃr˜Šµ,±ývš¿ "«9Àªö5í ۖܡËÀ3 Êï¦H„vŒ0céW‰a€ÅV ®®7ðË›fB¯ÁEV[ôYÌÊ ôÀ2p¥Ý°Ùôì'ÍÜÒâœòjĦޙ„+*ó„5rM°LYDî‰È¦ãWmêÚqÃsEMy6Ž1êØwSu)».O᪆Mç¶dÛ†M¹êÉcÄe ôxNÑç†ØþR„,¯™çð—oƒ™K vIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/action-icons/guac-last-page.png0100664 0001751 0001751 00000001303 12440135707 032610 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<@IDATxœí›MNAFßD,B$–Hœ »\Â"~®ÂØäQ.B¶’ÀàHì°Bª,pK(xÆÝ]5ÓSíù¤YÙm×{þW×4"Â*ç]éJgPº€Ò™”. tÖR4M³ |‘«êI­eøq×{9^x‹ˆDÀ&p <³”µ}À ÇmÛcDæ¯üð øœ6M3‹]?ÖD ø>d $,Ð$l[6T:,Y~y•Ð* >Ä­„®wÀwâàC\JèpÜ$>ž; ­DäØ&_ÂgMaC¥óKP)á§ Kk—u"T³„èSa ;‰ëIÒßá%$÷Þ3B Y ‘š$dw„ $ìæ>·eT-1¥„c î z—`Òõ,Á¬+ìU‚i[Ü@ÂËzbb¾/ ”p2´„^6F…(FÀ¶çþÌ;eìV`âŸõ· èÓJh€#àHŒ¬Ks¡Á–¡—/`ÝXfªéCHá³€«¤@¿çõõQ/B¤ ocº´Û.¬ONA/pÃPä.)‹@Y@€ @¨&@£ÀvVòÎÖ€3àøòØæ,pbe§˜dö.¯€C­Ð7Ö§^h€¶1€›ÐYà˜öJž]†w”¡³ÀïïÉ’žÓíŸÊ7Ì,\‹ÏðlIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/action-icons/guac-hide-pass.png0100664 0001751 0001751 00000001321 12612226251 032604 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<NIDATxœí›=kTQ†Ÿ‘h¬"¨‹VŠ‚C *)Ò Zø ”`¡XYJ~€­ö)"¶"Šরp!Š"F°PF‹lž³7'{}oØyàÀrî™w^Îý8ÖÜaf‡Z€š0@-@͈Z@ 3Î3À Œ?OÀ°Ì»û·âBîÞ¸\>^a¬sÀhQ-u³îWl|ãh“ÛÖ`øXØüúÕp°mep XÙbóëÇÕªµ­+@F÷f÷˜`Ú`ÜÝßö;± O[ä› Ì/wÀ^`8\¢w»{fvÆÝÿd«‹—þIÖ®Û^Ëø+p¹Oüð*ïÀõF߀» á¿©Š9öDžNÓ ø>·É<ç3«àD.Vö*lf‡ƒ‰Ãw6“ËÝχOçb•{Vbþ½»/ä{–˜?” j¢¯ ó½ḨV 5`WbþGa¾ï‰ùÑ\ÐÐo‡Ãµ5a€Z€š0@-@M  &ûAÄÌZÀÑšjOÌï3³sÝß_ܽ]Sý52[ÌkÀ/÷®t<¬¸%¾˜ˆPº¾ìÜŠ¹bÆÌÆëJž»ö×U´€Îy \é1ßÉ5á£è@p÷}šíÅÐ?µ5a€Z€š0@-@M  & P P¨¨ ÔÔ„jjµ5a€Z€š0@-@M9öæ¿©ÈóX¬+y΀Yô&,7Ý}©®ò?L¨‰{€Z€š0@-@ÍÐðÝåë° Í:IEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/action-icons/guac-user-add.png0100664 0001751 0001751 00000001452 12471273273 032451 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞbKGDÿÿÿ ½§“ pHYs × ×B(›xtIMEÝ 3 ¦"~Q·IDATxÚíÚ1h^UÀñ_b]©ÄªC µV£E#†KK‡¶‹ CA;8êPKé u*¸ Ž"êÒ¥¡…v©µ”I¥1Z!Á@i)DM4Öð‡¼¡¾ôûÞ—÷^¿{àNçÝwÏýsî¹çÜ{»"B'K·— H:[¶UÈÖè©£»‡ÛÍü´«BÛàÞ¨£û¯¤%$ @T{œi Âãutÿ`¾A߉zyBٴ˘XLK H€ äåð7 tãxªŽî\nÐ÷ïT§%°µK`£ÀMÌbõa0cÙÄû7èÖp q+åÉ=#ZmÛ#âóؼ,DÄ&Æ™jðÏ™fío5ìÃÏx÷ú<“EìOÐUå< g1ØLŠãx¯Êåð¼Õâø+ÁBÕ<àH“‡^|QävܬÌâ¥í8„KUñ€¼˜³ãUZ#x$g;ƪ`´ vT À`ìxº¨Ê´?µÁŽ_²t¹~hƒÓUZsXîd5\Íц®T-ü ÇúþÓ,T ÀNå0þ|Nÿ)¤êÆwØß‚ëïÇõª–Ã5Lâ«&ú.âpÑ“G.'B"b2"îlòD賈x,§q[ny‹÷áí,­à ™‡-eÛÜ´õ3ÁkJ$íºÀž Ê,~ÿJ(­æßÏY¿¬ÅxÒúƒÆþ ñe¿ß׿³zw«æÃx¯c/maüU|oq7Ê_Žˆsñ_´GÖ"âëˆØµ•Ap³žˆˆ?ckd)"&Ê´ |Œmíþ2Þ±~!Zh 8Š/ ŠO¿âUüUT&Ø‹ ÐÏâý"SáƒØ]ð6ýZ‘†K§<_$€¾è+@­jE(Ãã¡H$ @Qç—q²`mZ÷?¿Ä܃Þh¤‘IEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/action-icons/guac-logout-dark.png0100664 0001751 0001751 00000002010 12612226251 033153 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞbKGDÿÿÿ ½§“ pHYs × ×B(›xtIMEß )æn•IDATxÚí›_hOaÇ?Ïö³á‚0!#äÂ’²›5Š[þÊ•äJÜøSäO­6F¸™Ú"QâÂ…¦™MXåF$J)‹±ÉšÌÌã⼫u:Û~ó;¿ßùózê\üÞóž÷=ï÷<ïó|Ÿç}~¢ªØ, )V’¡y{€+ªú! TÀÕ©*A^YÀ®µo[Ð_? ˜àü3ÀÕò¶o¶°xb3ï€eÀi㮬´¿@ðÅf#x (Zlö M®þØêûJ`5Ðn3¸g¼Äm›‰Ðg`-pÈK+™ '@›ÍT¸Xܰ9è6{LŒom0T ÔØ 6Ú@Æ#IÉ‘<+ó"2¸,µ™4R'ÑÅç{³0Õ±\|¡â³ÂhÓ½øà¡_‹"²¸ƒÏiüDD¿8dÑ¥NDº“ª\U?F 9¡Û¼$‡Ë‰ŒˆHpØÎy!]|.pØv*<˜ok>àYîñ\3°€ÑÕ\éü8‚5 sÜãÿ« (ôh[“0õ÷먑öÜàcœ\åHòÃ/#8TØ[œU½ "À5`œG—*Um̤ h~y´g§mŸªÖ-ë ƒü„sRä–Ö´+ÕV x/PìÞO ayžv‹­ú (^é†ß½À0ÞaðÀŒ³&•*±HŠªve¤x&‘ ¢ª?Ed“Ñû0 ô“BÇÿ*1¶©¶ÐïjÛœÁù»Â€ÛŸÖ{34ÿÕÀsÀ"Cdr]÷nÛ¯£ä¯ýî<úõ—T5øâ+C*¶ATÞêù¯DèuÐåðI!U½ˆsÆïÞ“ùÀ}àpl=†‹^Î6—64Ób©ƒÀh3ÙžSqAð §f0¾<@U«êA èpÝžn4¡:±ÐDhˆF“âjñxæ(NÍ`~l0 W&»Ìl‰ŠØ0l¨j¥±î2Ù)@=pKq×l‰&2µçØzn¤Ý`’™Á)“íóp•½‘vƒI¦ªzX鑜̉íðâ‘ÙõÃtû[ ªº؇w™lCØ¿þ=."Å&¼\¬P¨ª/¬À€0(fTõJØ5à/WMÆè^ª–IEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/action-icons/guac-delete.png0100664 0001751 0001751 00000001143 12440135707 032177 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<àIDATxœí›ÁmÃ0 E¿zÎH^ èDÀÇLÓ ºQ‹^ÙCU (âÆ¢>õ%Ët Dñ='MKÉÌ0s<© PÇ)@]€:NÞ‰)¥5¥´0‹qÖ±¤”Vw3+n À;€Å“ƒ1,¹ps娀7¥„?ðæ•P /‘°ï’À€o*á|±| ;á‹$0áC%Âï–À†‘à„ß%!ž*¡þ¡„­E×Ê)Hð?c-À\Ø%¡U òÔkwSˆj;®†Bx/_IünѪà‹Dª„/ á3 ¼K@€¼[@gêî6½;‘Pý¼Q%@,ó°U›@$ö¸MÐX·×ÀJÔH½ÛD,!¦ÕÆNøKëÇr®fëôïÏŸÀ ðýÿ 6€‘0<]ˆðT £ÂÓ$Œ O‘0:|µ5üX-12üXMÑÈB•äðj ]À+%t¯Z³›Bdß:uj [ ‘g—ÏJâÀÕÌÞJ'æ9ל£6.øfº»ÐÖ˜w“AÂøÛä*$g£¤CÂñ¶ÊH8îf鎿]þ ó˜¸#aø#3)'*Ž|PéÕ79ÌÈ·žÍìÅ5ß+à(1ý»ÁS€ºuL/à õ»û"½X"IEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/action-icons/guac-key-dark.png0100664 0001751 0001751 00000001330 12612226251 032436 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞbKGDÿÿÿ ½§“ pHYs × ×B(›xtIMEß )õ·—&eIDATxÚ훿kAÇ?/wjb¡‘!ø«ý,DV ZY¨!*¶VÚ)XXYˆ$ÑB‚µ ÁZñHT°PQÔ€Áók±#9Ö…ìMöö̓ᮘÙyóÙÙ·ß7;c’ðl}8· H€ plÍn^ÜÌšÀ`X¬}6Âo3Ü„àg(ÿ^Hšï*I¥ 8¼~ZBi/;À²}•DÙƒß ´–8è¼2 ¬)Óç²cÀA`S'ì1àd•cÀÞ‚õž§À°Øl)Ðv?p¥’1x_`_ú:´] Ü.ÐþS™>[™é°™}†rª<—´=§}x ç\㫤u½ªýg6¶{uBCêô×À>3ÎyÑÚ(Á¶x`f—ƒ^ønÂ`ÐgƒbŒf±ƒ`ÖÓA0eƒ @$ @P_s|ž­2€‰®U9˜ §‚¶ïÔ_£ÀuÚd ª‹­\nV6(+l^?)i¬vAPÒ,0íý-0íÆ]03®VM{Ätæ°Û³\é] O”­âz €¤à‚÷dèðÌ-I¿ûžuÀpÆó#p)$K.…Ð6àˆç xš ®@ÅthÆ»šÞxB Ék!tøìY}'[×s; ÛçV î"ÛççvœsÍl3pØó[à8Å>Šüˆ™£”0øq²ï‡y[a§€h~ÅêèŸóG»À·EŠí-çÑY3[E¶K¼%éÕ²øÎ;·À;€?QAl¯8&IEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/action-icons/guac-first-page.png0100664 0001751 0001751 00000001262 12440135707 033000 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î</IDATxœíÛ=NÜPÅñÿ(’tHl‚¤ãS¶’%Ðd tYE€´Ÿ+€.+@¢$“ 餱‘!cãçŒß½|$7c½™w~òÌØ3Ï&‰·œwÞðÎà=ïLÞðÎRÛ3û ,÷xŽI÷‹›Ò°˜Ù'à—¤»¤’ænÀ- ÛjÛsäÚ€u`\+)c‹ ˜Ù:ðø¬?Íl¥ïø¢å?4NB(ÀÌ6ø·|ÞETåO˜_¾N/„âz–¯ó"BQ‰å묇m;‹XàøÒ¶³3Ûdxù®“£ðUùcF(ÁÆ.r”‡ f¶E†òÐq5è•Fù÷‰C“ËC°# wyàQ‚˜Ù6å!@Uþ‡òà à]"”'€(åÁÀÌvR2T忤F*úá«\¼é—¯¡$Ð×Iñå‹aÀ…B}ø¤0 8EÛøIq9ÀN—ò¢°x×;€ÇI#`°î/Ïôj¢­IÚÀv`~1)WZû€e-…%µ“ÔÛË~€¤]-ó»’j%u*àûúÌtÁ­ü4ÒEæŒ Mø½}PäÙ…\cÀ€ˆkc¦…Æz‚NôŸÒà%àYl}(ÙÊÞ¡Eú¢vÏC@ëˆûksíEäNŸ³ÔæÅBÛe¯bföÔ±FC‘cVÁ@×ÃrHƒ€¹À=àŠ,×K¹n¹¢èfA¬)„à0ámû€§’ŽÏ[’ ’F»Àò6,ÖBàŠ¤[•ß‘D1"ó+€›Àîr·CÀ—y@Ò` è1íI| ¨%àAÌæ#âÍ/®Ci/‡‹s0¿¸ ¬Í]?@Ò àI ÌwDZK*«n)ÑG`M$ÅмK‘ù@߀¡–Géšô8t4è¬Tp–½ Ö¯2d>Ðg`W(à 0•Aóf€³PîƒüP)..gØxµîË‚>Kð»³A˜ÁϬüùþ/1ÝnzpDZ&»Ùÿµ9¼Æ3ÜÂÝ¢­,…‡ðëhÀÆp ߪö-àÍÃ!«ò+°3çuOñ${´{±;°1Gß½¸T˜Ç!„"ËûÐÚ·ºþÒ·/„p3GÿOEú\t ø€&í/°¹I{ï°ºÉ5_±²]uÀãí³¸×ÉBh ç4Ú±vµx¼û±»“”à|[¸ˆ |Ì¡?Ó§3ÅXš•‹°¶‚i5˜$ @$ `ªŸ'« `´Wª¼CŸÆ†èÐ?þ/Ï&é,~Í«›ÀU\«òZ •­Ã›à¯ãH'ÁIÜŽ}˜ŽÀ Ç  †ËUÓe:sÛcV‚Kc—£E«¸v0ƒs±/†nàyÌæp?fƒ83€ Ùb)J›p0æ xBw Êth:»L#K|¯ÃZ:;¹%±ø ˜'úé¨ÁštIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/action-icons/guac-config-dark.png0100664 0001751 0001751 00000001706 12612226251 033122 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞbKGDÿÿÿ ½§“ pHYs × ×B(›xtIMEß )+É´s÷SIDATxÚ훿KAÇ?³E$Z\À…˜€ÄØ“Z°´ñ_üËûBÒ)ØX^mH:%D” –Z$)bâI<¸Ióvof÷v6îí »ï½yï»óã;oæ”Öš*—ˆŠ—@ÕxTtƒJ©9@%¼ÖZëf¡þ9 *¥O±—ZëÏý:Þæ$S¾ ”.ÇÑßÀ„ÖúºßzÀªCðˆÌj?5O²½­u¦ ŒËÀ`™X€NY¢u±?(>ŒgŽ£‡àOÄÑ?Àž|µ1y_6€ó ›õ\lÕÄö˜´µ'mkñe¼ŒàÍÚŽ€›7ëØî$¼ÏBžÁ?„š„~ >iVçÀL èýŒøš/RJ ?áüR& àð¤Gû×2aþÍ}”™7K·üÔ%˲‰L]t²´µçs\KéL ØF2¬6#¢ÛJÙæšOƺ,Cf=¦²”{mN‰-—6;w\Ä'8rpdr°5 Œ:È ‰M[»G¾y@ÍälwÑŸÖà›|±ŽüÞ‘w³]ô·ÈRÍ'Ý~(FoØÚ_±-² =Á66|¡ÈÂí[qc˜šfó&0Ÿ0'´,{‡È+‡·‚¿íÕÝ&€°eÑ[ñ@òÎÄtûfÔ¶iY"»ñ„F.ë{ì[œ¬ÇènæÈï7cì×-:ûâûpj€EàpåèàRÌlß΀¶¹:ctѽ’Xæ2ÐÜȰ±îa—·3)§¥Ës.»A•róq¬µîÏ<ìòŒ=Løš69í#)zóì…âl^>„¬ð…±m¦=0-¶mà‡ÃÑ¢˜4Ææ/à̃¯gb;±í€”CÄÙœð@ÚçJ)ÓÎŒ¹&’Zªs §”X¥‰P Âa3¶Ã!!Rb!)Òâá`$Uäp4§øúË%¸rW—}Üùœ– Çq*¾æO°Ò—¤Â5¹pQ²?¯Êy]~xå(þQkýº,Ya×òÞ“lo&üa¢ "í:ˆîüÿ8ú“Lù†À½¡PÝ¿ÍõëÙ` Pâò?¹LÄIÒêIEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/images/action-icons/guac-prev-page.png0100664 0001751 0001751 00000001210 12440135707 032616 0ustar00mjumpermjumper0000000 0000000 ‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<IDATxœí›;NÃ@†¿AT<ïÀ)Žj Η†>ˆ÷ë&\‘†ÂŒD€àÙÈ/¹qäÙý>×J´Uå?gÆ{Þ™ ðž€w¦¼'àðDdAD¶JÕ-@D€!0‘ÝcÌ–(j‘üF{j "¨êå8!; …?äš›5°î„p:ðëŸ|l/AUÃÀ"p è7Ç3°k2¦7ô/àM%H„/C"²H³à}Öö_å XQջߎíþháµ /û}àßG€¦í¯øyÛÿ5 ¼›€(ð.ZøëðÕDƒ¯* "|5ÀRDø*"ÃÐÂßD…/* |1Yà‹Èo. ¼©€þ6¼™€¬ð&€å¬ð½d¾ó¯Gß_…GÀcϾ1xæ€c’vM‘Äì %•`[,¡û‚„“,ÊM$¡\á$ÊO ¡ü„Ó¨ê´ÌG•PE@d ÕD•PU@D Õt$œEà" ’7Q$¸ ˆ !Ê&©yšMR›^Ú{“Tˆ’ªúl\–“Ô˜Çᜊk@ˆxMÛ ;|Ý #`O6M‡ ª÷Œ—` À —Óæð@Œ·À¸tþ3°Jx.Þ$¬ªêQ‘úÑ”NÈ5 f¦¼'à©ï xçËe<#hü¨\IEND®B`‚guacamole-client-0.9.9/guacamole/src/main/webapp/META-INF/context.xml0100664 0001751 0001751 00000000132 12635052306 027021 0ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/webapp/generated/templates-main/templates.js0100664 0001751 0001751 00000371040 12645510306 032676 0ustar00mjumpermjumper0000000 0000000 angular.module('templates-main', ['app/client/templates/client.html', 'app/client/templates/file.html', 'app/client/templates/guacClient.html', 'app/client/templates/guacFileBrowser.html', 'app/client/templates/guacFileTransfer.html', 'app/client/templates/guacFileTransferManager.html', 'app/client/templates/guacThumbnail.html', 'app/client/templates/guacViewport.html', 'app/element/templates/blank.html', 'app/form/templates/checkboxField.html', 'app/form/templates/dateField.html', 'app/form/templates/form.html', 'app/form/templates/formField.html', 'app/form/templates/numberField.html', 'app/form/templates/passwordField.html', 'app/form/templates/selectField.html', 'app/form/templates/textAreaField.html', 'app/form/templates/textField.html', 'app/form/templates/timeField.html', 'app/form/templates/timeZoneField.html', 'app/groupList/templates/guacGroupList.html', 'app/groupList/templates/guacGroupListFilter.html', 'app/home/templates/connection.html', 'app/home/templates/connectionGroup.html', 'app/home/templates/guacRecentConnections.html', 'app/home/templates/home.html', 'app/list/templates/guacFilter.html', 'app/list/templates/guacPager.html', 'app/login/templates/login.html', 'app/manage/templates/connectionGroupPermission.html', 'app/manage/templates/connectionPermission.html', 'app/manage/templates/locationChooser.html', 'app/manage/templates/locationChooserConnectionGroup.html', 'app/manage/templates/manageConnection.html', 'app/manage/templates/manageConnectionGroup.html', 'app/manage/templates/manageUser.html', 'app/navigation/templates/guacPageList.html', 'app/navigation/templates/guacUserMenu.html', 'app/notification/templates/guacNotification.html', 'app/osk/templates/guacOsk.html', 'app/settings/templates/connection.html', 'app/settings/templates/connectionGroup.html', 'app/settings/templates/settings.html', 'app/settings/templates/settingsConnectionHistory.html', 'app/settings/templates/settingsConnections.html', 'app/settings/templates/settingsPreferences.html', 'app/settings/templates/settingsSessions.html', 'app/settings/templates/settingsUsers.html', 'app/textInput/templates/guacKey.html', 'app/textInput/templates/guacTextInput.html']); angular.module('app/client/templates/client.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/client/templates/client.html', "\n" + "\n" + "\n" + "\n" + " \n" + "

\n" + "
\n" + "\n" + " \n" + "
\n" + "\n" + " \n" + " \n" + "\n" + "
\n" + "\n" + " \n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + "
\n" + "\n" + "
\n" + "\n" + "
\n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + "
\n" + "\n" + " \n" + "
\n" + "
\n" + "\n" + " \n" + "
\n" + "

{{client.name}}

\n" + " \n" + "
\n" + "\n" + " \n" + "
\n" + "\n" + " \n" + "
\n" + "

{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}

\n" + "
\n" + "

{{'CLIENT.HELP_CLIPBOARD' | translate}}

\n" + " \n" + "
\n" + "
\n" + "\n" + " \n" + "
\n" + "

{{'CLIENT.SECTION_HEADER_DEVICES' | translate}}

\n" + "
\n" + "
\n" + " {{filesystem.name}}\n" + "
\n" + "
\n" + "
\n" + "\n" + " \n" + "
\n" + "

{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}

\n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + "

\n" + "
\n" + "\n" + " \n" + "
\n" + "
\n" + " \n" + "

\n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + "

\n" + "
\n" + "\n" + "
\n" + "
\n" + "\n" + " \n" + "
\n" + "

{{'CLIENT.SECTION_HEADER_MOUSE_MODE' | translate}}

\n" + "
\n" + "

{{'CLIENT.HELP_MOUSE_MODE' | translate}}

\n" + "\n" + " \n" + "
\n" + " \n" + "
\n" + " \n" + "

\n" + "
\n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + "
\n" + " \n" + "

\n" + "
\n" + "
\n" + "\n" + "
\n" + "
\n" + "\n" + " \n" + "
\n" + "

{{'CLIENT.SECTION_HEADER_DISPLAY' | translate}}

\n" + "
\n" + "
\n" + "
\"-\"/
\n" + "
{{formattedScale()}}%
\n" + "
\"+\"/
\n" + "
\n" + "
\n" + "
\n" + "
\n" + "\n" + "
\n" + "\n" + "
\n" + "
\n" + "\n" + " \n" + "
\n" + "
\n" + "\n" + " \n" + "
\n" + "

{{filesystemMenuContents.name}}

\n" + " \n" + " \n" + "
\n" + "\n" + " \n" + "
{{file.name}}
\n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + "
\n" + "\n" + "
\n" + "
\n" + "\n" + ""); }]); angular.module('app/client/templates/file.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/client/templates/file.html', "
\n" + " \n" + "\n" + " \n" + "
\n" + "
\n" + " {{::name}}\n" + "
\n" + "\n" + "
"); }]); angular.module('app/client/templates/guacClient.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/client/templates/guacClient.html', "
\n" + " \n" + "\n" + " \n" + "
\n" + "\n" + "
\n" + "
\n" + "
\n" + "
\n" + "\n" + "
\n" + "\n" + "
"); }]); angular.module('app/client/templates/guacFileBrowser.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/client/templates/guacFileBrowser.html', "
\n" + " \n" + "\n" + " \n" + "
\n" + "\n" + "
"); }]); angular.module('app/client/templates/guacFileTransfer.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/client/templates/guacFileTransfer.html', "
\n" + " \n" + "\n" + " \n" + "
\n" + "\n" + " \n" + "
\n" + "
\n" + " {{transfer.filename}}\n" + "
\n" + "\n" + " \n" + "

{{getErrorText() | translate}}

\n" + "\n" + "
\n" + "\n" + " \n" + "
\n" + "\n" + "
"); }]); angular.module('app/client/templates/guacFileTransferManager.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/client/templates/guacFileTransferManager.html', "
\n" + " \n" + "\n" + " \n" + "
\n" + "

{{'CLIENT.SECTION_HEADER_FILE_TRANSFERS' | translate}}

\n" + " \n" + "
\n" + "\n" + " \n" + "
\n" + "
\n" + " \n" + " \n" + " \n" + "
\n" + "
\n" + "\n" + "
"); }]); angular.module('app/client/templates/guacThumbnail.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/client/templates/guacThumbnail.html', "
\n" + " \n" + "\n" + " \n" + "
\n" + "
\n" + "\n" + " \n" + " \"\"\n" + "\n" + "
"); }]); angular.module('app/client/templates/guacViewport.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/client/templates/guacViewport.html', "
\n" + " \n" + "
"); }]); angular.module('app/element/templates/blank.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/element/templates/blank.html', "\n" + "\n" + " \n" + " \n" + " _\n" + " \n" + " \n" + " \n" + ""); }]); angular.module('app/form/templates/checkboxField.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/form/templates/checkboxField.html', ""); }]); angular.module('app/form/templates/dateField.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/form/templates/dateField.html', "
\n" + " \n" + "
"); }]); angular.module('app/form/templates/form.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/form/templates/form.html', "
\n" + "
\n" + " \n" + "\n" + " \n" + "

{{getSectionHeader(form) | translate}}

\n" + "\n" + " \n" + "
\n" + " \n" + "
\n" + "\n" + "
\n" + "
"); }]); angular.module('app/form/templates/formField.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/form/templates/formField.html', ""); }]); angular.module('app/form/templates/numberField.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/form/templates/numberField.html', ""); }]); angular.module('app/form/templates/passwordField.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/form/templates/passwordField.html', "
\n" + " \n" + "
\n" + "
"); }]); angular.module('app/form/templates/selectField.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/form/templates/selectField.html', ""); }]); angular.module('app/form/templates/textAreaField.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/form/templates/textAreaField.html', ""); }]); angular.module('app/form/templates/textField.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/form/templates/textField.html', ""); }]); angular.module('app/form/templates/timeField.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/form/templates/timeField.html', "
\n" + " \n" + "
"); }]); angular.module('app/form/templates/timeZoneField.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/form/templates/timeZoneField.html', "
\n" + "\n" + " \n" + " \n" + "\n" + " \n" + " \n" + "\n" + "
"); }]); angular.module('app/groupList/templates/guacGroupList.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/groupList/templates/guacGroupList.html', "
\n" + " \n" + "\n" + " \n" + "\n" + " \n" + "
\n" + "
\n" + "
\n" + "\n" + " \n" + " \n" + "\n" + "
"); }]); angular.module('app/groupList/templates/guacGroupListFilter.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/groupList/templates/guacGroupListFilter.html', "
\n" + " \n" + "\n" + " \n" + " \n" + "\n" + "
"); }]); angular.module('app/home/templates/connection.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/home/templates/connection.html', "\n" + " \n" + "\n" + "
\n" + "\n" + " \n" + "
\n" + "
\n" + "
\n" + "\n" + " \n" + " {{item.name}}\n" + " \n" + " \n" + " \n" + "\n" + "
\n" + "
"); }]); angular.module('app/home/templates/connectionGroup.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/home/templates/connectionGroup.html', "\n" + " \n" + "\n" + " {{item.name}}\n" + " {{item.name}}\n" + ""); }]); angular.module('app/home/templates/guacRecentConnections.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/home/templates/guacRecentConnections.html', "
\n" + " \n" + "\n" + " \n" + "

{{'HOME.INFO_NO_RECENT_CONNECTIONS' | translate}}

\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n" + "
"); }]); angular.module('app/home/templates/home.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/home/templates/home.html', "\n" + "\n" + "
\n" + "\n" + "
\n" + "\n" + " \n" + "
\n" + "

{{'HOME.SECTION_HEADER_RECENT_CONNECTIONS' | translate}}

\n" + " \n" + "
\n" + "
\n" + " \n" + "
\n" + "\n" + " \n" + "
\n" + "

{{'HOME.SECTION_HEADER_ALL_CONNECTIONS' | translate}}

\n" + " \n" + "
\n" + "
\n" + " \n" + "
\n" + "\n" + "
\n" + "\n" + "
"); }]); angular.module('app/list/templates/guacFilter.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/list/templates/guacFilter.html', "
\n" + " \n" + "\n" + " \n" + " \n" + "\n" + "
"); }]); angular.module('app/list/templates/guacPager.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/list/templates/guacPager.html', "
1\">\n" + " \n" + "\n" + " \n" + "
\n" + "
\n" + "\n" + " \n" + "
...
\n" + " \n" + " \n" + "
    \n" + "
  • {{pageNumber}}
  • \n" + "
\n" + "\n" + " \n" + "
...
\n" + "\n" + " \n" + "
\n" + "
\n" + "\n" + "
"); }]); angular.module('app/login/templates/login.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/login/templates/login.html', "
\n" + " \n" + "\n" + " \n" + "

{{loginError | translate}}

\n" + "\n" + "
\n" + "\n" + "
\n" + "\n" + "
\n" + "\n" + " \n" + "
\n" + "
{{'APP.NAME' | translate}}
\n" + "\n" + " \n" + "

{{helpText | translate}}

\n" + "\n" + " \n" + "
\n" + " \n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + " \n" + "
\n" + "\n" + "
\n" + "\n" + "
\n" + "\n" + "
\n" + "\n" + "
"); }]); angular.module('app/manage/templates/connectionGroupPermission.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/manage/templates/connectionGroupPermission.html', "
\n" + " \n" + "\n" + " \n" + "\n" + " {{item.name}}\n" + "
"); }]); angular.module('app/manage/templates/connectionPermission.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/manage/templates/connectionPermission.html', "
\n" + " \n" + "\n" + " \n" + "
\n" + "
\n" + "
\n" + "\n" + " \n" + " \n" + "\n" + " \n" + " {{item.name}}\n" + "\n" + "
"); }]); angular.module('app/manage/templates/locationChooser.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/manage/templates/locationChooser.html', "
\n" + " \n" + "\n" + " \n" + "
{{chosenConnectionGroupName}}
\n" + "\n" + " \n" + "
\n" + " \n" + "
\n" + "\n" + "
"); }]); angular.module('app/manage/templates/locationChooserConnectionGroup.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/manage/templates/locationChooserConnectionGroup.html', "\n" + " \n" + "\n" + " {{item.name}}\n" + ""); }]); angular.module('app/manage/templates/manageConnection.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/manage/templates/manageConnection.html', "\n" + "\n" + "
\n" + "\n" + " \n" + "
\n" + "

{{'MANAGE_CONNECTION.SECTION_HEADER_EDIT_CONNECTION' | translate}}

\n" + " \n" + "
\n" + "
\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
{{'MANAGE_CONNECTION.FIELD_HEADER_NAME' | translate}}
{{'MANAGE_CONNECTION.FIELD_HEADER_LOCATION' | translate}}\n" + " \n" + "
{{'MANAGE_CONNECTION.FIELD_HEADER_PROTOCOL' | translate}}\n" + " \n" + "
\n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + "
\n" + "\n" + " \n" + "

{{'MANAGE_CONNECTION.SECTION_HEADER_PARAMETERS' | translate}}

\n" + "
\n" + " \n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + " \n" + " \n" + " \n" + "
\n" + "\n" + " \n" + "

{{'MANAGE_CONNECTION.SECTION_HEADER_HISTORY' | translate}}

\n" + "
\n" + "

{{'MANAGE_CONNECTION.INFO_CONNECTION_NOT_USED' | translate}}

\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
{{'MANAGE_CONNECTION.TABLE_HEADER_HISTORY_USERNAME' | translate}}{{'MANAGE_CONNECTION.TABLE_HEADER_HISTORY_START' | translate}}{{'MANAGE_CONNECTION.TABLE_HEADER_HISTORY_DURATION' | translate}}
{{wrapper.entry.username}}{{wrapper.entry.startDate | date:historyDateFormat}}
\n" + "\n" + " \n" + " \n" + "\n" + "
\n" + "\n" + "
"); }]); angular.module('app/manage/templates/manageConnectionGroup.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/manage/templates/manageConnectionGroup.html', "\n" + "\n" + "
\n" + "\n" + " \n" + "
\n" + "

{{'MANAGE_CONNECTION_GROUP.SECTION_HEADER_EDIT_CONNECTION_GROUP' | translate}}

\n" + " \n" + "
\n" + "
\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
{{'MANAGE_CONNECTION_GROUP.FIELD_HEADER_NAME' | translate}}
{{'MANAGE_CONNECTION_GROUP.FIELD_HEADER_LOCATION' | translate}}\n" + " \n" + "
{{'MANAGE_CONNECTION_GROUP.FIELD_HEADER_TYPE' | translate}}\n" + " \n" + "
\n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + " \n" + " \n" + "
\n" + "\n" + "
"); }]); angular.module('app/manage/templates/manageUser.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/manage/templates/manageUser.html', "\n" + "\n" + "
\n" + "\n" + " \n" + "
\n" + "

{{'MANAGE_USER.SECTION_HEADER_EDIT_USER' | translate}}

\n" + " \n" + "
\n" + "
\n" + " \n" + "
\n" + "\n" + " \n" + "
\n" + "

{{'MANAGE_USER.INFO_READ_ONLY' | translate}}

\n" + "
\n" + "\n" + " \n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
{{'MANAGE_USER.FIELD_HEADER_USERNAME' | translate}}\n" + " \n" + " {{user.username}}\n" + "
{{'MANAGE_USER.FIELD_HEADER_PASSWORD' | translate}}
{{'MANAGE_USER.FIELD_HEADER_PASSWORD_AGAIN' | translate}}
\n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + "
\n" + "\n" + " \n" + "
\n" + "

{{'MANAGE_USER.SECTION_HEADER_PERMISSIONS' | translate}}

\n" + "
\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
{{systemPermissionType.label | translate}}
{{'MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD' | translate}}
\n" + "
\n" + "
\n" + "\n" + " \n" + "
\n" + "
\n" + "

{{'MANAGE_USER.SECTION_HEADER_CONNECTIONS' | translate}}

\n" + " \n" + "
\n" + "
\n" + " \n" + "
\n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + " \n" + " \n" + "
\n" + "\n" + "
\n" + "\n" + "
"); }]); angular.module('app/navigation/templates/guacPageList.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/navigation/templates/guacPageList.html', "
\n" + " \n" + "\n" + " \n" + " \n" + "\n" + "
"); }]); angular.module('app/navigation/templates/guacUserMenu.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/navigation/templates/guacUserMenu.html', "
\n" + " \n" + "\n" + "
\n" + "
{{username}}
\n" + "
\n" + " \n" + " \n" + "
\n" + " \n" + " \n" + " \n" + "\n" + " \n" + " \n" + "\n" + " \n" + " \n" + "\n" + "
\n" + "
\n" + "\n" + "
"); }]); angular.module('app/notification/templates/guacNotification.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/notification/templates/guacNotification.html', "
\n" + " \n" + "\n" + " \n" + "
\n" + "
{{notification.title | translate}}
\n" + "
\n" + "\n" + "
\n" + "\n" + " \n" + "

{{notification.text | translate}}

\n" + "\n" + " \n" + "
\n" + "\n" + " \n" + "

\n" + "\n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + "
\n" + "\n" + "
"); }]); angular.module('app/osk/templates/guacOsk.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/osk/templates/guacOsk.html', "
\n" + " \n" + "
"); }]); angular.module('app/settings/templates/connection.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/settings/templates/connection.html', "\n" + " \n" + "\n" + "
\n" + "\n" + " \n" + "
\n" + "
\n" + "
\n" + "\n" + " \n" + " {{item.name}}\n" + "\n" + " \n" + " \n" + " \n" + "
\n" + "
"); }]); angular.module('app/settings/templates/connectionGroup.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/settings/templates/connectionGroup.html', "\n" + " \n" + "\n" + " {{item.name}}\n" + ""); }]); angular.module('app/settings/templates/settings.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/settings/templates/settings.html', "\n" + "\n" + "
\n" + "\n" + "
\n" + "

{{'SETTINGS.SECTION_HEADER_SETTINGS' | translate}}

\n" + " \n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + "
\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n" + "
"); }]); angular.module('app/settings/templates/settingsConnectionHistory.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/settings/templates/settingsConnectionHistory.html', "
\n" + " \n" + "\n" + " \n" + "

{{'SETTINGS_CONNECTION_HISTORY.HELP_CONNECTION_HISTORY' | translate}}

\n" + "\n" + " \n" + "
\n" + " \n" + " \n" + "
\n" + "\n" + " \n" + "
\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
\n" + " {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_USERNAME' | translate}}\n" + " \n" + " {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_STARTDATE' | translate}}\n" + " \n" + " {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_DURATION' | translate}}\n" + " \n" + " {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_CONNECTION_NAME' | translate}}\n" + "
{{historyEntryWrapper.username}}{{historyEntryWrapper.startDate | date : dateFormat}}{{historyEntryWrapper.connectionName}}
\n" + "\n" + " \n" + "

\n" + " {{'SETTINGS_CONNECTION_HISTORY.INFO_NO_HISTORY' | translate}}\n" + "

\n" + "\n" + " \n" + " \n" + "
\n" + "\n" + "
"); }]); angular.module('app/settings/templates/settingsConnections.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/settings/templates/settingsConnections.html', "
\n" + " \n" + "\n" + " \n" + "

{{'SETTINGS_CONNECTIONS.HELP_CONNECTIONS' | translate}}

\n" + "\n" + " \n" + "
\n" + "\n" + " \n" + " \n" + "\n" + " \n" + " \n" + "\n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + "
\n" + "
"); }]); angular.module('app/settings/templates/settingsPreferences.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/settings/templates/settingsPreferences.html', "
\n" + " \n" + "\n" + " \n" + "
\n" + "

{{'SETTINGS_PREFERENCES.HELP_LANGUAGE' | translate}}

\n" + "\n" + " \n" + "
\n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
{{'SETTINGS_PREFERENCES.FIELD_HEADER_LANGUAGE' | translate}}
\n" + "
\n" + "
\n" + " \n" + " \n" + "

{{'SETTINGS_PREFERENCES.SECTION_HEADER_UPDATE_PASSWORD' | translate}}

\n" + "
\n" + "

{{'SETTINGS_PREFERENCES.HELP_UPDATE_PASSWORD' | translate}}

\n" + "\n" + " \n" + "
\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
{{'SETTINGS_PREFERENCES.FIELD_HEADER_PASSWORD_OLD' | translate}}
{{'SETTINGS_PREFERENCES.FIELD_HEADER_PASSWORD_NEW' | translate}}
{{'SETTINGS_PREFERENCES.FIELD_HEADER_PASSWORD_NEW_AGAIN' | translate}}
\n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + "
\n" + "
\n" + "\n" + " \n" + "

{{'SETTINGS_PREFERENCES.SECTION_HEADER_DEFAULT_INPUT_METHOD' | translate}}

\n" + "
\n" + "

{{'SETTINGS_PREFERENCES.HELP_DEFAULT_INPUT_METHOD' | translate}}

\n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + "

\n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + "

\n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + "

\n" + "
\n" + "\n" + "
\n" + "
\n" + "\n" + " \n" + "

{{'SETTINGS_PREFERENCES.SECTION_HEADER_DEFAULT_MOUSE_MODE' | translate}}

\n" + "
\n" + "

{{'SETTINGS_PREFERENCES.HELP_DEFAULT_MOUSE_MODE' | translate}}

\n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + "
\n" + " \n" + "

\n" + "
\n" + "
\n" + "\n" + " \n" + "
\n" + " \n" + "
\n" + " \n" + "

\n" + "
\n" + "
\n" + "\n" + "
\n" + "
\n" + "\n" + "
"); }]); angular.module('app/settings/templates/settingsSessions.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/settings/templates/settingsSessions.html', "
\n" + " \n" + "\n" + " \n" + "

{{'SETTINGS_SESSIONS.HELP_SESSIONS' | translate}}

\n" + "\n" + " \n" + "
\n" + " \n" + "
\n" + "\n" + " \n" + " \n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
\n" + " {{'SETTINGS_SESSIONS.TABLE_HEADER_SESSION_USERNAME' | translate}}\n" + " \n" + " {{'SETTINGS_SESSIONS.TABLE_HEADER_SESSION_STARTDATE' | translate}}\n" + " \n" + " {{'SETTINGS_SESSIONS.TABLE_HEADER_SESSION_REMOTEHOST' | translate}}\n" + " \n" + " {{'SETTINGS_SESSIONS.TABLE_HEADER_SESSION_CONNECTION_NAME' | translate}}\n" + "
\n" + " \n" + " {{wrapper.activeConnection.username}}{{wrapper.startDate}}{{wrapper.activeConnection.remoteHost}}{{wrapper.name}}
\n" + "\n" + " \n" + "

\n" + " {{'SETTINGS_SESSIONS.INFO_NO_SESSIONS' | translate}}\n" + "

\n" + "\n" + " \n" + " \n" + "
"); }]); angular.module('app/settings/templates/settingsUsers.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/settings/templates/settingsUsers.html', "
\n" + " \n" + "\n" + " \n" + "

{{'SETTINGS_USERS.HELP_USERS' | translate}}

\n" + "\n" + "\n" + " \n" + "
\n" + "\n" + " \n" + " \n" + "\n" + " \n" + " \n" + "\n" + "
\n" + "\n" + " \n" + " \n" + "\n" + " \n" + " \n" + "\n" + "
"); }]); angular.module('app/textInput/templates/guacKey.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/textInput/templates/guacKey.html', ""); }]); angular.module('app/textInput/templates/guacTextInput.html', []).run(['$templateCache', function($templateCache) { $templateCache.put('app/textInput/templates/guacTextInput.html', "
\n" + " \n" + "\n" + " \n" + "
{{text}}
\n" + "\n" + "
"); }]); ././@LongLink0100644 0000000 0000000 00000000150 12645510255 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/HTTPTunnelRequest.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/HTTPTunnelRequest.j0100664 0001751 0001751 00000006015 12645510255 035232 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; /** * HTTP-specific implementation of TunnelRequest. * * @author Michael Jumper */ public class HTTPTunnelRequest extends TunnelRequest { /** * A copy of the parameters obtained from the HttpServletRequest used to * construct the HTTPTunnelRequest. */ private final Map> parameterMap = new HashMap>(); /** * Creates a HTTPTunnelRequest which copies and exposes the parameters * from the given HttpServletRequest. * * @param request * The HttpServletRequest to copy parameter values from. */ @SuppressWarnings("unchecked") // getParameterMap() is defined as returning Map public HTTPTunnelRequest(HttpServletRequest request) { // For each parameter for (Map.Entry mapEntry : ((Map) request.getParameterMap()).entrySet()) { // Get parameter name and corresponding values String parameterName = mapEntry.getKey(); List parameterValues = Arrays.asList(mapEntry.getValue()); // Store copy of all values in our own map parameterMap.put( parameterName, new ArrayList(parameterValues) ); } } @Override public String getParameter(String name) { List values = getParameterValues(name); // Return the first value from the list if available if (values != null && !values.isEmpty()) return values.get(0); return null; } @Override public List getParameterValues(String name) { return parameterMap.get(name); } } ././@LongLink0100644 0000000 0000000 00000000150 12612226251 011626 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/EnvironmentModule.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/EnvironmentModule.j0100664 0001751 0001751 00000003651 12612226251 035363 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic; import com.google.inject.AbstractModule; import org.glyptodon.guacamole.environment.Environment; /** * Guice module which binds the base Guacamole server environment. * * @author Michael Jumper */ public class EnvironmentModule extends AbstractModule { /** * The Guacamole server environment. */ private final Environment environment; /** * Creates a new EnvironmentModule which will bind the given environment * for future injection. * * @param environment * The environment to bind. */ public EnvironmentModule(Environment environment) { this.environment = environment; } @Override protected void configure() { // Bind environment bind(Environment.class).toInstance(environment); } } ././@LongLink0100644 0000000 0000000 00000000161 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/ByteArrayResource.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/ByteArrayR0100664 0001751 0001751 00000004202 12612226251 035325 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.resource; import java.io.ByteArrayInputStream; import java.io.InputStream; /** * A resource which contains a defined byte array. * * @author Michael Jumper */ public class ByteArrayResource extends AbstractResource { /** * The bytes contained by this resource. */ private final byte[] bytes; /** * Creates a new ByteArrayResource which provides access to the given byte * array. Changes to the given byte array will affect this resource even * after the resource is created. Changing the byte array while an input * stream from this resource is in use has undefined behavior. * * @param mimetype * The mimetype of the resource. * * @param bytes * The bytes that this resource should contain. */ public ByteArrayResource(String mimetype, byte[] bytes) { super(mimetype); this.bytes = bytes; } @Override public InputStream asStream() { return new ByteArrayInputStream(bytes); } } ././@LongLink0100644 0000000 0000000 00000000157 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/ResourceServlet.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/ResourceSe0100664 0001751 0001751 00000010363 12612226251 035365 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.resource; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Servlet which serves a given resource for all HTTP GET requests. The HEAD * method is correctly supported, and HTTP 304 ("Not Modified") responses will * be properly returned for GET requests depending on the last time the * resource was modified. * * @author Michael Jumper */ public class ResourceServlet extends HttpServlet { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(ResourceServlet.class); /** * The size of the buffer to use when transferring data from the input * stream of a resource to the output stream of a request. */ private static final int BUFFER_SIZE = 10240; /** * The resource to serve for every GET request. */ private final Resource resource; /** * Creates a new ResourceServlet which serves the given Resource for all * HTTP GET requests. * * @param resource * The Resource to serve. */ public ResourceServlet(Resource resource) { this.resource = resource; } @Override protected void doHead(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Set last modified and content type headers response.addDateHeader("Last-Modified", resource.getLastModified()); response.setContentType(resource.getMimeType()); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Get input stream from resource InputStream input = resource.asStream(); // If resource does not exist, return not found if (input == null) { logger.debug("Resource does not exist: \"{}\"", request.getServletPath()); response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } try { // Write headers doHead(request, response); // If not modified since "If-Modified-Since" header, return not modified long ifModifiedSince = request.getDateHeader("If-Modified-Since"); if (resource.getLastModified() - ifModifiedSince < 1000) { logger.debug("Resource not modified: \"{}\"", request.getServletPath()); response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); return; } int length; byte[] buffer = new byte[BUFFER_SIZE]; // Write resource to response body OutputStream output = response.getOutputStream(); while ((length = input.read(buffer)) != -1) output.write(buffer, 0, length); } // Ensure input stream is always closed finally { input.close(); } } } ././@LongLink0100644 0000000 0000000 00000000150 12612226251 011626 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/Resource.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/Resource.j0100664 0001751 0001751 00000004771 12612226251 035333 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.resource; import java.io.InputStream; /** * An arbitrary resource that can be served to a user via HTTP. Resources are * anonymous but have a defined mimetype and corresponding input stream. * * @author Michael Jumper */ public interface Resource { /** * Returns the mimetype of this resource. This function MUST always return * a value. If the type is unknown, return "application/octet-stream". * * @return * The mimetype of this resource. */ String getMimeType(); /** * Returns the time the resource was last modified in milliseconds since * midnight of January 1, 1970 UTC. * * @return * The time the resource was last modified, in milliseconds. */ long getLastModified(); /** * Returns an InputStream which reads the contents of this resource, * starting with the first byte. Reading from the returned InputStream will * not affect reads from other InputStreams returned by other calls to * asStream(). The returned InputStream must be manually closed when no * longer needed. If the resource is unexpectedly unavailable, this will * return null. * * @return * An InputStream which reads the contents of this resource, starting * with the first byte, or null if the resource is unavailable. */ InputStream asStream(); } ././@LongLink0100644 0000000 0000000 00000000166 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/WebApplicationResource.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/WebApplica0100664 0001751 0001751 00000007657 12612226251 035331 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.resource; import java.io.InputStream; import javax.servlet.ServletContext; /** * A resource which is located within the classpath associated with another * class. * * @author Michael Jumper */ public class WebApplicationResource extends AbstractResource { /** * The servlet context to use when reading the resource and, if necessary, * when determining the mimetype of the resource. */ private final ServletContext context; /** * The path of this resource relative to the ServletContext. */ private final String path; /** * Derives a mimetype from the filename within the given path using the * given ServletContext, if possible. * * @param context * The ServletContext to use to derive the mimetype. * * @param path * The path to derive the mimetype from. * * @return * An appropriate mimetype based on the name of the file in the path, * or "application/octet-stream" if no mimetype could be determined. */ private static String getMimeType(ServletContext context, String path) { // If mimetype is known, use defined mimetype String mimetype = context.getMimeType(path); if (mimetype != null) return mimetype; // Otherwise, default to application/octet-stream return "application/octet-stream"; } /** * Creates a new WebApplicationResource which serves the resource at the * given path relative to the given ServletContext. Rather than deriving * the mimetype of the resource from the filename within the path, the * mimetype given is used. * * @param context * The ServletContext to use when reading the resource. * * @param mimetype * The mimetype of the resource. * * @param path * The path of the resource relative to the given ServletContext. */ public WebApplicationResource(ServletContext context, String mimetype, String path) { super(mimetype); this.context = context; this.path = path; } /** * Creates a new WebApplicationResource which serves the resource at the * given path relative to the given ServletContext. The mimetype of the * resource is automatically determined based on the filename within the * path. * * @param context * The ServletContext to use when reading the resource and deriving the * mimetype. * * @param path * The path of the resource relative to the given ServletContext. */ public WebApplicationResource(ServletContext context, String path) { this(context, getMimeType(context, path), path); } @Override public InputStream asStream() { return context.getResourceAsStream(path); } } ././@LongLink0100644 0000000 0000000 00000000160 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/SequenceResource.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/SequenceRe0100664 0001751 0001751 00000012351 12612226251 035344 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.resource; import java.io.InputStream; import java.io.SequenceInputStream; import java.util.Arrays; import java.util.Enumeration; import java.util.Iterator; /** * A resource which is the logical concatenation of other resources. * * @author Michael Jumper */ public class SequenceResource extends AbstractResource { /** * The resources to be concatenated. */ private final Iterable resources; /** * Returns the mimetype of the first resource in the given Iterable, or * "application/octet-stream" if no resources are provided. * * @param resources * The resources from which the mimetype should be retrieved. * * @return * The mimetype of the first resource, or "application/octet-stream" * if no resources were provided. */ private static String getMimeType(Iterable resources) { // If no resources, just assume application/octet-stream Iterator resourceIterator = resources.iterator(); if (!resourceIterator.hasNext()) return "application/octet-stream"; // Return mimetype of first resource return resourceIterator.next().getMimeType(); } /** * Creates a new SequenceResource as the logical concatenation of the * given resources. Each resource is concatenated in iteration order as * needed when reading from the input stream of the SequenceResource. * * @param mimetype * The mimetype of the resource. * * @param resources * The resources to concatenate within the InputStream of this * SequenceResource. */ public SequenceResource(String mimetype, Iterable resources) { super(mimetype); this.resources = resources; } /** * Creates a new SequenceResource as the logical concatenation of the * given resources. Each resource is concatenated in iteration order as * needed when reading from the input stream of the SequenceResource. The * mimetype of the resulting concatenation is derived from the first * resource. * * @param resources * The resources to concatenate within the InputStream of this * SequenceResource. */ public SequenceResource(Iterable resources) { super(getMimeType(resources)); this.resources = resources; } /** * Creates a new SequenceResource as the logical concatenation of the * given resources. Each resource is concatenated in iteration order as * needed when reading from the input stream of the SequenceResource. * * @param mimetype * The mimetype of the resource. * * @param resources * The resources to concatenate within the InputStream of this * SequenceResource. */ public SequenceResource(String mimetype, Resource... resources) { this(mimetype, Arrays.asList(resources)); } /** * Creates a new SequenceResource as the logical concatenation of the * given resources. Each resource is concatenated in iteration order as * needed when reading from the input stream of the SequenceResource. The * mimetype of the resulting concatenation is derived from the first * resource. * * @param resources * The resources to concatenate within the InputStream of this * SequenceResource. */ public SequenceResource(Resource... resources) { this(Arrays.asList(resources)); } @Override public InputStream asStream() { return new SequenceInputStream(new Enumeration() { /** * Iterator over all resources associated with this * SequenceResource. */ private final Iterator resourceIterator = resources.iterator(); @Override public boolean hasMoreElements() { return resourceIterator.hasNext(); } @Override public InputStream nextElement() { return resourceIterator.next().asStream(); } }); } } ././@LongLink0100644 0000000 0000000 00000000154 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/package-info.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/package-in0100664 0001751 0001751 00000002512 12612226251 035302 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes which describe and provide access to arbitrary resources, such as * the contents of the classpath of a classloader, or files within the web * application itself. */ package org.glyptodon.guacamole.net.basic.resource; ././@LongLink0100644 0000000 0000000 00000000160 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/AbstractResource.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/AbstractRe0100664 0001751 0001751 00000005227 12612226251 035343 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.resource; /** * Base abstract resource implementation which provides an associated mimetype, * and modification time. Classes which extend AbstractResource must provide * their own InputStream, however. * * @author Michael Jumper */ public abstract class AbstractResource implements Resource { /** * The mimetype of this resource. */ private final String mimetype; /** * The time this resource was last modified, in milliseconds since midnight * of January 1, 1970 UTC. */ private final long lastModified; /** * Initializes this AbstractResource with the given mimetype and * modification time. * * @param mimetype * The mimetype of this resource. * * @param lastModified * The time this resource was last modified, in milliseconds since * midnight of January 1, 1970 UTC. */ public AbstractResource(String mimetype, long lastModified) { this.mimetype = mimetype; this.lastModified = lastModified; } /** * Initializes this AbstractResource with the given mimetype. The * modification time of the resource is set to the current system time. * * @param mimetype * The mimetype of this resource. */ public AbstractResource(String mimetype) { this(mimetype, System.currentTimeMillis()); } @Override public long getLastModified() { return lastModified; } @Override public String getMimeType() { return mimetype; } } ././@LongLink0100644 0000000 0000000 00000000161 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/ClassPathResource.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/resource/ClassPathR0100664 0001751 0001751 00000005440 12612226251 035312 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.resource; import java.io.InputStream; /** * A resource which is located within the classpath of an arbitrary * ClassLoader. * * @author Michael Jumper */ public class ClassPathResource extends AbstractResource { /** * The classloader to use when reading this resource. */ private final ClassLoader classLoader; /** * The path of this resource relative to the classloader. */ private final String path; /** * Creates a new ClassPathResource which uses the given ClassLoader to * read the resource having the given path. * * @param classLoader * The ClassLoader to use when reading the resource. * * @param mimetype * The mimetype of the resource. * * @param path * The path of the resource relative to the given ClassLoader. */ public ClassPathResource(ClassLoader classLoader, String mimetype, String path) { super(mimetype); this.classLoader = classLoader; this.path = path; } /** * Creates a new ClassPathResource which uses the ClassLoader associated * with the ClassPathResource class to read the resource having the given * path. * * @param mimetype * The mimetype of the resource. * * @param path * The path of the resource relative to the ClassLoader associated * with the ClassPathResource class. */ public ClassPathResource(String mimetype, String path) { this(ClassPathResource.class.getClassLoader(), mimetype, path); } @Override public InputStream asStream() { return classLoader.getResourceAsStream(path); } } ././@LongLink0100644 0000000 0000000 00000000171 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/xml/usermapping/ProtocolTagHandler.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/xml/usermapping/Pro0100664 0001751 0001751 00000004705 12612226251 035354 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.xml.usermapping; import org.glyptodon.guacamole.xml.TagHandler; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; import org.xml.sax.Attributes; import org.xml.sax.SAXException; /** * TagHandler for the "protocol" element. * * @author Mike Jumper */ public class ProtocolTagHandler implements TagHandler { /** * The GuacamoleConfiguration which will be populated with data from * the tag handled by this tag handler. */ private GuacamoleConfiguration config; /** * Creates a new handler for a "protocol" tag having the given * attributes. * * @param config The GuacamoleConfiguration to update with the data parsed * from the "protocol" tag. * @throws SAXException If the attributes given are not valid. */ public ProtocolTagHandler(GuacamoleConfiguration config) throws SAXException { this.config = config; } @Override public void init(Attributes attributes) throws SAXException { // Do nothing } @Override public TagHandler childElement(String localName) throws SAXException { throw new SAXException("The 'protocol' tag can contain no elements."); } @Override public void complete(String textContent) throws SAXException { config.setProtocol(textContent); } } ././@LongLink0100644 0000000 0000000 00000000174 12612226251 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/xml/usermapping/UserMappingTagHandler.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/xml/usermapping/Use0100664 0001751 0001751 00000005356 12612226251 035353 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.xml.usermapping; import org.glyptodon.guacamole.net.basic.auth.UserMapping; import org.glyptodon.guacamole.xml.TagHandler; import org.xml.sax.Attributes; import org.xml.sax.SAXException; /** * TagHandler for the "user-mapping" element. * * @author Mike Jumper */ public class UserMappingTagHandler implements TagHandler { /** * The UserMapping which will contain all data parsed by this tag handler. */ private UserMapping user_mapping = new UserMapping(); @Override public void init(Attributes attributes) throws SAXException { // Do nothing } @Override public TagHandler childElement(String localName) throws SAXException { // Start parsing of authorize tags, add to list of all authorizations if (localName.equals("authorize")) return new AuthorizeTagHandler(user_mapping); return null; } @Override public void complete(String textContent) throws SAXException { // Do nothing } /** * Returns a user mapping containing all authorizations and configurations * parsed so far. This user mapping will be backed by the data being parsed, * thus any additional authorizations or configurations will be available * in the object returned by this function even after this function has * returned, once the data corresponding to those authorizations or * configurations has been parsed. * * @return A user mapping containing all authorizations and configurations * parsed so far. */ public UserMapping asUserMapping() { return user_mapping; } } ././@LongLink0100644 0000000 0000000 00000000172 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/xml/usermapping/AuthorizeTagHandler.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/xml/usermapping/Aut0100664 0001751 0001751 00000011713 12612226251 035342 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.xml.usermapping; import org.glyptodon.guacamole.net.basic.auth.Authorization; import org.glyptodon.guacamole.net.basic.auth.UserMapping; import org.glyptodon.guacamole.xml.TagHandler; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; import org.xml.sax.Attributes; import org.xml.sax.SAXException; /** * TagHandler for the "authorize" element. * * @author Mike Jumper */ public class AuthorizeTagHandler implements TagHandler { /** * The Authorization corresponding to the "authorize" tag being handled * by this tag handler. The data of this Authorization will be populated * as the tag is parsed. */ private Authorization authorization = new Authorization(); /** * The default GuacamoleConfiguration to use if "param" or "protocol" * tags occur outside a "connection" tag. */ private GuacamoleConfiguration default_config = null; /** * The UserMapping this authorization belongs to. */ private UserMapping parent; /** * Creates a new AuthorizeTagHandler that parses an Authorization owned * by the given UserMapping. * * @param parent The UserMapping that owns the Authorization this handler * will parse. */ public AuthorizeTagHandler(UserMapping parent) { this.parent = parent; } @Override public void init(Attributes attributes) throws SAXException { // Init username and password authorization.setUsername(attributes.getValue("username")); authorization.setPassword(attributes.getValue("password")); // Get encoding String encoding = attributes.getValue("encoding"); if (encoding != null) { // If "md5", use MD5 encoding if (encoding.equals("md5")) authorization.setEncoding(Authorization.Encoding.MD5); // If "plain", use plain text else if (encoding.equals("plain")) authorization.setEncoding(Authorization.Encoding.PLAIN_TEXT); // Otherwise, bad encoding else throw new SAXException( "Invalid encoding: '" + encoding + "'"); } parent.addAuthorization(this.asAuthorization()); } @Override public TagHandler childElement(String localName) throws SAXException { // "connection" tag if (localName.equals("connection")) return new ConnectionTagHandler(authorization); // "param" tag if (localName.equals("param")) { // Create default config if it doesn't exist if (default_config == null) { default_config = new GuacamoleConfiguration(); authorization.addConfiguration("DEFAULT", default_config); } return new ParamTagHandler(default_config); } // "protocol" tag if (localName.equals("protocol")) { // Create default config if it doesn't exist if (default_config == null) { default_config = new GuacamoleConfiguration(); authorization.addConfiguration("DEFAULT", default_config); } return new ProtocolTagHandler(default_config); } return null; } @Override public void complete(String textContent) throws SAXException { // Do nothing } /** * Returns an Authorization backed by the data of this authorize tag * handler. This Authorization is guaranteed to at least have the username, * password, and encoding available. Any associated configurations will be * added dynamically as the authorize tag is parsed. * * @return An Authorization backed by the data of this authorize tag * handler. */ public Authorization asAuthorization() { return authorization; } } ././@LongLink0100644 0000000 0000000 00000000163 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/xml/usermapping/package-info.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/xml/usermapping/pac0100664 0001751 0001751 00000002334 12612226251 035353 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes related to parsing the user-mapping.xml file. */ package org.glyptodon.guacamole.net.basic.xml.usermapping; ././@LongLink0100644 0000000 0000000 00000000166 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/xml/usermapping/ParamTagHandler.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/xml/usermapping/Par0100664 0001751 0001751 00000004716 12612226251 035340 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.xml.usermapping; import org.glyptodon.guacamole.xml.TagHandler; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; import org.xml.sax.Attributes; import org.xml.sax.SAXException; /** * TagHandler for the "param" element. * * @author Mike Jumper */ public class ParamTagHandler implements TagHandler { /** * The GuacamoleConfiguration which will be populated with data from * the tag handled by this tag handler. */ private GuacamoleConfiguration config; /** * The name of the parameter. */ private String name; /** * Creates a new handler for an "param" tag having the given * attributes. * * @param config The GuacamoleConfiguration to update with the data parsed * from the "protocol" tag. */ public ParamTagHandler(GuacamoleConfiguration config) { this.config = config; } @Override public void init(Attributes attributes) throws SAXException { this.name = attributes.getValue("name"); } @Override public TagHandler childElement(String localName) throws SAXException { throw new SAXException("The 'param' tag can contain no elements."); } @Override public void complete(String textContent) throws SAXException { config.setParameter(name, textContent); } } ././@LongLink0100644 0000000 0000000 00000000173 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/xml/usermapping/ConnectionTagHandler.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/xml/usermapping/Con0100664 0001751 0001751 00000006712 12612226251 035333 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.xml.usermapping; import org.glyptodon.guacamole.net.basic.auth.Authorization; import org.glyptodon.guacamole.xml.TagHandler; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; import org.xml.sax.Attributes; import org.xml.sax.SAXException; /** * TagHandler for the "connection" element. * * @author Mike Jumper */ public class ConnectionTagHandler implements TagHandler { /** * The GuacamoleConfiguration backing this tag handler. */ private GuacamoleConfiguration config = new GuacamoleConfiguration(); /** * The name associated with the connection being parsed. */ private String name; /** * The Authorization this connection belongs to. */ private Authorization parent; /** * Creates a new ConnectionTagHandler that parses a Connection owned by * the given Authorization. * * @param parent The Authorization that will own this Connection once * parsed. */ public ConnectionTagHandler(Authorization parent) { this.parent = parent; } @Override public void init(Attributes attributes) throws SAXException { name = attributes.getValue("name"); parent.addConfiguration(name, this.asGuacamoleConfiguration()); } @Override public TagHandler childElement(String localName) throws SAXException { if (localName.equals("param")) return new ParamTagHandler(config); if (localName.equals("protocol")) return new ProtocolTagHandler(config); return null; } @Override public void complete(String textContent) throws SAXException { // Do nothing } /** * Returns a GuacamoleConfiguration whose contents are populated from data * within this connection element and child elements. This * GuacamoleConfiguration will continue to be modified as the user mapping * is parsed. * * @return A GuacamoleConfiguration whose contents are populated from data * within this connection element. */ public GuacamoleConfiguration asGuacamoleConfiguration() { return config; } /** * Returns the name associated with this connection. * * @return The name associated with this connection. */ public String getName() { return name; } } ././@LongLink0100644 0000000 0000000 00000000147 12471273273 011645 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/auth/UserMapping.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/auth/UserMapping.ja0100664 0001751 0001751 00000004320 12471273273 035250 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.auth; import java.util.HashMap; import java.util.Map; /** * Mapping of all usernames to corresponding authorizations. * * @author Mike Jumper */ public class UserMapping { /** * All authorizations, indexed by username. */ private Map authorizations = new HashMap(); /** * Adds the given authorization to the user mapping. * * @param authorization The authorization to add to the user mapping. */ public void addAuthorization(Authorization authorization) { authorizations.put(authorization.getUsername(), authorization); } /** * Returns the authorization corresponding to the user having the given * username, if any. * * @param username The username to find the authorization for. * @return The authorization corresponding to the user having the given * username, or null if no such authorization exists. */ public Authorization getAuthorization(String username) { return authorizations.get(username); } } ././@LongLink0100644 0000000 0000000 00000000151 12471273273 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/auth/Authorization.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/auth/Authorization.0100664 0001751 0001751 00000017747 12471273273 035364 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.auth; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Map; import java.util.TreeMap; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; /** * Mapping of username/password pair to configuration set. In addition to basic * storage of the username, password, and configurations, this class also * provides password validation functions. * * @author Mike Jumper */ public class Authorization { /** * All supported password encodings. */ public static enum Encoding { /** * Plain-text password (not hashed at all). */ PLAIN_TEXT, /** * Password hashed with MD5. */ MD5 } /** * The username being authorized. */ private String username; /** * The password corresponding to the username being authorized, which may * be hashed. */ private String password; /** * The encoding used when the password was hashed. */ private Encoding encoding = Encoding.PLAIN_TEXT; /** * Map of all authorized configurations, indexed by configuration name. */ private Map configs = new TreeMap(); /** * Lookup table of hex bytes characters by value. */ private static final char HEX_CHARS[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; /** * Produces a String containing the bytes provided in hexadecimal notation. * * @param bytes The bytes to convert into hex. * @return A String containing the hex representation of the given bytes. */ private static String getHexString(byte[] bytes) { // If null byte array given, return null if (bytes == null) return null; // Create string builder for holding the hex representation, // pre-calculating the exact length StringBuilder hex = new StringBuilder(2 * bytes.length); // Convert each byte into a pair of hex digits for (byte b : bytes) { hex.append(HEX_CHARS[(b & 0xF0) >> 4]) .append(HEX_CHARS[ b & 0x0F ]); } // Return the string produced return hex.toString(); } /** * Returns the username associated with this authorization. * * @return The username associated with this authorization. */ public String getUsername() { return username; } /** * Sets the username associated with this authorization. * * @param username The username to associate with this authorization. */ public void setUsername(String username) { this.username = username; } /** * Returns the password associated with this authorization, which may be * encoded or hashed. * * @return The password associated with this authorization. */ public String getPassword() { return password; } /** * Sets the password associated with this authorization, which must be * encoded using the encoding specified with setEncoding(). By default, * passwords are plain text. * * @param password Sets the password associated with this authorization. */ public void setPassword(String password) { this.password = password; } /** * Returns the encoding used to hash the password, if any. * * @return The encoding used to hash the password. */ public Encoding getEncoding() { return encoding; } /** * Sets the encoding which will be used to hash the password or when * comparing a given password for validation. * * @param encoding The encoding to use for password hashing. */ public void setEncoding(Encoding encoding) { this.encoding = encoding; } /** * Returns whether a given username/password pair is authorized based on * the stored username and password. The password given must be plain text. * It will be hashed as necessary to perform the validation. * * @param username The username to validate. * @param password The password to validate. * @return true if the username/password pair given is authorized, false * otherwise. */ public boolean validate(String username, String password) { // If username matches if (username != null && password != null && username.equals(this.username)) { switch (encoding) { // If plain text, just compare case PLAIN_TEXT: // Compare plaintext return password.equals(this.password); // If hased with MD5, hash password and compare case MD5: // Compare hashed password try { MessageDigest digest = MessageDigest.getInstance("MD5"); String hashedPassword = getHexString(digest.digest(password.getBytes("UTF-8"))); return hashedPassword.equals(this.password.toUpperCase()); } catch (UnsupportedEncodingException e) { throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e); } catch (NoSuchAlgorithmException e) { throw new UnsupportedOperationException("Unexpected lack of MD5 support.", e); } } } // end validation check return false; } /** * Returns the GuacamoleConfiguration having the given name and associated * with the username/password pair stored within this authorization. * * @param name The name of the GuacamoleConfiguration to return. * @return The GuacamoleConfiguration having the given name, or null if no * such GuacamoleConfiguration exists. */ public GuacamoleConfiguration getConfiguration(String name) { return configs.get(name); } /** * Adds the given GuacamoleConfiguration to the set of stored configurations * under the given name. * * @param name The name to associate this GuacamoleConfiguration with. * @param config The GuacamoleConfiguration to store. */ public void addConfiguration(String name, GuacamoleConfiguration config) { configs.put(name, config); } /** * Returns a Map of all stored GuacamoleConfigurations associated with the * username/password pair stored within this authorization, indexed by * configuration name. * * @return A Map of all stored GuacamoleConfigurations. */ public Map getConfigurations() { return configs; } } ././@LongLink0100644 0000000 0000000 00000000150 12471273273 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/auth/package-info.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/auth/package-info.j0100664 0001751 0001751 00000002366 12471273273 035211 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes which drive the default, basic authentication of the Guacamole * web application. */ package org.glyptodon.guacamole.net.basic.auth; ././@LongLink0100644 0000000 0000000 00000000172 12645510255 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/BasicGuacamoleProperties.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/BasicGua0100664 0001751 0001751 00000006147 12645510255 035344 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.properties; import org.glyptodon.guacamole.properties.FileGuacamoleProperty; import org.glyptodon.guacamole.properties.IntegerGuacamoleProperty; import org.glyptodon.guacamole.properties.StringGuacamoleProperty; /** * Properties used by the default Guacamole web application. * * @author Michael Jumper */ public class BasicGuacamoleProperties { /** * This class should not be instantiated. */ private BasicGuacamoleProperties() {} /** * The authentication provider to user when retrieving the authorized * configurations of a user. This property is currently supported, but * deprecated in favor of the GUACAMOLE_HOME/extensions directory. */ @Deprecated public static final AuthenticationProviderProperty AUTH_PROVIDER = new AuthenticationProviderProperty() { @Override public String getName() { return "auth-provider"; } }; /** * The directory to search for authentication provider classes. This * property is currently supported, but deprecated in favor of the * GUACAMOLE_HOME/lib directory. */ @Deprecated public static final FileGuacamoleProperty LIB_DIRECTORY = new FileGuacamoleProperty() { @Override public String getName() { return "lib-directory"; } }; /** * The session timeout for the API, in minutes. */ public static final IntegerGuacamoleProperty API_SESSION_TIMEOUT = new IntegerGuacamoleProperty() { @Override public String getName() { return "api-session-timeout"; } }; /** * Comma-separated list of all allowed languages, where each language is * represented by a language key, such as "en" or "en_US". If specified, * only languages within this list will be listed as available by the REST * service. */ public static final StringSetProperty ALLOWED_LANGUAGES = new StringSetProperty() { @Override public String getName() { return "allowed-languages"; } }; } ././@LongLink0100644 0000000 0000000 00000000200 12612226251 011622 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/AuthenticationProviderProperty.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/Authenti0100664 0001751 0001751 00000005517 12612226251 035441 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.properties; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.properties.GuacamoleProperty; /** * A GuacamoleProperty whose value is the name of a class to use to * authenticate users. This class must implement AuthenticationProvider. Use * of this property type is deprecated in favor of the * GUACAMOLE_HOME/extensions directory. * * @author Michael Jumper */ @Deprecated public abstract class AuthenticationProviderProperty implements GuacamoleProperty> { @Override @SuppressWarnings("unchecked") // Explicitly checked within by isAssignableFrom() public Class parseValue(String authProviderClassName) throws GuacamoleException { // If no property provided, return null. if (authProviderClassName == null) return null; // Get auth provider instance try { // Get authentication provider class Class authProviderClass = org.glyptodon.guacamole.net.basic.GuacamoleClassLoader.getInstance().loadClass(authProviderClassName); // Verify the located class is actually a subclass of AuthenticationProvider if (!AuthenticationProvider.class.isAssignableFrom(authProviderClass)) throw new GuacamoleException("Specified authentication provider class is not a AuthenticationProvider."); // Return located class return (Class) authProviderClass; } catch (ClassNotFoundException e) { throw new GuacamoleException("Authentication provider class not found", e); } } } ././@LongLink0100644 0000000 0000000 00000000163 12645510255 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/StringSetProperty.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/StringSe0100664 0001751 0001751 00000005161 12645510255 035417 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.properties; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.properties.GuacamoleProperty; /** * A GuacamoleProperty whose value is a Set of unique Strings. The string value * parsed to produce this set is a comma-delimited list. Duplicate values are * ignored, as is any whitespace following delimiters. To maintain * compatibility with the behavior of Java properties in general, only * whitespace at the beginning of each value is ignored; trailing whitespace * becomes part of the value. * * @author Michael Jumper */ public abstract class StringSetProperty implements GuacamoleProperty> { /** * A pattern which matches against the delimiters between values. This is * currently simply a comma and any following whitespace. Parts of the * input string which match this pattern will not be included in the parsed * result. */ private static final Pattern DELIMITER_PATTERN = Pattern.compile(",\\s*"); @Override public Set parseValue(String values) throws GuacamoleException { // If no property provided, return null. if (values == null) return null; // Split string into a set of individual values List valueList = Arrays.asList(DELIMITER_PATTERN.split(values)); return new HashSet(valueList); } } ././@LongLink0100644 0000000 0000000 00000000156 12471273273 011645 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/package-info.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/package-0100664 0001751 0001751 00000002442 12471273273 035333 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes related to the properties which the Guacamole web application * (and stock parts of it) read from guacamole.properties. */ package org.glyptodon.guacamole.net.basic.properties; guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIError.java0100664 0001751 0001751 00000011761 12612226251 035002 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest; import java.util.Collection; import javax.ws.rs.core.Response; import org.glyptodon.guacamole.form.Field; /** * Describes an error that occurred within a REST endpoint. * * @author James Muehlner * @author Michael Jumper */ public class APIError { /** * The error message. */ private final String message; /** * All expected request parameters, if any, as a collection of fields. */ private final Collection expected; /** * The type of error that occurred. */ private final Type type; /** * All possible types of REST API errors. */ public enum Type { /** * The requested operation could not be performed because the request * itself was malformed. */ BAD_REQUEST(Response.Status.BAD_REQUEST), /** * The credentials provided were invalid. */ INVALID_CREDENTIALS(Response.Status.FORBIDDEN), /** * The credentials provided were not necessarily invalid, but were not * sufficient to determine validity. */ INSUFFICIENT_CREDENTIALS(Response.Status.FORBIDDEN), /** * An internal server error has occurred. */ INTERNAL_ERROR(Response.Status.INTERNAL_SERVER_ERROR), /** * An object related to the request does not exist. */ NOT_FOUND(Response.Status.NOT_FOUND), /** * Permission was denied to perform the requested operation. */ PERMISSION_DENIED(Response.Status.FORBIDDEN); /** * The HTTP status associated with this error type. */ private final Response.Status status; /** * Defines a new error type associated with the given HTTP status. * * @param status * The HTTP status to associate with the error type. */ Type(Response.Status status) { this.status = status; } /** * Returns the HTTP status associated with this error type. * * @return * The HTTP status associated with this error type. */ public Response.Status getStatus() { return status; } } /** * Create a new APIError with the specified error message. * * @param type * The type of error that occurred. * * @param message * The error message. */ public APIError(Type type, String message) { this.type = type; this.message = message; this.expected = null; } /** * Create a new APIError with the specified error message and parameter * information. * * @param type * The type of error that occurred. * * @param message * The error message. * * @param expected * All parameters expected in the original request, or now required as * a result of the original request, as a collection of fields. */ public APIError(Type type, String message, Collection expected) { this.type = type; this.message = message; this.expected = expected; } /** * Returns the type of error that occurred. * * @return * The type of error that occurred. */ public Type getType() { return type; } /** * Returns a collection of all required parameters, where each parameter is * represented by a field. * * @return * A collection of all required parameters. */ public Collection getExpected() { return expected; } /** * Returns a human-readable error message describing the error that * occurred. * * @return * A human-readable error message. */ public String getMessage() { return message; } } ././@LongLink0100644 0000000 0000000 00000000160 12645510255 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTExceptionWrapper.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTExceptionW0100664 0001751 0001751 00000022361 12645510255 035226 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest; import com.google.inject.Inject; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import javax.ws.rs.FormParam; import javax.ws.rs.QueryParam; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.GuacamoleUnauthorizedException; import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A method interceptor which wraps custom exception handling around methods * which can throw GuacamoleExceptions and which are exposed through the REST * interface. The various types of GuacamoleExceptions are automatically * translated into appropriate HTTP responses, including JSON describing the * error that occurred. * * @author James Muehlner * @author Michael Jumper */ public class RESTExceptionWrapper implements MethodInterceptor { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(RESTExceptionWrapper.class); /** * Service for authenticating users and managing their Guacamole sessions. */ @Inject private AuthenticationService authenticationService; /** * Determines whether the given set of annotations describes an HTTP * request parameter of the given name. For a parameter to be associated * with an HTTP request parameter, it must be annotated with either the * @QueryParam or @FormParam annotations. * * @param annotations * The annotations associated with the Java parameter being checked. * * @param name * The name of the HTTP request parameter. * * @return * true if the given set of annotations describes an HTTP request * parameter having the given name, false otherwise. */ private boolean isRequestParameter(Annotation[] annotations, String name) { // Search annotations for associated HTTP parameters for (Annotation annotation : annotations) { // Check if parameter is associated with the HTTP query string if (annotation instanceof QueryParam && name.equals(((QueryParam) annotation).value())) return true; // Failing that, check whether the parameter is associated with the // HTTP request body if (annotation instanceof FormParam && name.equals(((FormParam) annotation).value())) return true; } // No parameter annotations are present return false; } /** * Returns the authentication token that was passed in the given method * invocation. If the given method invocation is not associated with an * HTTP request (it lacks the appropriate JAX-RS annotations) or there is * no authentication token, null is returned. * * @param invocation * The method invocation whose corresponding authentication token * should be determined. * * @return * The authentication token passed in the given method invocation, or * null if there is no such token. */ private String getAuthenticationToken(MethodInvocation invocation) { Method method = invocation.getMethod(); // Get the types and annotations associated with each parameter Annotation[][] parameterAnnotations = method.getParameterAnnotations(); Class[] parameterTypes = method.getParameterTypes(); // The Java standards require these to be parallel arrays assert(parameterAnnotations.length == parameterTypes.length); // Iterate through all parameters, looking for the authentication token for (int i = 0; i < parameterTypes.length; i++) { // Only inspect String parameters Class parameterType = parameterTypes[i]; if (parameterType != String.class) continue; // Parameter must be declared as a REST service parameter Annotation[] annotations = parameterAnnotations[i]; if (!isRequestParameter(annotations, "token")) continue; // The token parameter has been found - return its value Object[] args = invocation.getArguments(); return (String) args[i]; } // No token parameter is defined return null; } @Override public Object invoke(MethodInvocation invocation) throws Throwable { try { // Invoke wrapped method try { return invocation.proceed(); } // Ensure any associated session is invalidated if unauthorized catch (GuacamoleUnauthorizedException e) { // Pull authentication token from request String token = getAuthenticationToken(invocation); // If there is an associated auth token, invalidate it if (authenticationService.destroyGuacamoleSession(token)) logger.debug("Implicitly invalidated session for token \"{}\".", token); // Continue with exception processing throw e; } } // Additional credentials are needed catch (GuacamoleInsufficientCredentialsException e) { // Generate default message String message = e.getMessage(); if (message == null) message = "Permission denied."; throw new APIException( APIError.Type.INSUFFICIENT_CREDENTIALS, message, e.getCredentialsInfo().getFields() ); } // The provided credentials are wrong catch (GuacamoleInvalidCredentialsException e) { // Generate default message String message = e.getMessage(); if (message == null) message = "Permission denied."; throw new APIException( APIError.Type.INVALID_CREDENTIALS, message, e.getCredentialsInfo().getFields() ); } // Generic permission denied catch (GuacamoleSecurityException e) { // Generate default message String message = e.getMessage(); if (message == null) message = "Permission denied."; throw new APIException( APIError.Type.PERMISSION_DENIED, message ); } // Arbitrary resource not found catch (GuacamoleResourceNotFoundException e) { // Generate default message String message = e.getMessage(); if (message == null) message = "Not found."; throw new APIException( APIError.Type.NOT_FOUND, message ); } // Arbitrary bad requests catch (GuacamoleClientException e) { // Generate default message String message = e.getMessage(); if (message == null) message = "Invalid request."; throw new APIException( APIError.Type.BAD_REQUEST, message ); } // All other errors catch (GuacamoleException e) { // Generate default message String message = e.getMessage(); if (message == null) message = "Unexpected server error."; // Ensure internal errors are logged at the debug level logger.debug("Unexpected exception in REST endpoint.", e); throw new APIException( APIError.Type.INTERNAL_ERROR, message ); } } } ././@LongLink0100644 0000000 0000000 00000000204 12645510255 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/history/APIConnectionRecordSortPredicate.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/history/APICon0100664 0001751 0001751 00000012145 12645510255 035214 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.history; import org.glyptodon.guacamole.net.auth.ConnectionRecordSet; import org.glyptodon.guacamole.net.basic.rest.APIError; import org.glyptodon.guacamole.net.basic.rest.APIException; /** * A sort predicate which species the property to use when sorting connection * records, along with the sort order. * * @author Michael Jumper */ public class APIConnectionRecordSortPredicate { /** * The prefix which will be included before the name of a sortable property * to indicate that the sort order is descending, not ascending. */ public static final String DESCENDING_PREFIX = "-"; /** * All possible property name strings and their corresponding * ConnectionRecordSet.SortableProperty values. */ public enum SortableProperty { /** * The date that the connection associated with the connection record * began (connected). */ startDate(ConnectionRecordSet.SortableProperty.START_DATE); /** * The ConnectionRecordSet.SortableProperty that this property name * string represents. */ public final ConnectionRecordSet.SortableProperty recordProperty; /** * Creates a new SortableProperty which associates the property name * string (identical to its own name) with the given * ConnectionRecordSet.SortableProperty value. * * @param recordProperty * The ConnectionRecordSet.SortableProperty value to associate with * the new SortableProperty. */ SortableProperty(ConnectionRecordSet.SortableProperty recordProperty) { this.recordProperty = recordProperty; } } /** * The property to use when sorting ConnectionRecords. */ private ConnectionRecordSet.SortableProperty property; /** * Whether the requested sort order is descending (true) or ascending * (false). */ private boolean descending; /** * Parses the given string value, determining the requested sort property * and ordering. Possible values consist of any valid property name, and * may include an optional prefix to denote descending sort order. Each * possible property name is enumerated by the SortableValue enum. * * @param value * The sort predicate string to parse, which must consist ONLY of a * valid property name, possibly preceded by the DESCENDING_PREFIX. * * @throws APIException * If the provided sort predicate string is invalid. */ public APIConnectionRecordSortPredicate(String value) throws APIException { // Parse whether sort order is descending if (value.startsWith(DESCENDING_PREFIX)) { descending = true; value = value.substring(DESCENDING_PREFIX.length()); } // Parse sorting property into ConnectionRecordSet.SortableProperty try { this.property = SortableProperty.valueOf(value).recordProperty; } // Bail out if sort property is not valid catch (IllegalArgumentException e) { throw new APIException( APIError.Type.BAD_REQUEST, String.format("Invalid sort property: \"%s\"", value) ); } } /** * Returns the SortableProperty defined by ConnectionRecordSet which * represents the property requested. * * @return * The ConnectionRecordSet.SortableProperty which refers to the same * property as the string originally provided when this * APIConnectionRecordSortPredicate was created. */ public ConnectionRecordSet.SortableProperty getProperty() { return property; } /** * Returns whether the requested sort order is descending. * * @return * true if the sort order is descending, false if the sort order is * ascending. */ public boolean isDescending() { return descending; } } ././@LongLink0100644 0000000 0000000 00000000166 12645510255 011643 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/history/HistoryRESTService.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/history/Histor0100664 0001751 0001751 00000013206 12645510255 035412 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.history; import com.google.inject.Inject; import java.util.ArrayList; import java.util.List; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.ConnectionRecord; import org.glyptodon.guacamole.net.auth.ConnectionRecordSet; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A REST Service for retrieving and managing the history records of Guacamole * objects. * * @author Michael Jumper */ @Path("/data/{dataSource}/history") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class HistoryRESTService { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(HistoryRESTService.class); /** * The maximum number of history records to return in any one response. */ private static final int MAXIMUM_HISTORY_SIZE = 1000; /** * A service for authenticating users from auth tokens. */ @Inject private AuthenticationService authenticationService; /** * Service for convenient retrieval of objects. */ @Inject private ObjectRetrievalService retrievalService; /** * Retrieves the usage history for all connections, restricted by optional * filter parameters. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext containing the connection whose history is to be * retrieved. * * @param requiredContents * The set of strings that each must occur somewhere within the * returned connection records, whether within the associated username, * the name of the associated connection, or any associated date. If * non-empty, any connection record not matching each of the strings * within the collection will be excluded from the results. * * @param sortPredicates * A list of predicates to apply while sorting the resulting connection * records, describing the properties involved and the sort order for * those properties. * * @return * A list of connection records, describing the start and end times of * various usages of this connection. * * @throws GuacamoleException * If an error occurs while retrieving the connection history. */ @GET @Path("/connections") public List getConnectionHistory(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @QueryParam("contains") List requiredContents, @QueryParam("order") List sortPredicates) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Retrieve overall connection history ConnectionRecordSet history = userContext.getConnectionHistory(); // Restrict to records which contain the specified strings for (String required : requiredContents) { if (!required.isEmpty()) history = history.contains(required); } // Sort according to specified ordering for (APIConnectionRecordSortPredicate predicate : sortPredicates) history = history.sort(predicate.getProperty(), predicate.isDescending()); // Limit to maximum result size history = history.limit(MAXIMUM_HISTORY_SIZE); // Convert record set to collection of API connection records List apiRecords = new ArrayList(); for (ConnectionRecord record : history.asCollection()) apiRecords.add(new APIConnectionRecord(record)); // Return the converted history return apiRecords; } } ././@LongLink0100644 0000000 0000000 00000000167 12645510255 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/history/APIConnectionRecord.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/history/APICon0100664 0001751 0001751 00000011354 12645510255 035215 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.history; import java.util.Date; import org.glyptodon.guacamole.net.auth.ConnectionRecord; /** * A connection record which may be exposed through the REST endpoints. * * @author Michael Jumper */ public class APIConnectionRecord { /** * The identifier of the connection associated with this record. */ private final String connectionIdentifier; /** * The identifier of the connection associated with this record. */ private final String connectionName; /** * The date and time the connection began. */ private final Date startDate; /** * The date and time the connection ended, or null if the connection is * still running or if the end time is unknown. */ private final Date endDate; /** * The host from which the connection originated, if known. */ private final String remoteHost; /** * The name of the user who used or is using the connection. */ private final String username; /** * Whether the connection is currently active. */ private final boolean active; /** * Creates a new APIConnectionRecord, copying the data from the given * record. * * @param record * The record to copy data from. */ public APIConnectionRecord(ConnectionRecord record) { this.connectionIdentifier = record.getConnectionIdentifier(); this.connectionName = record.getConnectionName(); this.startDate = record.getStartDate(); this.endDate = record.getEndDate(); this.remoteHost = record.getRemoteHost(); this.username = record.getUsername(); this.active = record.isActive(); } /** * Returns the identifier of the connection associated with this * record. * * @return * The identifier of the connection associated with this record. */ public String getConnectionIdentifier() { return connectionIdentifier; } /** * Returns the name of the connection associated with this record. * * @return * The name of the connection associated with this record. */ public String getConnectionName() { return connectionName; } /** * Returns the date and time the connection began. * * @return * The date and time the connection began. */ public Date getStartDate() { return startDate; } /** * Returns the date and time the connection ended, if applicable. * * @return * The date and time the connection ended, or null if the connection is * still running or if the end time is unknown. */ public Date getEndDate() { return endDate; } /** * Returns the remote host from which this connection originated. * * @return * The remote host from which this connection originated. */ public String getRemoteHost() { return remoteHost; } /** * Returns the name of the user who used or is using the connection at the * times given by this connection record. * * @return * The name of the user who used or is using the associated connection. */ public String getUsername() { return username; } /** * Returns whether the connection associated with this record is still * active. * * @return * true if the connection associated with this record is still active, * false otherwise. */ public boolean isActive() { return active; } } ././@LongLink0100644 0000000 0000000 00000000160 12645510255 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/history/package-info.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/history/packag0100664 0001751 0001751 00000002403 12645510255 035365 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes related to retrieval or maintenance of history records using the * Guacamole REST API. */ package org.glyptodon.guacamole.net.basic.rest.history; ././@LongLink0100644 0000000 0000000 00000000150 12612226251 011626 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIException.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIException.j0100664 0001751 0001751 00000006057 12612226251 035161 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest; import java.util.Collection; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; import org.glyptodon.guacamole.form.Field; /** * An exception that will result in the given error error information being * returned from the API layer. All error messages have the same format which * is defined by APIError. * * @author James Muehlner * @author Michael Jumper */ public class APIException extends WebApplicationException { /** * Construct a new APIException with the given error. All information * associated with this new exception will be extracted from the given * APIError. * * @param error * The error that occurred. */ public APIException(APIError error) { super(Response.status(error.getType().getStatus()).entity(error).build()); } /** * Creates a new APIException with the given type and message. The * corresponding APIError will be created from the provided information. * * @param type * The type of error that occurred. * * @param message * A human-readable message describing the error. */ public APIException(APIError.Type type, String message) { this(new APIError(type, message)); } /** * Creates a new APIException with the given type, message, and parameter * information. The corresponding APIError will be created from the * provided information. * * @param type * The type of error that occurred. * * @param message * A human-readable message describing the error. * * @param expected * All parameters expected in the original request, or now required as * a result of the original request, as a collection of fields. */ public APIException(APIError.Type type, String message, Collection expected) { this(new APIError(type, message, expected)); } } guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIPatch.java0100664 0001751 0001751 00000005321 12612226251 034743 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest; /** * An object for representing the body of a HTTP PATCH method. * See https://tools.ietf.org/html/rfc6902 * * @author James Muehlner * @param The type of object being patched. */ public class APIPatch { /** * The possible operations for a PATCH request. */ public enum Operation { add, remove, test, copy, replace, move } /** * The operation to perform for this patch. */ private Operation op; /** * The value for this patch. */ private T value; /** * The path for this patch. */ private String path; /** * Returns the operation for this patch. * @return the operation for this patch. */ public Operation getOp() { return op; } /** * Set the operation for this patch. * @param op The operation for this patch. */ public void setOp(Operation op) { this.op = op; } /** * Returns the value of this patch. * @return The value of this patch. */ public T getValue() { return value; } /** * Sets the value of this patch. * @param value The value of this patch. */ public void setValue(T value) { this.value = value; } /** * Returns the path for this patch. * @return The path for this patch. */ public String getPath() { return path; } /** * Set the path for this patch. * @param path The path for this patch. */ public void setPath(String path) { this.path = path; } } ././@LongLink0100644 0000000 0000000 00000000170 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/language/LanguageRESTService.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/language/Langu0100664 0001751 0001751 00000004155 12612226251 035267 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.language; import com.google.inject.Inject; import java.util.Map; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.glyptodon.guacamole.net.basic.extension.LanguageResourceService; /** * A REST Service for handling the listing of languages. * * @author James Muehlner */ @Path("/languages") @Produces(MediaType.APPLICATION_JSON) public class LanguageRESTService { /** * Service for retrieving information regarding available language * resources. */ @Inject private LanguageResourceService languageResourceService; /** * Returns a map of all available language keys to their corresponding * human-readable names. * * @return * A map of languages defined in the system, of language key to * display name. */ @GET public Map getLanguages() { return languageResourceService.getLanguageNames(); } } ././@LongLink0100644 0000000 0000000 00000000161 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/language/package-info.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/language/packa0100664 0001751 0001751 00000002360 12612226251 035274 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes related to the language retrieval aspect of the Guacamole REST API. */ package org.glyptodon.guacamole.net.basic.rest.language; guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/PATCH.java0100664 0001751 0001751 00000003065 12612226251 034214 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.ws.rs.HttpMethod; /** * An annotation for using the HTTP PATCH method in the REST endpoints. * * @author James Muehlner */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @HttpMethod("PATCH") public @interface PATCH {} ././@LongLink0100644 0000000 0000000 00000000164 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/APIConnection.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/API0100664 0001751 0001751 00000015635 12612226251 035213 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.connection; import java.util.Map; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.map.annotate.JsonSerialize; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; /** * A simple connection to expose through the REST endpoints. * * @author James Muehlner */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) public class APIConnection { /** * The name of this connection. */ private String name; /** * The identifier of this connection. */ private String identifier; /** * The identifier of the parent connection group for this connection. */ private String parentIdentifier; /** * The protocol of this connection. */ private String protocol; /** * Map of all associated parameter values, indexed by parameter name. */ private Map parameters; /** * Map of all associated attributes by attribute identifier. */ private Map attributes; /** * The count of currently active connections using this connection. */ private int activeConnections; /** * Create an empty APIConnection. */ public APIConnection() {} /** * Create an APIConnection from a Connection record. Parameters for the * connection will not be included. * * @param connection The connection to create this APIConnection from. * @throws GuacamoleException If a problem is encountered while * instantiating this new APIConnection. */ public APIConnection(Connection connection) throws GuacamoleException { // Set connection information this.name = connection.getName(); this.identifier = connection.getIdentifier(); this.parentIdentifier = connection.getParentIdentifier(); this.activeConnections = connection.getActiveConnections(); // Set protocol from configuration GuacamoleConfiguration configuration = connection.getConfiguration(); this.protocol = configuration.getProtocol(); // Associate any attributes this.attributes = connection.getAttributes(); } /** * Returns the name of this connection. * @return The name of this connection. */ public String getName() { return name; } /** * Set the name of this connection. * @param name The name of this connection. */ public void setName(String name) { this.name = name; } /** * Returns the unique identifier for this connection. * @return The unique identifier for this connection. */ public String getIdentifier() { return identifier; } /** * Sets the unique identifier for this connection. * @param identifier The unique identifier for this connection. */ public void setIdentifier(String identifier) { this.identifier = identifier; } /** * Returns the unique identifier for this connection. * @return The unique identifier for this connection. */ public String getParentIdentifier() { return parentIdentifier; } /** * Sets the parent connection group identifier for this connection. * @param parentIdentifier The parent connection group identifier * for this connection. */ public void setParentIdentifier(String parentIdentifier) { this.parentIdentifier = parentIdentifier; } /** * Returns the parameter map for this connection. * @return The parameter map for this connection. */ public Map getParameters() { return parameters; } /** * Sets the parameter map for this connection. * @param parameters The parameter map for this connection. */ public void setParameters(Map parameters) { this.parameters = parameters; } /** * Returns the protocol for this connection. * @return The protocol for this connection. */ public String getProtocol() { return protocol; } /** * Sets the protocol for this connection. * @param protocol protocol for this connection. */ public void setProtocol(String protocol) { this.protocol = protocol; } /** * Returns the number of currently active connections using this * connection. * * @return * The number of currently active usages of this connection. */ public int getActiveConnections() { return activeConnections; } /** * Set the number of currently active connections using this connection. * * @param activeConnections * The number of currently active usages of this connection. */ public void setActiveUsers(int activeConnections) { this.activeConnections = activeConnections; } /** * Returns a map of all attributes associated with this connection. Each * entry key is the attribute identifier, while each value is the attribute * value itself. * * @return * The attribute map for this connection. */ public Map getAttributes() { return attributes; } /** * Sets the map of all attributes associated with this connection. Each * entry key is the attribute identifier, while each value is the attribute * value itself. * * @param attributes * The attribute map for this connection. */ public void setAttributes(Map attributes) { this.attributes = attributes; } } ././@LongLink0100644 0000000 0000000 00000000173 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/APIConnectionWrapper.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/API0100664 0001751 0001751 00000010505 12612226251 035202 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.connection; import java.util.Collections; import java.util.List; import java.util.Map; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionRecord; import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; /** * A wrapper to make an APIConnection look like a Connection. Useful where a * org.glyptodon.guacamole.net.auth.Connection is required. * * @author James Muehlner */ public class APIConnectionWrapper implements Connection { /** * The wrapped APIConnection. */ private final APIConnection apiConnection; /** * Creates a new APIConnectionWrapper which wraps the given APIConnection * as a Connection. * * @param apiConnection * The APIConnection to wrap. */ public APIConnectionWrapper(APIConnection apiConnection) { this.apiConnection = apiConnection; } @Override public String getName() { return apiConnection.getName(); } @Override public void setName(String name) { apiConnection.setName(name); } @Override public String getIdentifier() { return apiConnection.getIdentifier(); } @Override public void setIdentifier(String identifier) { apiConnection.setIdentifier(identifier); } @Override public String getParentIdentifier() { return apiConnection.getParentIdentifier(); } @Override public void setParentIdentifier(String parentIdentifier) { apiConnection.setParentIdentifier(parentIdentifier); } @Override public int getActiveConnections() { return apiConnection.getActiveConnections(); } @Override public GuacamoleConfiguration getConfiguration() { // Create the GuacamoleConfiguration with current protocol GuacamoleConfiguration configuration = new GuacamoleConfiguration(); configuration.setProtocol(apiConnection.getProtocol()); // Add parameters, if available Map parameters = apiConnection.getParameters(); if (parameters != null) configuration.setParameters(parameters); return configuration; } @Override public void setConfiguration(GuacamoleConfiguration config) { // Set protocol and parameters apiConnection.setProtocol(config.getProtocol()); apiConnection.setParameters(config.getParameters()); } @Override public Map getAttributes() { return apiConnection.getAttributes(); } @Override public void setAttributes(Map attributes) { apiConnection.setAttributes(attributes); } @Override public GuacamoleTunnel connect(GuacamoleClientInformation info) throws GuacamoleException { throw new UnsupportedOperationException("Operation not supported."); } @Override public List getHistory() throws GuacamoleException { return Collections.emptyList(); } } ././@LongLink0100644 0000000 0000000 00000000163 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/package-info.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/pac0100664 0001751 0001751 00000002367 12612226251 035343 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes related to the connection manipulation aspect of the Guacamole REST API. */ package org.glyptodon.guacamole.net.basic.rest.connection; ././@LongLink0100644 0000000 0000000 00000000174 12645510255 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionRESTService.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/Con0100664 0001751 0001751 00000032546 12645510255 035327 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.connection; import com.google.inject.Inject; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionRecord; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.User; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.glyptodon.guacamole.net.basic.rest.history.APIConnectionRecord; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A REST Service for handling connection CRUD operations. * * @author James Muehlner */ @Path("/data/{dataSource}/connections") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class ConnectionRESTService { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(ConnectionRESTService.class); /** * A service for authenticating users from auth tokens. */ @Inject private AuthenticationService authenticationService; /** * Service for convenient retrieval of objects. */ @Inject private ObjectRetrievalService retrievalService; /** * Retrieves an individual connection. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext containing the connection to be retrieved. * * @param connectionID * The identifier of the connection to retrieve. * * @return * The connection having the given identifier. * * @throws GuacamoleException * If an error occurs while retrieving the connection. */ @GET @Path("/{connectionID}") public APIConnection getConnection(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionID") String connectionID) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); // Retrieve the requested connection return new APIConnection(retrievalService.retrieveConnection(session, authProviderIdentifier, connectionID)); } /** * Retrieves the parameters associated with a single connection. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext containing the connection whose parameters are to be * retrieved. * * @param connectionID * The identifier of the connection. * * @return * A map of parameter name/value pairs. * * @throws GuacamoleException * If an error occurs while retrieving the connection parameters. */ @GET @Path("/{connectionID}/parameters") public Map getConnectionParameters(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionID") String connectionID) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); User self = userContext.self(); // Retrieve permission sets SystemPermissionSet systemPermissions = self.getSystemPermissions(); ObjectPermissionSet connectionPermissions = self.getConnectionPermissions(); // Deny access if adminstrative or update permission is missing if (!systemPermissions.hasPermission(SystemPermission.Type.ADMINISTER) && !connectionPermissions.hasPermission(ObjectPermission.Type.UPDATE, connectionID)) throw new GuacamoleSecurityException("Permission to read connection parameters denied."); // Retrieve the requested connection Connection connection = retrievalService.retrieveConnection(userContext, connectionID); // Retrieve connection configuration GuacamoleConfiguration config = connection.getConfiguration(); // Return parameter map return config.getParameters(); } /** * Retrieves the usage history of a single connection. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext containing the connection whose history is to be * retrieved. * * @param connectionID * The identifier of the connection. * * @return * A list of connection records, describing the start and end times of * various usages of this connection. * * @throws GuacamoleException * If an error occurs while retrieving the connection history. */ @GET @Path("/{connectionID}/history") public List getConnectionHistory(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionID") String connectionID) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); // Retrieve the requested connection Connection connection = retrievalService.retrieveConnection(session, authProviderIdentifier, connectionID); // Retrieve the requested connection's history List apiRecords = new ArrayList(); for (ConnectionRecord record : connection.getHistory()) apiRecords.add(new APIConnectionRecord(record)); // Return the converted history return apiRecords; } /** * Deletes an individual connection. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext containing the connection to be deleted. * * @param connectionID * The identifier of the connection to delete. * * @throws GuacamoleException * If an error occurs while deleting the connection. */ @DELETE @Path("/{connectionID}") public void deleteConnection(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionID") String connectionID) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Get the connection directory Directory connectionDirectory = userContext.getConnectionDirectory(); // Delete the specified connection connectionDirectory.remove(connectionID); } /** * Creates a new connection and returns the new connection, with identifier * field populated. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext in which the connection is to be created. * * @param connection * The connection to create. * * @return * The new connection. * * @throws GuacamoleException * If an error occurs while creating the connection. */ @POST public APIConnection createConnection(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, APIConnection connection) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Validate that connection data was provided if (connection == null) throw new GuacamoleClientException("Connection JSON must be submitted when creating connections."); // Add the new connection Directory connectionDirectory = userContext.getConnectionDirectory(); connectionDirectory.add(new APIConnectionWrapper(connection)); // Return the new connection return connection; } /** * Updates an existing connection. If the parent identifier of the * connection is changed, the connection will also be moved to the new * parent group. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext containing the connection to be updated. * * @param connectionID * The identifier of the connection to update. * * @param connection * The connection data to update the specified connection with. * * @throws GuacamoleException * If an error occurs while updating the connection. */ @PUT @Path("/{connectionID}") public void updateConnection(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionID") String connectionID, APIConnection connection) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Validate that connection data was provided if (connection == null) throw new GuacamoleClientException("Connection JSON must be submitted when updating connections."); // Get the connection directory Directory connectionDirectory = userContext.getConnectionDirectory(); // Retrieve connection to update Connection existingConnection = retrievalService.retrieveConnection(userContext, connectionID); // Build updated configuration GuacamoleConfiguration config = new GuacamoleConfiguration(); config.setProtocol(connection.getProtocol()); config.setParameters(connection.getParameters()); // Update the connection existingConnection.setConfiguration(config); existingConnection.setParentIdentifier(connection.getParentIdentifier()); existingConnection.setName(connection.getName()); existingConnection.setAttributes(connection.getAttributes()); connectionDirectory.update(existingConnection); } } ././@LongLink0100644 0000000 0000000 00000000172 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/APIAuthenticationResponse.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/APIAuthen0100664 0001751 0001751 00000006506 12612226251 035157 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.auth; /** * A simple object to represent an auth token/username pair in the API. * * @author James Muehlner */ public class APIAuthenticationResponse { /** * The auth token. */ private final String authToken; /** * The username of the user that authenticated. */ private final String username; /** * The unique identifier of the data source from which this user account * came. Although this user account may exist across several data sources * (AuthenticationProviders), this will be the unique identifier of the * AuthenticationProvider that authenticated this user for the current * session. */ private final String dataSource; /** * Returns the unique authentication token which identifies the current * session. * * @return * The user's authentication token. */ public String getAuthToken() { return authToken; } /** * Returns the user identified by the authentication token associated with * the current session. * * @return * The user identified by this authentication token. */ public String getUsername() { return username; } /** * Returns the unique identifier of the data source associated with the user * account associated with this auth token. * * @return * The unique identifier of the data source associated with the user * account associated with this auth token. */ public String getDataSource() { return dataSource; } /** * Create a new APIAuthToken Object with the given auth token. * * @param dataSource * The unique identifier of the AuthenticationProvider which * authenticated the user. * * @param authToken * The auth token to create the new APIAuthToken with. * * @param username * The username of the user owning the given token. */ public APIAuthenticationResponse(String dataSource, String authToken, String username) { this.dataSource = dataSource; this.authToken = authToken; this.username = username; } } ././@LongLink0100644 0000000 0000000 00000000163 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/AuthTokenGenerator.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/AuthToken0100664 0001751 0001751 00000002623 12612226251 035277 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.auth; /** * Generates an auth token for an authenticated user. * * @author James Muehlner */ public interface AuthTokenGenerator { /** * Get a new auth token. * * @return A new auth token. */ public String getToken(); } ././@LongLink0100644 0000000 0000000 00000000177 12612226251 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/SecureRandomAuthTokenGenerator.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/SecureRan0100664 0001751 0001751 00000003356 12612226251 035270 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.auth; import java.security.SecureRandom; import org.apache.commons.codec.binary.Hex; /** * An implementation of the AuthTokenGenerator based around SecureRandom. * * @author James Muehlner */ public class SecureRandomAuthTokenGenerator implements AuthTokenGenerator { /** * Instance of SecureRandom for generating the auth token. */ private final SecureRandom secureRandom = new SecureRandom(); @Override public String getToken() { byte[] bytes = new byte[32]; secureRandom.nextBytes(bytes); return Hex.encodeHexString(bytes); } } ././@LongLink0100644 0000000 0000000 00000000161 12645510255 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenREST0100664 0001751 0001751 00000021147 12645510255 035163 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.auth; import com.google.inject.Inject; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.DELETE; import javax.ws.rs.FormParam; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.xml.bind.DatatypeConverter; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; import org.glyptodon.guacamole.net.auth.AuthenticatedUser; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.rest.APIRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A service for managing auth tokens via the Guacamole REST API. * * @author James Muehlner * @author Michael Jumper */ @Path("/tokens") @Produces(MediaType.APPLICATION_JSON) public class TokenRESTService { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(TokenRESTService.class); /** * Service for authenticating users and managing their Guacamole sessions. */ @Inject private AuthenticationService authenticationService; /** * Returns the credentials associated with the given request, using the * provided username and password. * * @param request * The request to use to derive the credentials. * * @param username * The username to associate with the credentials, or null if the * username should be derived from the request. * * @param password * The password to associate with the credentials, or null if the * password should be derived from the request. * * @return * A new Credentials object whose contents have been derived from the * given request, along with the provided username and password. */ private Credentials getCredentials(HttpServletRequest request, String username, String password) { // If no username/password given, try Authorization header if (username == null && password == null) { String authorization = request.getHeader("Authorization"); if (authorization != null && authorization.startsWith("Basic ")) { try { // Decode base64 authorization String basicBase64 = authorization.substring(6); String basicCredentials = new String(DatatypeConverter.parseBase64Binary(basicBase64), "UTF-8"); // Pull username/password from auth data int colon = basicCredentials.indexOf(':'); if (colon != -1) { username = basicCredentials.substring(0, colon); password = basicCredentials.substring(colon + 1); } else logger.debug("Invalid HTTP Basic \"Authorization\" header received."); } // UTF-8 support is required by the Java specification catch (UnsupportedEncodingException e) { throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e); } } } // end Authorization header fallback // Build credentials Credentials credentials = new Credentials(); credentials.setUsername(username); credentials.setPassword(password); credentials.setRequest(request); credentials.setSession(request.getSession(true)); return credentials; } /** * Authenticates a user, generates an auth token, associates that auth token * with the user's UserContext for use by further requests. If an existing * token is provided, the authentication procedure will attempt to update * or reuse the provided token. * * @param username * The username of the user who is to be authenticated. * * @param password * The password of the user who is to be authenticated. * * @param token * An optional existing auth token for the user who is to be * authenticated. * * @param consumedRequest * The HttpServletRequest associated with the login attempt. The * parameters of this request may not be accessible, as the request may * have been fully consumed by JAX-RS. * * @param parameters * A MultivaluedMap containing all parameters from the given HTTP * request. All request parameters must be made available through this * map, even if those parameters are no longer accessible within the * now-fully-consumed HTTP request. * * @return * An authentication response object containing the possible-new auth * token, as well as other related data. * * @throws GuacamoleException * If an error prevents successful authentication. */ @POST public APIAuthenticationResult createToken(@FormParam("username") String username, @FormParam("password") String password, @FormParam("token") String token, @Context HttpServletRequest consumedRequest, MultivaluedMap parameters) throws GuacamoleException { // Reconstitute the HTTP request with the map of parameters HttpServletRequest request = new APIRequest(consumedRequest, parameters); // Build credentials from request Credentials credentials = getCredentials(request, username, password); // Create/update session producing possibly-new token token = authenticationService.authenticate(credentials, token); // Pull corresponding session GuacamoleSession session = authenticationService.getGuacamoleSession(token); if (session == null) throw new GuacamoleResourceNotFoundException("No such token."); // Build list of all available auth providers List userContexts = session.getUserContexts(); List authProviderIdentifiers = new ArrayList(userContexts.size()); for (UserContext userContext : userContexts) authProviderIdentifiers.add(userContext.getAuthenticationProvider().getIdentifier()); // Return possibly-new auth token AuthenticatedUser authenticatedUser = session.getAuthenticatedUser(); return new APIAuthenticationResult( token, authenticatedUser.getIdentifier(), authenticatedUser.getAuthenticationProvider().getIdentifier(), authProviderIdentifiers ); } /** * Invalidates a specific auth token, effectively logging out the associated * user. * * @param authToken * The token being invalidated. * * @throws GuacamoleException * If the specified token does not exist. */ @DELETE @Path("/{token}") public void invalidateToken(@PathParam("token") String authToken) throws GuacamoleException { // Invalidate session, if it exists if (!authenticationService.destroyGuacamoleSession(authToken)) throw new GuacamoleResourceNotFoundException("No such token."); } } ././@LongLink0100644 0000000 0000000 00000000160 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenSessionMap.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenSess0100664 0001751 0001751 00000005214 12612226251 035312 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.auth; import org.glyptodon.guacamole.net.basic.GuacamoleSession; /** * Represents a mapping of auth token to Guacamole session for the REST * authentication system. * * @author James Muehlner */ public interface TokenSessionMap { /** * Registers that a user has just logged in with the specified authToken and * GuacamoleSession. * * @param authToken The authentication token for the logged in user. * @param session The GuacamoleSession for the logged in user. */ public void put(String authToken, GuacamoleSession session); /** * Get the GuacamoleSession for a logged in user. If the auth token does not * represent a user who is currently logged in, returns null. * * @param authToken The authentication token for the logged in user. * @return The GuacamoleSession for the given auth token, if the auth token * represents a currently logged in user, null otherwise. */ public GuacamoleSession get(String authToken); /** * Removes the GuacamoleSession associated with the given auth token. * * @param authToken The token to remove. * @return The GuacamoleSession for the given auth token, if the auth token * represents a currently logged in user, null otherwise. */ public GuacamoleSession remove(String authToken); /** * Shuts down this session map, disallowing future sessions and reclaiming * any resources. */ public void shutdown(); } ././@LongLink0100644 0000000 0000000 00000000170 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/APIAuthenticationResult.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/APIAuthen0100664 0001751 0001751 00000010471 12612226251 035153 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.auth; import java.util.Collections; import java.util.List; /** * A simple object which describes the result of an authentication operation, * including the resulting token. * * @author James Muehlner * @author Michael Jumper */ public class APIAuthenticationResult { /** * The unique token generated for the user that authenticated. */ private final String authToken; /** * The username of the user that authenticated. */ private final String username; /** * The unique identifier of the data source from which this user account * came. Although this user account may exist across several data sources * (AuthenticationProviders), this will be the unique identifier of the * AuthenticationProvider that authenticated this user for the current * session. */ private final String dataSource; /** * The identifiers of all data sources available to this user. */ private final List availableDataSources; /** * Returns the unique authentication token which identifies the current * session. * * @return * The user's authentication token. */ public String getAuthToken() { return authToken; } /** * Returns the user identified by the authentication token associated with * the current session. * * @return * The user identified by this authentication token. */ public String getUsername() { return username; } /** * Returns the unique identifier of the data source associated with the user * account associated with the current session. * * @return * The unique identifier of the data source associated with the user * account associated with the current session. */ public String getDataSource() { return dataSource; } /** * Returns the identifiers of all data sources available to the user * associated with the current session. * * @return * The identifiers of all data sources available to the user associated * with the current session. */ public List getAvailableDataSources() { return availableDataSources; } /** * Create a new APIAuthenticationResult object containing the given data. * * @param authToken * The unique token generated for the user that authenticated, to be * used for the duration of their session. * * @param username * The username of the user owning the given token. * * @param dataSource * The unique identifier of the AuthenticationProvider which * authenticated the user. * * @param availableDataSources * The unique identifier of all AuthenticationProviders to which the * user now has access. */ public APIAuthenticationResult(String authToken, String username, String dataSource, List availableDataSources) { this.authToken = authToken; this.username = username; this.dataSource = dataSource; this.availableDataSources = Collections.unmodifiableList(availableDataSources); } } ././@LongLink0100644 0000000 0000000 00000000166 12645510255 011643 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/AuthenticationService.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/Authentic0100664 0001751 0001751 00000042626 12645510255 035336 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.auth; import com.google.inject.Inject; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.GuacamoleUnauthorizedException; import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.net.auth.AuthenticatedUser; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo; import org.glyptodon.guacamole.net.auth.credentials.GuacamoleCredentialsException; import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A service for performing authentication checks in REST endpoints. * * @author James Muehlner * @author Michael Jumper */ public class AuthenticationService { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(AuthenticationService.class); /** * The Guacamole server environment. */ @Inject private Environment environment; /** * All configured authentication providers which can be used to * authenticate users or retrieve data associated with authenticated users. */ @Inject private List authProviders; /** * The map of auth tokens to sessions for the REST endpoints. */ @Inject private TokenSessionMap tokenSessionMap; /** * A generator for creating new auth tokens. */ @Inject private AuthTokenGenerator authTokenGenerator; /** * Regular expression which matches any IPv4 address. */ private static final String IPV4_ADDRESS_REGEX = "([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})"; /** * Regular expression which matches any IPv6 address. */ private static final String IPV6_ADDRESS_REGEX = "([0-9a-fA-F]*(:[0-9a-fA-F]*){0,7})"; /** * Regular expression which matches any IP address, regardless of version. */ private static final String IP_ADDRESS_REGEX = "(" + IPV4_ADDRESS_REGEX + "|" + IPV6_ADDRESS_REGEX + ")"; /** * Pattern which matches valid values of the de-facto standard * "X-Forwarded-For" header. */ private static final Pattern X_FORWARDED_FOR = Pattern.compile("^" + IP_ADDRESS_REGEX + "(, " + IP_ADDRESS_REGEX + ")*$"); /** * Returns a formatted string containing an IP address, or list of IP * addresses, which represent the HTTP client and any involved proxies. As * the headers used to determine proxies can easily be forged, this data is * superficially validated to ensure that it at least looks like a list of * IPs. * * @param request * The HTTP request to format. * * @return * A formatted string containing one or more IP addresses. */ private String getLoggableAddress(HttpServletRequest request) { // Log X-Forwarded-For, if present and valid String header = request.getHeader("X-Forwarded-For"); if (header != null && X_FORWARDED_FOR.matcher(header).matches()) return "[" + header + ", " + request.getRemoteAddr() + "]"; // If header absent or invalid, just use source IP return request.getRemoteAddr(); } /** * Attempts authentication against all AuthenticationProviders, in order, * using the provided credentials. The first authentication failure takes * priority, but remaining AuthenticationProviders are attempted. If any * AuthenticationProvider succeeds, the resulting AuthenticatedUser is * returned, and no further AuthenticationProviders are tried. * * @param credentials * The credentials to use for authentication. * * @return * The AuthenticatedUser given by the highest-priority * AuthenticationProvider for which the given credentials are valid. * * @throws GuacamoleException * If the given credentials are not valid for any * AuthenticationProvider, or if an error occurs while authenticating * the user. */ private AuthenticatedUser authenticateUser(Credentials credentials) throws GuacamoleException { GuacamoleCredentialsException authFailure = null; // Attempt authentication against each AuthenticationProvider for (AuthenticationProvider authProvider : authProviders) { // Attempt authentication try { AuthenticatedUser authenticatedUser = authProvider.authenticateUser(credentials); if (authenticatedUser != null) return authenticatedUser; } // First failure takes priority for now catch (GuacamoleCredentialsException e) { if (authFailure == null) authFailure = e; } } // If a specific failure occured, rethrow that if (authFailure != null) throw authFailure; // Otherwise, request standard username/password throw new GuacamoleInvalidCredentialsException( "Permission Denied.", CredentialsInfo.USERNAME_PASSWORD ); } /** * Re-authenticates the given AuthenticatedUser against the * AuthenticationProvider that originally created it, using the given * Credentials. * * @param authenticatedUser * The AuthenticatedUser to re-authenticate. * * @param credentials * The Credentials to use to re-authenticate the user. * * @return * A AuthenticatedUser which may have been updated due to re- * authentication. * * @throws GuacamoleException * If an error prevents the user from being re-authenticated. */ private AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser, Credentials credentials) throws GuacamoleException { // Get original AuthenticationProvider AuthenticationProvider authProvider = authenticatedUser.getAuthenticationProvider(); // Re-authenticate the AuthenticatedUser against the original AuthenticationProvider only authenticatedUser = authProvider.updateAuthenticatedUser(authenticatedUser, credentials); if (authenticatedUser == null) throw new GuacamoleSecurityException("User re-authentication failed."); return authenticatedUser; } /** * Returns the AuthenticatedUser associated with the given session and * credentials, performing a fresh authentication and creating a new * AuthenticatedUser if necessary. * * @param existingSession * The current GuacamoleSession, or null if no session exists yet. * * @param credentials * The Credentials to use to authenticate the user. * * @return * The AuthenticatedUser associated with the given session and * credentials. * * @throws GuacamoleException * If an error occurs while authenticating or re-authenticating the * user. */ private AuthenticatedUser getAuthenticatedUser(GuacamoleSession existingSession, Credentials credentials) throws GuacamoleException { try { // Re-authenticate user if session exists if (existingSession != null) return updateAuthenticatedUser(existingSession.getAuthenticatedUser(), credentials); // Otherwise, attempt authentication as a new user AuthenticatedUser authenticatedUser = AuthenticationService.this.authenticateUser(credentials); if (logger.isInfoEnabled()) logger.info("User \"{}\" successfully authenticated from {}.", authenticatedUser.getIdentifier(), getLoggableAddress(credentials.getRequest())); return authenticatedUser; } // Log and rethrow any authentication errors catch (GuacamoleException e) { // Get request and username for sake of logging HttpServletRequest request = credentials.getRequest(); String username = credentials.getUsername(); // Log authentication failures with associated usernames if (username != null) { if (logger.isWarnEnabled()) logger.warn("Authentication attempt from {} for user \"{}\" failed.", getLoggableAddress(request), username); } // Log anonymous authentication failures else if (logger.isDebugEnabled()) logger.debug("Anonymous authentication attempt from {} failed.", getLoggableAddress(request)); // Rethrow exception throw e; } } /** * Returns all UserContexts associated with the given AuthenticatedUser, * updating existing UserContexts, if any. If no UserContexts are yet * associated with the given AuthenticatedUser, new UserContexts are * generated by polling each available AuthenticationProvider. * * @param existingSession * The current GuacamoleSession, or null if no session exists yet. * * @param authenticatedUser * The AuthenticatedUser that has successfully authenticated or re- * authenticated. * * @return * A List of all UserContexts associated with the given * AuthenticatedUser. * * @throws GuacamoleException * If an error occurs while creating or updating any UserContext. */ private List getUserContexts(GuacamoleSession existingSession, AuthenticatedUser authenticatedUser) throws GuacamoleException { List userContexts = new ArrayList(authProviders.size()); // If UserContexts already exist, update them and add to the list if (existingSession != null) { // Update all old user contexts List oldUserContexts = existingSession.getUserContexts(); for (UserContext oldUserContext : oldUserContexts) { // Update existing UserContext AuthenticationProvider authProvider = oldUserContext.getAuthenticationProvider(); UserContext userContext = authProvider.updateUserContext(oldUserContext, authenticatedUser); // Add to available data, if successful if (userContext != null) userContexts.add(userContext); // If unsuccessful, log that this happened, as it may be a bug else logger.debug("AuthenticationProvider \"{}\" retroactively destroyed its UserContext.", authProvider.getClass().getName()); } } // Otherwise, create new UserContexts from available AuthenticationProviders else { // Get UserContexts from each available AuthenticationProvider for (AuthenticationProvider authProvider : authProviders) { // Generate new UserContext UserContext userContext = authProvider.getUserContext(authenticatedUser); // Add to available data, if successful if (userContext != null) userContexts.add(userContext); } } return userContexts; } /** * Authenticates a user using the given credentials and optional * authentication token, returning the authentication token associated with * the user's Guacamole session, which may be newly generated. If an * existing token is provided, the authentication procedure will attempt to * update or reuse the provided token, but it is possible that a new token * will be returned. Note that this function CANNOT return null. * * @param credentials * The credentials to use when authenticating the user. * * @param token * The authentication token to use if attempting to re-authenticate an * existing session, or null to request a new token. * * @return * The authentication token associated with the newly created or * existing session. * * @throws GuacamoleException * If the authentication or re-authentication attempt fails. */ public String authenticate(Credentials credentials, String token) throws GuacamoleException { // Pull existing session if token provided GuacamoleSession existingSession; if (token != null) existingSession = tokenSessionMap.get(token); else existingSession = null; // Get up-to-date AuthenticatedUser and associated UserContexts AuthenticatedUser authenticatedUser = getAuthenticatedUser(existingSession, credentials); List userContexts = getUserContexts(existingSession, authenticatedUser); // Update existing session, if it exists String authToken; if (existingSession != null) { authToken = token; existingSession.setAuthenticatedUser(authenticatedUser); existingSession.setUserContexts(userContexts); } // If no existing session, generate a new token/session pair else { authToken = authTokenGenerator.getToken(); tokenSessionMap.put(authToken, new GuacamoleSession(environment, authenticatedUser, userContexts)); logger.debug("Login was successful for user \"{}\".", authenticatedUser.getIdentifier()); } return authToken; } /** * Finds the Guacamole session for a given auth token, if the auth token * represents a currently logged in user. Throws an unauthorized error * otherwise. * * @param authToken The auth token to check against the map of logged in users. * @return The session that corresponds to the provided auth token. * @throws GuacamoleException If the auth token does not correspond to any * logged in user. */ public GuacamoleSession getGuacamoleSession(String authToken) throws GuacamoleException { // Try to get the session from the map of logged in users. GuacamoleSession session = tokenSessionMap.get(authToken); // Authentication failed. if (session == null) throw new GuacamoleUnauthorizedException("Permission Denied."); return session; } /** * Invalidates a specific authentication token and its corresponding * Guacamole session, effectively logging out the associated user. If the * authentication token is not valid, this function has no effect. * * @param authToken * The token being invalidated. * * @return * true if the given authentication token was valid and the * corresponding Guacamole session was destroyed, false if the given * authentication token was not valid and no action was taken. */ public boolean destroyGuacamoleSession(String authToken) { // Remove corresponding GuacamoleSession if the token is valid GuacamoleSession session = tokenSessionMap.remove(authToken); if (session == null) return false; // Invalidate the removed session session.invalidate(); return true; } /** * Returns all UserContexts associated with a given auth token, if the auth * token represents a currently logged in user. Throws an unauthorized * error otherwise. * * @param authToken * The auth token to check against the map of logged in users. * * @return * A List of all UserContexts associated with the provided auth token. * * @throws GuacamoleException * If the auth token does not correspond to any logged in user. */ public List getUserContexts(String authToken) throws GuacamoleException { return getGuacamoleSession(authToken).getUserContexts(); } } ././@LongLink0100644 0000000 0000000 00000000165 12645510255 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/BasicTokenSessionMap.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/BasicToke0100664 0001751 0001751 00000014567 12645510255 035261 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.auth; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A basic, HashMap-based implementation of the TokenSessionMap with support * for session timeouts. * * @author James Muehlner */ public class BasicTokenSessionMap implements TokenSessionMap { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(BasicTokenSessionMap.class); /** * Executor service which runs the period session eviction task. */ private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); /** * Keeps track of the authToken to GuacamoleSession mapping. */ private final ConcurrentMap sessionMap = new ConcurrentHashMap(); /** * Create a new BasicTokenGuacamoleSessionMap configured using the given * environment. * * @param environment * The environment to use when configuring the token session map. */ public BasicTokenSessionMap(Environment environment) { int sessionTimeoutValue; // Read session timeout from guacamole.properties try { sessionTimeoutValue = environment.getProperty(BasicGuacamoleProperties.API_SESSION_TIMEOUT, 60); } catch (GuacamoleException e) { logger.error("Unable to read guacamole.properties: {}", e.getMessage()); logger.debug("Error while reading session timeout value.", e); sessionTimeoutValue = 60; } // Check for expired sessions every minute logger.info("Sessions will expire after {} minutes of inactivity.", sessionTimeoutValue); executor.scheduleAtFixedRate(new SessionEvictionTask(sessionTimeoutValue * 60000l), 1, 1, TimeUnit.MINUTES); } /** * Task which iterates through all active sessions, evicting those sessions * which are beyond the session timeout. */ private class SessionEvictionTask implements Runnable { /** * The maximum allowed age of any session, in milliseconds. */ private final long sessionTimeout; /** * Creates a new task which automatically evicts sessions which are * older than the specified timeout. * * @param sessionTimeout The maximum age of any session, in * milliseconds. */ public SessionEvictionTask(long sessionTimeout) { this.sessionTimeout = sessionTimeout; } @Override public void run() { // Get start time of session check time long sessionCheckStart = System.currentTimeMillis(); logger.debug("Checking for expired sessions..."); // For each session, remove sesions which have expired Iterator> entries = sessionMap.entrySet().iterator(); while (entries.hasNext()) { Map.Entry entry = entries.next(); GuacamoleSession session = entry.getValue(); // Do not expire sessions which are active if (session.hasTunnels()) continue; // Get elapsed time since last access long age = sessionCheckStart - session.getLastAccessedTime(); // If session is too old, evict it and check the next one if (age >= sessionTimeout) { logger.debug("Session \"{}\" has timed out.", entry.getKey()); entries.remove(); session.invalidate(); } } // Log completion and duration logger.debug("Session check completed in {} ms.", System.currentTimeMillis() - sessionCheckStart); } } @Override public GuacamoleSession get(String authToken) { // There are no null auth tokens if (authToken == null) return null; // Update the last access time and return the GuacamoleSession GuacamoleSession session = sessionMap.get(authToken); if (session != null) session.access(); return session; } @Override public void put(String authToken, GuacamoleSession session) { sessionMap.put(authToken, session); } @Override public GuacamoleSession remove(String authToken) { // There are no null auth tokens if (authToken == null) return null; // Attempt to retrieve only if non-null return sessionMap.remove(authToken); } @Override public void shutdown() { executor.shutdownNow(); } } ././@LongLink0100644 0000000 0000000 00000000155 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/package-info.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/package-i0100664 0001751 0001751 00000002350 12612226251 035213 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes related to the authentication aspect of the Guacamole REST API. */ package org.glyptodon.guacamole.net.basic.rest.auth; ././@LongLink0100644 0000000 0000000 00000000164 12645510255 011641 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/schema/SchemaRESTService.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/schema/SchemaR0100664 0001751 0001751 00000017114 12645510255 035225 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.schema; import com.google.inject.Inject; import java.util.Collection; import java.util.Map; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.environment.LocalEnvironment; import org.glyptodon.guacamole.form.Form; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.glyptodon.guacamole.protocols.ProtocolInfo; /** * A REST service which provides access to descriptions of the properties, * attributes, etc. of objects used within the Guacamole REST API. * * @author Michael Jumper */ @Path("/schema/{dataSource}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class SchemaRESTService { /** * A service for authenticating users from auth tokens. */ @Inject private AuthenticationService authenticationService; /** * Service for convenient retrieval of objects. */ @Inject private ObjectRetrievalService retrievalService; /** * Retrieves the possible attributes of a user object. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext dictating the available user attributes. * * @return * A collection of forms which describe the possible attributes of a * user object. * * @throws GuacamoleException * If an error occurs while retrieving the possible attributes. */ @GET @Path("/users/attributes") public Collection
getUserAttributes(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier) throws GuacamoleException { // Retrieve all possible user attributes GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); return userContext.getUserAttributes(); } /** * Retrieves the possible attributes of a connection object. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext dictating the available connection attributes. * * @return * A collection of forms which describe the possible attributes of a * connection object. * * @throws GuacamoleException * If an error occurs while retrieving the possible attributes. */ @GET @Path("/connections/attributes") public Collection getConnectionAttributes(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier) throws GuacamoleException { // Retrieve all possible connection attributes GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); return userContext.getConnectionAttributes(); } /** * Retrieves the possible attributes of a connection group object. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext dictating the available connection group * attributes. * * @return * A collection of forms which describe the possible attributes of a * connection group object. * * @throws GuacamoleException * If an error occurs while retrieving the possible attributes. */ @GET @Path("/connectionGroups/attributes") public Collection getConnectionGroupAttributes(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier) throws GuacamoleException { // Retrieve all possible connection group attributes GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); return userContext.getConnectionGroupAttributes(); } /** * Gets a map of protocols defined in the system - protocol name to protocol. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext dictating the protocols available. Currently, the * UserContext actually does not dictate this, the the same set of * protocols will be retrieved for all users, though the identifier * given here will be validated. * * @return * A map of protocol information, where each key is the unique name * associated with that protocol. * * @throws GuacamoleException * If an error occurs while retrieving the available protocols. */ @GET @Path("/protocols") public Map getProtocols(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier) throws GuacamoleException { // Verify the given auth token and auth provider identifier are valid GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); retrievalService.retrieveUserContext(session, authProviderIdentifier); // Get and return a map of all protocols. Environment env = new LocalEnvironment(); return env.getProtocols(); } } ././@LongLink0100644 0000000 0000000 00000000157 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/schema/package-info.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/schema/package0100664 0001751 0001751 00000002454 12612226251 035271 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes related to the self-description of the Guacamole REST API, such as * the attributes or parameters applicable to specific objects. */ package org.glyptodon.guacamole.net.basic.rest.schema; ././@LongLink0100644 0000000 0000000 00000000155 12645510255 011641 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTMethodMatcher.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTMethodMatc0100664 0001751 0001751 00000007433 12645510255 035171 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest; import com.google.inject.matcher.AbstractMatcher; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import javax.ws.rs.HttpMethod; import org.glyptodon.guacamole.GuacamoleException; /** * A Guice Matcher which matches only methods which throw GuacamoleException * (or a subclass thereof) and are explicitly annotated as with an HTTP method * annotation like @GET or @POST. Any method which * throws GuacamoleException and is annotated with an annotation that is * annotated with @HttpMethod will match. * * @author Michael Jumper */ public class RESTMethodMatcher extends AbstractMatcher { /** * Returns whether the given method throws the specified exception type, * including any subclasses of that type. * * @param method * The method to test. * * @param exceptionType * The exception type to test for. * * @return * true if the given method throws an exception of the specified type, * false otherwise. */ private boolean methodThrowsException(Method method, Class exceptionType) { // Check whether the method throws an exception of the specified type for (Class thrownType : method.getExceptionTypes()) { if (exceptionType.isAssignableFrom(thrownType)) return true; } // No such exception is declared to be thrown return false; } /** * Returns whether the given method is annotated as a REST method. A REST * method is annotated with an annotation which is annotated with * @HttpMethod. * * @param method * The method to test. * * @return * true if the given method is annotated as a REST method, false * otherwise. */ private boolean isRESTMethod(Method method) { // Check whether the required REST annotations are present for (Annotation annotation : method.getAnnotations()) { // A method is a REST method if it is annotated with @HttpMethod Class annotationType = annotation.annotationType(); if (annotationType.isAnnotationPresent(HttpMethod.class)) return true; } // The method is not an HTTP method return false; } @Override public boolean matches(Method method) { // Guacamole REST methods are REST methods which throw // GuacamoleExceptions return isRESTMethod(method) && methodThrowsException(method, GuacamoleException.class); } } ././@LongLink0100644 0000000 0000000 00000000167 12612226251 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/APIPermissionSet.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/API0100664 0001751 0001751 00000027507 12612226251 035245 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.permission; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.Set; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.User; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; /** * The set of permissions which are granted to a specific user, organized by * object type and, if applicable, identifier. This object can be constructed * with arbitrary permissions present, or manipulated after creation through * the manipulation or replacement of its collections of permissions, but is * otherwise not intended for internal use as a data structure for permissions. * Its primary purpose is as a hierarchical format for exchanging granted * permissions with REST clients. */ public class APIPermissionSet { /** * Map of connection ID to the set of granted permissions. */ private Map> connectionPermissions = new HashMap>(); /** * Map of connection group ID to the set of granted permissions. */ private Map> connectionGroupPermissions = new HashMap>(); /** * Map of active connection ID to the set of granted permissions. */ private Map> activeConnectionPermissions = new HashMap>(); /** * Map of user ID to the set of granted permissions. */ private Map> userPermissions = new HashMap>(); /** * Set of all granted system-level permissions. */ private Set systemPermissions = EnumSet.noneOf(SystemPermission.Type.class); /** * Creates a new permission set which contains no granted permissions. Any * permissions must be added by manipulating or replacing the applicable * permission collection. */ public APIPermissionSet() { } /** * Adds the system permissions from the given SystemPermissionSet to the * Set of system permissions provided. * * @param permissions * The Set to add system permissions to. * * @param permSet * The SystemPermissionSet containing the system permissions to add. * * @throws GuacamoleException * If an error occurs while retrieving system permissions from the * SystemPermissionSet. */ private void addSystemPermissions(Set permissions, SystemPermissionSet permSet) throws GuacamoleException { // Add all provided system permissions for (SystemPermission permission : permSet.getPermissions()) permissions.add(permission.getType()); } /** * Adds the object permissions from the given ObjectPermissionSet to the * Map of object permissions provided. * * @param permissions * The Map to add object permissions to. * * @param permSet * The ObjectPermissionSet containing the object permissions to add. * * @throws GuacamoleException * If an error occurs while retrieving object permissions from the * ObjectPermissionSet. */ private void addObjectPermissions(Map> permissions, ObjectPermissionSet permSet) throws GuacamoleException { // Add all provided object permissions for (ObjectPermission permission : permSet.getPermissions()) { // Get associated set of permissions String identifier = permission.getObjectIdentifier(); Set objectPermissions = permissions.get(identifier); // Create new set if none yet exists if (objectPermissions == null) permissions.put(identifier, EnumSet.of(permission.getType())); // Otherwise add to existing set else objectPermissions.add(permission.getType()); } } /** * Creates a new permission set containing all permissions currently * granted to the given user. * * @param user * The user whose permissions should be stored within this permission * set. * * @throws GuacamoleException * If an error occurs while retrieving the user's permissions. */ public APIPermissionSet(User user) throws GuacamoleException { // Add all permissions from the provided user addSystemPermissions(systemPermissions, user.getSystemPermissions()); addObjectPermissions(connectionPermissions, user.getConnectionPermissions()); addObjectPermissions(connectionGroupPermissions, user.getConnectionGroupPermissions()); addObjectPermissions(activeConnectionPermissions, user.getActiveConnectionPermissions()); addObjectPermissions(userPermissions, user.getUserPermissions()); } /** * Returns a map of connection IDs to the set of permissions granted for * that connection. If no permissions are granted to a particular * connection, its ID will not be present as a key in the map. This map is * mutable, and changes to this map will affect the permission set * directly. * * @return * A map of connection IDs to the set of permissions granted for that * connection. */ public Map> getConnectionPermissions() { return connectionPermissions; } /** * Returns a map of connection group IDs to the set of permissions granted * for that connection group. If no permissions are granted to a particular * connection group, its ID will not be present as a key in the map. This * map is mutable, and changes to this map will affect the permission set * directly. * * @return * A map of connection group IDs to the set of permissions granted for * that connection group. */ public Map> getConnectionGroupPermissions() { return connectionGroupPermissions; } /** * Returns a map of active connection IDs to the set of permissions granted * for that active connection. If no permissions are granted to a particular * active connection, its ID will not be present as a key in the map. This * map is mutable, and changes to this map will affect the permission set * directly. * * @return * A map of active connection IDs to the set of permissions granted for * that active connection. */ public Map> getActiveConnectionPermissions() { return activeConnectionPermissions; } /** * Returns a map of user IDs to the set of permissions granted for that * user. If no permissions are granted to a particular user, its ID will * not be present as a key in the map. This map is mutable, and changes to * to this map will affect the permission set directly. * * @return * A map of user IDs to the set of permissions granted for that user. */ public Map> getUserPermissions() { return userPermissions; } /** * Returns the set of granted system-level permissions. If no permissions * are granted at the system level, this will be an empty set. This set is * mutable, and changes to this set will affect the permission set * directly. * * @return * The set of granted system-level permissions. */ public Set getSystemPermissions() { return systemPermissions; } /** * Replaces the current map of connection permissions with the given map, * which must map connection ID to its corresponding set of granted * permissions. If a connection has no permissions, its ID must not be * present as a key in the map. * * @param connectionPermissions * The map which must replace the currently-stored map of permissions. */ public void setConnectionPermissions(Map> connectionPermissions) { this.connectionPermissions = connectionPermissions; } /** * Replaces the current map of connection group permissions with the given * map, which must map connection group ID to its corresponding set of * granted permissions. If a connection group has no permissions, its ID * must not be present as a key in the map. * * @param connectionGroupPermissions * The map which must replace the currently-stored map of permissions. */ public void setConnectionGroupPermissions(Map> connectionGroupPermissions) { this.connectionGroupPermissions = connectionGroupPermissions; } /** * Replaces the current map of active connection permissions with the give * map, which must map active connection ID to its corresponding set of * granted permissions. If an active connection has no permissions, its ID * must not be present as a key in the map. * * @param activeConnectionPermissions * The map which must replace the currently-stored map of permissions. */ public void setActiveConnectionPermissions(Map> activeConnectionPermissions) { this.activeConnectionPermissions = activeConnectionPermissions; } /** * Replaces the current map of user permissions with the given map, which * must map user ID to its corresponding set of granted permissions. If a * user has no permissions, its ID must not be present as a key in the map. * * @param userPermissions * The map which must replace the currently-stored map of permissions. */ public void setUserPermissions(Map> userPermissions) { this.userPermissions = userPermissions; } /** * Replaces the current set of system-level permissions with the given set. * If no system-level permissions are granted, the empty set must be * specified. * * @param systemPermissions * The set which must replace the currently-stored set of permissions. */ public void setSystemPermissions(Set systemPermissions) { this.systemPermissions = systemPermissions; } } ././@LongLink0100644 0000000 0000000 00000000163 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/package-info.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/pac0100664 0001751 0001751 00000002367 12612226251 035374 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes related to the permission manipulation aspect of the Guacamole REST API. */ package org.glyptodon.guacamole.net.basic.rest.permission; ././@LongLink0100644 0000000 0000000 00000000150 12612226251 011626 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/package-info.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/package-info.j0100664 0001751 0001751 00000002314 12612226251 035205 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes related to the basic Guacamole REST API. */ package org.glyptodon.guacamole.net.basic.rest; ././@LongLink0100644 0000000 0000000 00000000177 12612226251 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupTree.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongrou0100664 0001751 0001751 00000023540 12612226251 035471 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.connectiongroup; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.basic.rest.connection.APIConnection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Provides access to the entire tree of connection groups and their * connections. * * @author Michael Jumper */ public class ConnectionGroupTree { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(ConnectionGroupTree.class); /** * The context of the user obtaining this tree. */ private final UserContext userContext; /** * The root connection group as an APIConnectionGroup. */ private final APIConnectionGroup rootAPIGroup; /** * All connection groups that have been retrieved, stored by their * identifiers. */ private final Map retrievedGroups = new HashMap(); /** * Adds each of the provided connections to the current tree as children * of their respective parents. The parent connection groups must already * be added. * * @param connections * The connections to add to the tree. * * @throws GuacamoleException * If an error occurs while adding the connection to the tree. */ private void addConnections(Collection connections) throws GuacamoleException { // Add each connection to the tree for (Connection connection : connections) { // Retrieve the connection's parent group APIConnectionGroup parent = retrievedGroups.get(connection.getParentIdentifier()); if (parent != null) { Collection children = parent.getChildConnections(); // Create child collection if it does not yet exist if (children == null) { children = new ArrayList(); parent.setChildConnections(children); } // Add child children.add(new APIConnection(connection)); } // Warn of internal consistency issues else logger.debug("Connection \"{}\" cannot be added to the tree: parent \"{}\" does not actually exist.", connection.getIdentifier(), connection.getParentIdentifier()); } // end for each connection } /** * Adds each of the provided connection groups to the current tree as * children of their respective parents. The parent connection groups must * already be added. * * @param connectionGroups * The connection groups to add to the tree. */ private void addConnectionGroups(Collection connectionGroups) { // Add each connection group to the tree for (ConnectionGroup connectionGroup : connectionGroups) { // Retrieve the connection group's parent group APIConnectionGroup parent = retrievedGroups.get(connectionGroup.getParentIdentifier()); if (parent != null) { Collection children = parent.getChildConnectionGroups(); // Create child collection if it does not yet exist if (children == null) { children = new ArrayList(); parent.setChildConnectionGroups(children); } // Add child APIConnectionGroup apiConnectionGroup = new APIConnectionGroup(connectionGroup); retrievedGroups.put(connectionGroup.getIdentifier(), apiConnectionGroup); children.add(apiConnectionGroup); } // Warn of internal consistency issues else logger.debug("Connection group \"{}\" cannot be added to the tree: parent \"{}\" does not actually exist.", connectionGroup.getIdentifier(), connectionGroup.getParentIdentifier()); } // end for each connection group } /** * Adds all descendants of the given parent groups to their corresponding * parents already stored under root. * * @param parents * The parents whose descendants should be added to the tree. * * @param permissions * If specified and non-empty, limit added connections to only * connections for which the current user has any of the given * permissions. Otherwise, all visible connections are added. * Connection groups are unaffected by this parameter. * * @throws GuacamoleException * If an error occurs while retrieving the descendants. */ private void addDescendants(Collection parents, List permissions) throws GuacamoleException { // If no parents, nothing to do if (parents.isEmpty()) return; Collection childConnectionIdentifiers = new ArrayList(); Collection childConnectionGroupIdentifiers = new ArrayList(); // Build lists of identifiers for retrieval for (ConnectionGroup parent : parents) { childConnectionIdentifiers.addAll(parent.getConnectionIdentifiers()); childConnectionGroupIdentifiers.addAll(parent.getConnectionGroupIdentifiers()); } // Filter identifiers based on permissions, if requested if (permissions != null && !permissions.isEmpty()) { ObjectPermissionSet permissionSet = userContext.self().getConnectionPermissions(); childConnectionIdentifiers = permissionSet.getAccessibleObjects(permissions, childConnectionIdentifiers); } // Retrieve child connections if (!childConnectionIdentifiers.isEmpty()) { Collection childConnections = userContext.getConnectionDirectory().getAll(childConnectionIdentifiers); addConnections(childConnections); } // Retrieve child connection groups if (!childConnectionGroupIdentifiers.isEmpty()) { Collection childConnectionGroups = userContext.getConnectionGroupDirectory().getAll(childConnectionGroupIdentifiers); addConnectionGroups(childConnectionGroups); addDescendants(childConnectionGroups, permissions); } } /** * Creates a new connection group tree using the given connection group as * the tree root. * * @param userContext * The context of the user obtaining the connection group tree. * * @param root * The connection group to use as the root of this connection group * tree. * * @param permissions * If specified and non-empty, limit the contents of the tree to only * those connections for which the current user has any of the given * permissions. Otherwise, all visible connections are returned. * Connection groups are unaffected by this parameter. * * @throws GuacamoleException * If an error occurs while retrieving the tree of connection groups * and their descendants. */ public ConnectionGroupTree(UserContext userContext, ConnectionGroup root, List permissions) throws GuacamoleException { this.userContext = userContext; // Store root of tree this.rootAPIGroup = new APIConnectionGroup(root); retrievedGroups.put(root.getIdentifier(), this.rootAPIGroup); // Add all descendants addDescendants(Collections.singleton(root), permissions); } /** * Returns the entire connection group tree as an APIConnectionGroup. The * returned APIConnectionGroup is the root group and will contain all * descendant connection groups and connections, arranged hierarchically. * * @return * The root connection group, containing the entire connection group * tree and all connections. */ public APIConnectionGroup getRootAPIConnectionGroup() { return rootAPIGroup; } } ././@LongLink0100644 0000000 0000000 00000000206 12645510255 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongrou0100664 0001751 0001751 00000026572 12645510255 035507 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.connectiongroup; import com.google.inject.Inject; import java.util.List; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A REST Service for handling connection group CRUD operations. * * @author James Muehlner */ @Path("/data/{dataSource}/connectionGroups") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class ConnectionGroupRESTService { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(ConnectionGroupRESTService.class); /** * A service for authenticating users from auth tokens. */ @Inject private AuthenticationService authenticationService; /** * Service for convenient retrieval of objects. */ @Inject private ObjectRetrievalService retrievalService; /** * Gets an individual connection group. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext containing the connection group to be retrieved. * * @param connectionGroupID * The ID of the connection group to retrieve. * * @return * The connection group, without any descendants. * * @throws GuacamoleException * If a problem is encountered while retrieving the connection group. */ @GET @Path("/{connectionGroupID}") public APIConnectionGroup getConnectionGroup(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionGroupID") String connectionGroupID) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); // Retrieve the requested connection group return new APIConnectionGroup(retrievalService.retrieveConnectionGroup(session, authProviderIdentifier, connectionGroupID)); } /** * Gets an individual connection group and all children. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext containing the connection group to be retrieved. * * @param connectionGroupID * The ID of the connection group to retrieve. * * @param permissions * If specified and non-empty, limit the returned list to only those * connections for which the current user has any of the given * permissions. Otherwise, all visible connections are returned. * Connection groups are unaffected by this parameter. * * @return * The requested connection group, including all descendants. * * @throws GuacamoleException * If a problem is encountered while retrieving the connection group or * its descendants. */ @GET @Path("/{connectionGroupID}/tree") public APIConnectionGroup getConnectionGroupTree(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionGroupID") String connectionGroupID, @QueryParam("permission") List permissions) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Retrieve the requested tree, filtering by the given permissions ConnectionGroup treeRoot = retrievalService.retrieveConnectionGroup(userContext, connectionGroupID); ConnectionGroupTree tree = new ConnectionGroupTree(userContext, treeRoot, permissions); // Return tree as a connection group return tree.getRootAPIConnectionGroup(); } /** * Deletes an individual connection group. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext containing the connection group to be deleted. * * @param connectionGroupID * The identifier of the connection group to delete. * * @throws GuacamoleException * If an error occurs while deleting the connection group. */ @DELETE @Path("/{connectionGroupID}") public void deleteConnectionGroup(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionGroupID") String connectionGroupID) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Get the connection group directory Directory connectionGroupDirectory = userContext.getConnectionGroupDirectory(); // Delete the connection group connectionGroupDirectory.remove(connectionGroupID); } /** * Creates a new connection group and returns the new connection group, * with identifier field populated. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext in which the connection group is to be created. * * @param connectionGroup * The connection group to create. * * @return * The new connection group. * * @throws GuacamoleException * If an error occurs while creating the connection group. */ @POST public APIConnectionGroup createConnectionGroup( @QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, APIConnectionGroup connectionGroup) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Validate that connection group data was provided if (connectionGroup == null) throw new GuacamoleClientException("Connection group JSON must be submitted when creating connections groups."); // Add the new connection group Directory connectionGroupDirectory = userContext.getConnectionGroupDirectory(); connectionGroupDirectory.add(new APIConnectionGroupWrapper(connectionGroup)); // Return the new connection group return connectionGroup; } /** * Updates a connection group. If the parent identifier of the * connection group is changed, the connection group will also be moved to * the new parent group. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext containing the connection group to be updated. * * @param connectionGroupID * The identifier of the existing connection group to update. * * @param connectionGroup * The data to update the existing connection group with. * * @throws GuacamoleException * If an error occurs while updating the connection group. */ @PUT @Path("/{connectionGroupID}") public void updateConnectionGroup(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionGroupID") String connectionGroupID, APIConnectionGroup connectionGroup) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Validate that connection group data was provided if (connectionGroup == null) throw new GuacamoleClientException("Connection group JSON must be submitted when updating connection groups."); // Get the connection group directory Directory connectionGroupDirectory = userContext.getConnectionGroupDirectory(); // Retrieve connection group to update ConnectionGroup existingConnectionGroup = retrievalService.retrieveConnectionGroup(userContext, connectionGroupID); // Update the connection group existingConnectionGroup.setName(connectionGroup.getName()); existingConnectionGroup.setParentIdentifier(connectionGroup.getParentIdentifier()); existingConnectionGroup.setType(connectionGroup.getType()); existingConnectionGroup.setAttributes(connectionGroup.getAttributes()); connectionGroupDirectory.update(existingConnectionGroup); } } ././@LongLink0100644 0000000 0000000 00000000170 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/package-info.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongrou0100664 0001751 0001751 00000002405 12612226251 035466 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes related to the connection group manipulation aspect * of the Guacamole REST API. */ package org.glyptodon.guacamole.net.basic.rest.connectiongroup; ././@LongLink0100644 0000000 0000000 00000000176 12612226251 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/APIConnectionGroup.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongrou0100664 0001751 0001751 00000020575 12612226251 035476 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.connectiongroup; import java.util.Collection; import java.util.Map; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.map.annotate.JsonSerialize; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.ConnectionGroup.Type; import org.glyptodon.guacamole.net.basic.rest.connection.APIConnection; /** * A simple connection group to expose through the REST endpoints. * * @author James Muehlner */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) public class APIConnectionGroup { /** * The identifier of the root connection group. */ public static final String ROOT_IDENTIFIER = "ROOT"; /** * The name of this connection group. */ private String name; /** * The identifier of this connection group. */ private String identifier; /** * The identifier of the parent connection group for this connection group. */ private String parentIdentifier; /** * The type of this connection group. */ private Type type; /** * The count of currently active connections using this connection group. */ private int activeConnections; /** * All child connection groups. If children are not being queried, this may * be omitted. */ private Collection childConnectionGroups; /** * All child connections. If children are not being queried, this may be * omitted. */ private Collection childConnections; /** * Map of all associated attributes by attribute identifier. */ private Map attributes; /** * Create an empty APIConnectionGroup. */ public APIConnectionGroup() {} /** * Create a new APIConnectionGroup from the given ConnectionGroup record. * * @param connectionGroup The ConnectionGroup record to initialize this * APIConnectionGroup from. */ public APIConnectionGroup(ConnectionGroup connectionGroup) { // Set connection group information this.identifier = connectionGroup.getIdentifier(); this.parentIdentifier = connectionGroup.getParentIdentifier(); this.name = connectionGroup.getName(); this.type = connectionGroup.getType(); this.activeConnections = connectionGroup.getActiveConnections(); // Associate any attributes this.attributes = connectionGroup.getAttributes(); } /** * Returns the name of this connection group. * @return The name of this connection group. */ public String getName() { return name; } /** * Set the name of this connection group. * @param name The name of this connection group. */ public void setName(String name) { this.name = name; } /** * Returns the identifier of this connection group. * @return The identifier of this connection group. */ public String getIdentifier() { return identifier; } /** * Set the identifier of this connection group. * @param identifier The identifier of this connection group. */ public void setIdentifier(String identifier) { this.identifier = identifier; } /** * Returns the unique identifier for this connection group. * @return The unique identifier for this connection group. */ public String getParentIdentifier() { return parentIdentifier; } /** * Sets the parent connection group identifier for this connection group. * @param parentIdentifier The parent connection group identifier * for this connection group. */ public void setParentIdentifier(String parentIdentifier) { this.parentIdentifier = parentIdentifier; } /** * Returns the type of this connection group. * @return The type of this connection group. */ public Type getType() { return type; } /** * Set the type of this connection group. * @param type The Type of this connection group. */ public void setType(Type type) { this.type = type; } /** * Returns a collection of all child connection groups, or null if children * have not been queried. * * @return * A collection of all child connection groups, or null if children * have not been queried. */ public Collection getChildConnectionGroups() { return childConnectionGroups; } /** * Sets the collection of all child connection groups to the given * collection, which may be null if children have not been queried. * * @param childConnectionGroups * The collection containing all child connection groups of this * connection group, or null if children have not been queried. */ public void setChildConnectionGroups(Collection childConnectionGroups) { this.childConnectionGroups = childConnectionGroups; } /** * Returns a collection of all child connections, or null if children have * not been queried. * * @return * A collection of all child connections, or null if children have not * been queried. */ public Collection getChildConnections() { return childConnections; } /** * Sets the collection of all child connections to the given collection, * which may be null if children have not been queried. * * @param childConnections * The collection containing all child connections of this connection * group, or null if children have not been queried. */ public void setChildConnections(Collection childConnections) { this.childConnections = childConnections; } /** * Returns the number of currently active connections using this * connection group. * * @return * The number of currently active usages of this connection group. */ public int getActiveConnections() { return activeConnections; } /** * Set the number of currently active connections using this connection * group. * * @param activeConnections * The number of currently active usages of this connection group. */ public void setActiveUsers(int activeConnections) { this.activeConnections = activeConnections; } /** * Returns a map of all attributes associated with this connection group. * Each entry key is the attribute identifier, while each value is the * attribute value itself. * * @return * The attribute map for this connection group. */ public Map getAttributes() { return attributes; } /** * Sets the map of all attributes associated with this connection group. * Each entry key is the attribute identifier, while each value is the * attribute value itself. * * @param attributes * The attribute map for this connection group. */ public void setAttributes(Map attributes) { this.attributes = attributes; } } ././@LongLink0100644 0000000 0000000 00000000205 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/APIConnectionGroupWrapper.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongrou0100664 0001751 0001751 00000007545 12612226251 035500 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.connectiongroup; import java.util.Map; import java.util.Set; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; /** * A wrapper to make an APIConnection look like a ConnectionGroup. * Useful where a org.glyptodon.guacamole.net.auth.ConnectionGroup is required. * * @author James Muehlner */ public class APIConnectionGroupWrapper implements ConnectionGroup { /** * The wrapped APIConnectionGroup. */ private final APIConnectionGroup apiConnectionGroup; /** * Create a new APIConnectionGroupWrapper to wrap the given * APIConnectionGroup as a ConnectionGroup. * @param apiConnectionGroup the APIConnectionGroup to wrap. */ public APIConnectionGroupWrapper(APIConnectionGroup apiConnectionGroup) { this.apiConnectionGroup = apiConnectionGroup; } @Override public String getName() { return apiConnectionGroup.getName(); } @Override public void setName(String name) { apiConnectionGroup.setName(name); } @Override public String getIdentifier() { return apiConnectionGroup.getIdentifier(); } @Override public void setIdentifier(String identifier) { apiConnectionGroup.setIdentifier(identifier); } @Override public String getParentIdentifier() { return apiConnectionGroup.getParentIdentifier(); } @Override public void setParentIdentifier(String parentIdentifier) { apiConnectionGroup.setParentIdentifier(parentIdentifier); } @Override public void setType(Type type) { apiConnectionGroup.setType(type); } @Override public Type getType() { return apiConnectionGroup.getType(); } @Override public int getActiveConnections() { return apiConnectionGroup.getActiveConnections(); } @Override public Set getConnectionIdentifiers() { throw new UnsupportedOperationException("Operation not supported."); } @Override public Set getConnectionGroupIdentifiers() { throw new UnsupportedOperationException("Operation not supported."); } @Override public Map getAttributes() { return apiConnectionGroup.getAttributes(); } @Override public void setAttributes(Map attributes) { apiConnectionGroup.setAttributes(attributes); } @Override public GuacamoleTunnel connect(GuacamoleClientInformation info) throws GuacamoleException { throw new UnsupportedOperationException("Operation not supported."); } } ././@LongLink0100644 0000000 0000000 00000000200 12612226251 011622 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/APIActiveConnection.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnecti0100664 0001751 0001751 00000007670 12612226251 035441 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.activeconnection; import java.util.Date; import org.glyptodon.guacamole.net.auth.ActiveConnection; /** * Information related to active connections which may be exposed through the * REST endpoints. * * @author Michael Jumper */ public class APIActiveConnection { /** * The identifier of the active connection itself. */ private final String identifier; /** * The identifier of the connection associated with this * active connection. */ private final String connectionIdentifier; /** * The date and time the connection began. */ private final Date startDate; /** * The host from which the connection originated, if known. */ private final String remoteHost; /** * The name of the user who used or is using the connection. */ private final String username; /** * Creates a new APIActiveConnection, copying the information from the given * active connection. * * @param connection * The active connection to copy data from. */ public APIActiveConnection(ActiveConnection connection) { this.identifier = connection.getIdentifier(); this.connectionIdentifier = connection.getConnectionIdentifier(); this.startDate = connection.getStartDate(); this.remoteHost = connection.getRemoteHost(); this.username = connection.getUsername(); } /** * Returns the identifier of the connection associated with this tunnel. * * @return * The identifier of the connection associated with this tunnel. */ public String getConnectionIdentifier() { return connectionIdentifier; } /** * Returns the date and time the connection began. * * @return * The date and time the connection began. */ public Date getStartDate() { return startDate; } /** * Returns the remote host from which this connection originated. * * @return * The remote host from which this connection originated. */ public String getRemoteHost() { return remoteHost; } /** * Returns the name of the user who used or is using the connection at the * times given by this tunnel. * * @return * The name of the user who used or is using the associated connection. */ public String getUsername() { return username; } /** * Returns the identifier of the active connection itself. This is * distinct from the connection identifier, and uniquely identifies a * specific use of a connection. * * @return * The identifier of the active connection. */ public String getIdentifier() { return identifier; } } ././@LongLink0100644 0000000 0000000 00000000210 12645510255 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnecti0100664 0001751 0001751 00000020464 12645510255 035443 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.activeconnection; import com.google.inject.Inject; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleUnsupportedException; import org.glyptodon.guacamole.net.auth.ActiveConnection; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.User; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.rest.APIPatch; import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.PATCH; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A REST Service for retrieving and managing the tunnels of active connections. * * @author Michael Jumper */ @Path("/data/{dataSource}/activeConnections") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class ActiveConnectionRESTService { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(ActiveConnectionRESTService.class); /** * A service for authenticating users from auth tokens. */ @Inject private AuthenticationService authenticationService; /** * Service for convenient retrieval of objects. */ @Inject private ObjectRetrievalService retrievalService; /** * Gets a list of active connections in the system, filtering the returned * list by the given permissions, if specified. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext containing the active connections to be retrieved. * * @param permissions * The set of permissions to filter with. A user must have one or more * of these permissions for a user to appear in the result. * If null, no filtering will be performed. * * @return * A list of all active connections. If a permission was specified, * this list will contain only those active connections for which the * current user has that permission. * * @throws GuacamoleException * If an error is encountered while retrieving active connections. */ @GET public Map getActiveConnections(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @QueryParam("permission") List permissions) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); User self = userContext.self(); // Do not filter on permissions if no permissions are specified if (permissions != null && permissions.isEmpty()) permissions = null; // An admin user has access to any connection SystemPermissionSet systemPermissions = self.getSystemPermissions(); boolean isAdmin = systemPermissions.hasPermission(SystemPermission.Type.ADMINISTER); // Get the directory Directory activeConnectionDirectory = userContext.getActiveConnectionDirectory(); // Filter connections, if requested Collection activeConnectionIdentifiers = activeConnectionDirectory.getIdentifiers(); if (!isAdmin && permissions != null) { ObjectPermissionSet activeConnectionPermissions = self.getActiveConnectionPermissions(); activeConnectionIdentifiers = activeConnectionPermissions.getAccessibleObjects(permissions, activeConnectionIdentifiers); } // Retrieve all active connections , converting to API active connections Map apiActiveConnections = new HashMap(); for (ActiveConnection activeConnection : activeConnectionDirectory.getAll(activeConnectionIdentifiers)) apiActiveConnections.put(activeConnection.getIdentifier(), new APIActiveConnection(activeConnection)); return apiActiveConnections; } /** * Applies the given active connection patches. This operation currently * only supports deletion of active connections through the "remove" patch * operation. Deleting an active connection effectively kills the * connection. The path of each patch operation is of the form "/ID" * where ID is the identifier of the active connection being modified. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext containing the active connections to be deleted. * * @param patches * The active connection patches to apply for this request. * * @throws GuacamoleException * If an error occurs while deleting the active connections. */ @PATCH public void patchTunnels(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, List> patches) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Get the directory Directory activeConnectionDirectory = userContext.getActiveConnectionDirectory(); // Close each connection listed for removal for (APIPatch patch : patches) { // Only remove is supported if (patch.getOp() != APIPatch.Operation.remove) throw new GuacamoleUnsupportedException("Only the \"remove\" operation is supported when patching active connections."); // Retrieve and validate path String path = patch.getPath(); if (!path.startsWith("/")) throw new GuacamoleClientException("Patch paths must start with \"/\"."); // Close connection activeConnectionDirectory.remove(path.substring(1)); } } } ././@LongLink0100644 0000000 0000000 00000000162 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/ObjectRetrievalService.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/ObjectRetrieva0100664 0001751 0001751 00000025121 12612226251 035342 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest; import java.util.List; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.User; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.rest.connectiongroup.APIConnectionGroup; /** * Provides easy access and automatic error handling for retrieval of objects, * such as users, connections, or connection groups. REST API semantics, such * as the special root connection group identifier, are also handled * automatically. */ public class ObjectRetrievalService { /** * Retrieves a single UserContext from the given GuacamoleSession, which * may contain multiple UserContexts. * * @param session * The GuacamoleSession to retrieve the UserContext from. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider that created the * UserContext being retrieved. Only one UserContext per User per * AuthenticationProvider can exist. * * @return * The UserContext that was created by the AuthenticationProvider * having the given identifier. * * @throws GuacamoleException * If an error occurs while retrieving the UserContext, or if the * UserContext does not exist. */ public UserContext retrieveUserContext(GuacamoleSession session, String authProviderIdentifier) throws GuacamoleException { // Get list of UserContexts List userContexts = session.getUserContexts(); // Locate and return the UserContext associated with the // AuthenticationProvider having the given identifier, if any for (UserContext userContext : userContexts) { // Get AuthenticationProvider associated with current UserContext AuthenticationProvider authProvider = userContext.getAuthenticationProvider(); // If AuthenticationProvider identifier matches, done if (authProvider.getIdentifier().equals(authProviderIdentifier)) return userContext; } throw new GuacamoleResourceNotFoundException("Session not associated with authentication provider \"" + authProviderIdentifier + "\"."); } /** * Retrieves a single user from the given user context. * * @param userContext * The user context to retrieve the user from. * * @param identifier * The identifier of the user to retrieve. * * @return * The user having the given identifier. * * @throws GuacamoleException * If an error occurs while retrieving the user, or if the * user does not exist. */ public User retrieveUser(UserContext userContext, String identifier) throws GuacamoleException { // Get user directory Directory directory = userContext.getUserDirectory(); // Pull specified user User user = directory.get(identifier); if (user == null) throw new GuacamoleResourceNotFoundException("No such user: \"" + identifier + "\""); return user; } /** * Retrieves a single user from the given GuacamoleSession. * * @param session * The GuacamoleSession to retrieve the user from. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider that created the * UserContext from which the user should be retrieved. Only one * UserContext per User per AuthenticationProvider can exist. * * @param identifier * The identifier of the user to retrieve. * * @return * The user having the given identifier. * * @throws GuacamoleException * If an error occurs while retrieving the user, or if the * user does not exist. */ public User retrieveUser(GuacamoleSession session, String authProviderIdentifier, String identifier) throws GuacamoleException { UserContext userContext = retrieveUserContext(session, authProviderIdentifier); return retrieveUser(userContext, identifier); } /** * Retrieves a single connection from the given user context. * * @param userContext * The user context to retrieve the connection from. * * @param identifier * The identifier of the connection to retrieve. * * @return * The connection having the given identifier. * * @throws GuacamoleException * If an error occurs while retrieving the connection, or if the * connection does not exist. */ public Connection retrieveConnection(UserContext userContext, String identifier) throws GuacamoleException { // Get connection directory Directory directory = userContext.getConnectionDirectory(); // Pull specified connection Connection connection = directory.get(identifier); if (connection == null) throw new GuacamoleResourceNotFoundException("No such connection: \"" + identifier + "\""); return connection; } /** * Retrieves a single connection from the given GuacamoleSession. * * @param session * The GuacamoleSession to retrieve the connection from. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider that created the * UserContext from which the connection should be retrieved. Only one * UserContext per User per AuthenticationProvider can exist. * * @param identifier * The identifier of the connection to retrieve. * * @return * The connection having the given identifier. * * @throws GuacamoleException * If an error occurs while retrieving the connection, or if the * connection does not exist. */ public Connection retrieveConnection(GuacamoleSession session, String authProviderIdentifier, String identifier) throws GuacamoleException { UserContext userContext = retrieveUserContext(session, authProviderIdentifier); return retrieveConnection(userContext, identifier); } /** * Retrieves a single connection group from the given user context. If * the given identifier the REST API root identifier, the root connection * group will be returned. The underlying authentication provider may * additionally use a different identifier for root. * * @param userContext * The user context to retrieve the connection group from. * * @param identifier * The identifier of the connection group to retrieve. * * @return * The connection group having the given identifier, or the root * connection group if the identifier the root identifier. * * @throws GuacamoleException * If an error occurs while retrieving the connection group, or if the * connection group does not exist. */ public ConnectionGroup retrieveConnectionGroup(UserContext userContext, String identifier) throws GuacamoleException { // Use root group if identifier is the standard root identifier if (identifier != null && identifier.equals(APIConnectionGroup.ROOT_IDENTIFIER)) return userContext.getRootConnectionGroup(); // Pull specified connection group otherwise Directory directory = userContext.getConnectionGroupDirectory(); ConnectionGroup connectionGroup = directory.get(identifier); if (connectionGroup == null) throw new GuacamoleResourceNotFoundException("No such connection group: \"" + identifier + "\""); return connectionGroup; } /** * Retrieves a single connection group from the given GuacamoleSession. If * the given identifier is the REST API root identifier, the root * connection group will be returned. The underlying authentication * provider may additionally use a different identifier for root. * * @param session * The GuacamoleSession to retrieve the connection group from. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider that created the * UserContext from which the connection group should be retrieved. * Only one UserContext per User per AuthenticationProvider can exist. * * @param identifier * The identifier of the connection group to retrieve. * * @return * The connection group having the given identifier, or the root * connection group if the identifier is the root identifier. * * @throws GuacamoleException * If an error occurs while retrieving the connection group, or if the * connection group does not exist. */ public ConnectionGroup retrieveConnectionGroup(GuacamoleSession session, String authProviderIdentifier, String identifier) throws GuacamoleException { UserContext userContext = retrieveUserContext(session, authProviderIdentifier); return retrieveConnectionGroup(userContext, identifier); } } ././@LongLink0100644 0000000 0000000 00000000155 12645510255 011641 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServiceModule.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServiceMod0100664 0001751 0001751 00000010473 12645510255 035202 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest; import com.google.inject.Scopes; import com.google.inject.matcher.Matchers; import com.google.inject.servlet.ServletModule; import com.sun.jersey.guice.spi.container.servlet.GuiceContainer; import org.aopalliance.intercept.MethodInterceptor; import org.codehaus.jackson.jaxrs.JacksonJsonProvider; import org.glyptodon.guacamole.net.basic.rest.auth.TokenRESTService; import org.glyptodon.guacamole.net.basic.rest.connection.ConnectionRESTService; import org.glyptodon.guacamole.net.basic.rest.connectiongroup.ConnectionGroupRESTService; import org.glyptodon.guacamole.net.basic.rest.activeconnection.ActiveConnectionRESTService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthTokenGenerator; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.glyptodon.guacamole.net.basic.rest.auth.SecureRandomAuthTokenGenerator; import org.glyptodon.guacamole.net.basic.rest.auth.TokenSessionMap; import org.glyptodon.guacamole.net.basic.rest.history.HistoryRESTService; import org.glyptodon.guacamole.net.basic.rest.language.LanguageRESTService; import org.glyptodon.guacamole.net.basic.rest.schema.SchemaRESTService; import org.glyptodon.guacamole.net.basic.rest.user.UserRESTService; /** * A Guice Module to set up the servlet mappings and authentication-specific * dependency injection for the Guacamole REST API. * * @author James Muehlner * @author Michael Jumper */ public class RESTServiceModule extends ServletModule { /** * Singleton instance of TokenSessionMap. */ private final TokenSessionMap tokenSessionMap; /** * Creates a module which handles binding of REST services and related * authentication objects, including the singleton TokenSessionMap. * * @param tokenSessionMap * An instance of TokenSessionMap to inject as a singleton wherever * needed. */ public RESTServiceModule(TokenSessionMap tokenSessionMap) { this.tokenSessionMap = tokenSessionMap; } @Override protected void configureServlets() { // Bind session map bind(TokenSessionMap.class).toInstance(tokenSessionMap); // Bind low-level services bind(AuthenticationService.class); bind(AuthTokenGenerator.class).to(SecureRandomAuthTokenGenerator.class); // Automatically translate GuacamoleExceptions for REST methods MethodInterceptor interceptor = new RESTExceptionWrapper(); requestInjection(interceptor); bindInterceptor(Matchers.any(), new RESTMethodMatcher(), interceptor); // Bind convenience services used by the REST API bind(ObjectRetrievalService.class); // Set up the API endpoints bind(ActiveConnectionRESTService.class); bind(ConnectionGroupRESTService.class); bind(ConnectionRESTService.class); bind(HistoryRESTService.class); bind(LanguageRESTService.class); bind(SchemaRESTService.class); bind(TokenRESTService.class); bind(UserRESTService.class); // Set up the servlet and JSON mappings bind(GuiceContainer.class); bind(JacksonJsonProvider.class).in(Scopes.SINGLETON); serve("/api/*").with(GuiceContainer.class); } } ././@LongLink0100644 0000000 0000000 00000000157 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUserWrapper.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUserWr0100664 0001751 0001751 00000007517 12612226251 035202 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.user; import java.util.Map; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleUnsupportedException; import org.glyptodon.guacamole.net.auth.User; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; /** * A wrapper to make an APIUser look like a User. Useful where an * org.glyptodon.guacamole.net.auth.User is required. As a simple wrapper for * APIUser, access to permissions is not provided. Any attempt to access or * manipulate permissions on an APIUserWrapper will result in an exception. * * @author James Muehlner */ public class APIUserWrapper implements User { /** * The wrapped APIUser. */ private final APIUser apiUser; /** * Wrap a given APIUser to expose as a User. * @param apiUser The APIUser to wrap. */ public APIUserWrapper(APIUser apiUser) { this.apiUser = apiUser; } @Override public String getIdentifier() { return apiUser.getUsername(); } @Override public void setIdentifier(String username) { apiUser.setUsername(username); } @Override public String getPassword() { return apiUser.getPassword(); } @Override public void setPassword(String password) { apiUser.setPassword(password); } @Override public Map getAttributes() { return apiUser.getAttributes(); } @Override public void setAttributes(Map attributes) { apiUser.setAttributes(attributes); } @Override public SystemPermissionSet getSystemPermissions() throws GuacamoleException { throw new GuacamoleUnsupportedException("APIUserWrapper does not provide permission access."); } @Override public ObjectPermissionSet getConnectionPermissions() throws GuacamoleException { throw new GuacamoleUnsupportedException("APIUserWrapper does not provide permission access."); } @Override public ObjectPermissionSet getConnectionGroupPermissions() throws GuacamoleException { throw new GuacamoleUnsupportedException("APIUserWrapper does not provide permission access."); } @Override public ObjectPermissionSet getUserPermissions() throws GuacamoleException { throw new GuacamoleUnsupportedException("APIUserWrapper does not provide permission access."); } @Override public ObjectPermissionSet getActiveConnectionPermissions() throws GuacamoleException { throw new GuacamoleUnsupportedException("APIUserWrapper does not provide permission access."); } } ././@LongLink0100644 0000000 0000000 00000000163 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/PermissionSetPatch.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/Permissio0100664 0001751 0001751 00000006527 12612226251 035373 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.user; import java.util.HashSet; import java.util.Set; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.permission.Permission; import org.glyptodon.guacamole.net.auth.permission.PermissionSet; /** * A set of changes to be applied to a PermissionSet, describing the set of * permissions being added and removed. * * @author Michael Jumper * @param * The type of permissions being added and removed. */ public class PermissionSetPatch { /** * The set of all permissions being added. */ private final Set addedPermissions = new HashSet(); /** * The set of all permissions being removed. */ private final Set removedPermissions = new HashSet(); /** * Queues the given permission to be added. The add operation will be * performed only when apply() is called. * * @param permission * The permission to add. */ public void addPermission(PermissionType permission) { addedPermissions.add(permission); } /** * Queues the given permission to be removed. The remove operation will be * performed only when apply() is called. * * @param permission * The permission to remove. */ public void removePermission(PermissionType permission) { removedPermissions.add(permission); } /** * Applies all queued changes to the given permission set. * * @param permissionSet * The permission set to add and remove permissions from. * * @throws GuacamoleException * If an error occurs while manipulating the permissions of the given * permission set. */ public void apply(PermissionSet permissionSet) throws GuacamoleException { // Add any added permissions if (!addedPermissions.isEmpty()) permissionSet.addPermissions(addedPermissions); // Remove any removed permissions if (!removedPermissions.isEmpty()) permissionSet.removePermissions(removedPermissions); } } ././@LongLink0100644 0000000 0000000 00000000155 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/package-info.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/package-i0100664 0001751 0001751 00000002353 12612226251 035233 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes related to the user manipulation aspect of the Guacamole REST API. */ package org.glyptodon.guacamole.net.basic.rest.user; ././@LongLink0100644 0000000 0000000 00000000160 12645510255 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTS0100664 0001751 0001751 00000063175 12645510255 035170 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.user; import com.google.inject.Inject; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.User; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.auth.credentials.GuacamoleCredentialsException; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.Permission; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.rest.APIPatch; import static org.glyptodon.guacamole.net.basic.rest.APIPatch.Operation.add; import static org.glyptodon.guacamole.net.basic.rest.APIPatch.Operation.remove; import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.PATCH; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.glyptodon.guacamole.net.basic.rest.permission.APIPermissionSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A REST Service for handling user CRUD operations. * * @author James Muehlner * @author Michael Jumper */ @Path("/data/{dataSource}/users") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class UserRESTService { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(UserRESTService.class); /** * The prefix of any path within an operation of a JSON patch which * modifies the permissions of a user regarding a specific connection. */ private static final String CONNECTION_PERMISSION_PATCH_PATH_PREFIX = "/connectionPermissions/"; /** * The prefix of any path within an operation of a JSON patch which * modifies the permissions of a user regarding a specific connection group. */ private static final String CONNECTION_GROUP_PERMISSION_PATCH_PATH_PREFIX = "/connectionGroupPermissions/"; /** * The prefix of any path within an operation of a JSON patch which * modifies the permissions of a user regarding a specific active connection. */ private static final String ACTIVE_CONNECTION_PERMISSION_PATCH_PATH_PREFIX = "/activeConnectionPermissions/"; /** * The prefix of any path within an operation of a JSON patch which * modifies the permissions of a user regarding another, specific user. */ private static final String USER_PERMISSION_PATCH_PATH_PREFIX = "/userPermissions/"; /** * The path of any operation within a JSON patch which modifies the * permissions of a user regarding the entire system. */ private static final String SYSTEM_PERMISSION_PATCH_PATH = "/systemPermissions"; /** * A service for authenticating users from auth tokens. */ @Inject private AuthenticationService authenticationService; /** * Service for convenient retrieval of objects. */ @Inject private ObjectRetrievalService retrievalService; /** * Gets a list of users in the given data source (UserContext), filtering * the returned list by the given permission, if specified. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext from which the users are to be retrieved. * * @param permissions * The set of permissions to filter with. A user must have one or more * of these permissions for a user to appear in the result. * If null, no filtering will be performed. * * @return * A list of all visible users. If a permission was specified, this * list will contain only those users for whom the current user has * that permission. * * @throws GuacamoleException * If an error is encountered while retrieving users. */ @GET public List getUsers(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @QueryParam("permission") List permissions) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // An admin user has access to any user User self = userContext.self(); SystemPermissionSet systemPermissions = self.getSystemPermissions(); boolean isAdmin = systemPermissions.hasPermission(SystemPermission.Type.ADMINISTER); // Get the directory Directory userDirectory = userContext.getUserDirectory(); // Filter users, if requested Collection userIdentifiers = userDirectory.getIdentifiers(); if (!isAdmin && permissions != null && !permissions.isEmpty()) { ObjectPermissionSet userPermissions = self.getUserPermissions(); userIdentifiers = userPermissions.getAccessibleObjects(permissions, userIdentifiers); } // Retrieve all users, converting to API users List apiUsers = new ArrayList(); for (User user : userDirectory.getAll(userIdentifiers)) apiUsers.add(new APIUser(user)); return apiUsers; } /** * Retrieves an individual user. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext from which the requested user is to be retrieved. * * @param username * The username of the user to retrieve. * * @return user * The user having the given username. * * @throws GuacamoleException * If an error occurs while retrieving the user. */ @GET @Path("/{username}") public APIUser getUser(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); // Retrieve the requested user User user = retrievalService.retrieveUser(session, authProviderIdentifier, username); return new APIUser(user); } /** * Creates a new user and returns the user that was created. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext in which the requested user is to be created. * * @param user * The new user to create. * * @throws GuacamoleException * If a problem is encountered while creating the user. * * @return * The newly created user. */ @POST public APIUser createUser(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, APIUser user) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Get the directory Directory userDirectory = userContext.getUserDirectory(); // Randomly set the password if it wasn't provided if (user.getPassword() == null) user.setPassword(UUID.randomUUID().toString()); // Create the user userDirectory.add(new APIUserWrapper(user)); return user; } /** * Updates an individual existing user. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext in which the requested user is to be updated. * * @param username * The username of the user to update. * * @param user * The data to update the user with. * * @throws GuacamoleException * If an error occurs while updating the user. */ @PUT @Path("/{username}") public void updateUser(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username, APIUser user) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Get the directory Directory userDirectory = userContext.getUserDirectory(); // Validate data and path are sane if (!user.getUsername().equals(username)) throw new GuacamoleClientException("Username in path does not match username provided JSON data."); // A user may not use this endpoint to modify himself if (userContext.self().getIdentifier().equals(user.getUsername())) throw new GuacamoleSecurityException("Permission denied."); // Get the user User existingUser = retrievalService.retrieveUser(userContext, username); // Do not update the user password if no password was provided if (user.getPassword() != null) existingUser.setPassword(user.getPassword()); // Update user attributes existingUser.setAttributes(user.getAttributes()); // Update the user userDirectory.update(existingUser); } /** * Updates the password for an individual existing user. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext in which the requested user is to be updated. * * @param username * The username of the user to update. * * @param userPasswordUpdate * The object containing the old password for the user, as well as the * new password to set for that user. * * @param request * The HttpServletRequest associated with the password update attempt. * * @throws GuacamoleException * If an error occurs while updating the user's password. */ @PUT @Path("/{username}/password") public void updatePassword(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username, APIUserPasswordUpdate userPasswordUpdate, @Context HttpServletRequest request) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Build credentials Credentials credentials = new Credentials(); credentials.setUsername(username); credentials.setPassword(userPasswordUpdate.getOldPassword()); credentials.setRequest(request); credentials.setSession(request.getSession(true)); // Verify that the old password was correct try { AuthenticationProvider authProvider = userContext.getAuthenticationProvider(); if (authProvider.authenticateUser(credentials) == null) throw new GuacamoleSecurityException("Permission denied."); } // Pass through any credentials exceptions as simple permission denied catch (GuacamoleCredentialsException e) { throw new GuacamoleSecurityException("Permission denied."); } // Get the user directory Directory userDirectory = userContext.getUserDirectory(); // Get the user that we want to updates User user = retrievalService.retrieveUser(userContext, username); // Set password to the newly provided one user.setPassword(userPasswordUpdate.getNewPassword()); // Update the user userDirectory.update(user); } /** * Deletes an individual existing user. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext from which the requested user is to be deleted. * * @param username * The username of the user to delete. * * @throws GuacamoleException * If an error occurs while deleting the user. */ @DELETE @Path("/{username}") public void deleteUser(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Get the directory Directory userDirectory = userContext.getUserDirectory(); // Get the user User existingUser = userDirectory.get(username); if (existingUser == null) throw new GuacamoleResourceNotFoundException("No such user: \"" + username + "\""); // Delete the user userDirectory.remove(username); } /** * Gets a list of permissions for the user with the given username. * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext in which the requested user is to be found. * * @param username * The username of the user to retrieve permissions for. * * @return * A list of all permissions granted to the specified user. * * @throws GuacamoleException * If an error occurs while retrieving permissions. */ @GET @Path("/{username}/permissions") public APIPermissionSet getPermissions(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); User user; // If username is own username, just use self - might not have query permissions if (userContext.self().getIdentifier().equals(username)) user = userContext.self(); // If not self, query corresponding user from directory else { user = userContext.getUserDirectory().get(username); if (user == null) throw new GuacamoleResourceNotFoundException("No such user: \"" + username + "\""); } return new APIPermissionSet(user); } /** * Updates the given permission set patch by queuing an add or remove * operation for the given permission based on the given patch operation. * * @param * The type of permission stored within the permission set. * * @param operation * The patch operation to perform. * * @param permissionSetPatch * The permission set patch being modified. * * @param permission * The permission being added or removed from the set. * * @throws GuacamoleException * If the requested patch operation is not supported. */ private void updatePermissionSet( APIPatch.Operation operation, PermissionSetPatch permissionSetPatch, PermissionType permission) throws GuacamoleException { // Add or remove permission based on operation switch (operation) { // Add permission case add: permissionSetPatch.addPermission(permission); break; // Remove permission case remove: permissionSetPatch.removePermission(permission); break; // Unsupported patch operation default: throw new GuacamoleClientException("Unsupported patch operation: \"" + operation + "\""); } } /** * Applies a given list of permission patches. Each patch specifies either * an "add" or a "remove" operation for a permission type, represented by * a string. Valid permission types depend on the path of each patch * operation, as the path dictates the permission being modified, such as * "/connectionPermissions/42" or "/systemPermissions". * * @param authToken * The authentication token that is used to authenticate the user * performing the operation. * * @param authProviderIdentifier * The unique identifier of the AuthenticationProvider associated with * the UserContext in which the requested user is to be found. * * @param username * The username of the user to modify the permissions of. * * @param patches * The permission patches to apply for this request. * * @throws GuacamoleException * If a problem is encountered while modifying permissions. */ @PATCH @Path("/{username}/permissions") public void patchPermissions(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username, List> patches) throws GuacamoleException { GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); // Get the user User user = userContext.getUserDirectory().get(username); if (user == null) throw new GuacamoleResourceNotFoundException("No such user: \"" + username + "\""); // Permission patches for all types of permissions PermissionSetPatch connectionPermissionPatch = new PermissionSetPatch(); PermissionSetPatch connectionGroupPermissionPatch = new PermissionSetPatch(); PermissionSetPatch activeConnectionPermissionPatch = new PermissionSetPatch(); PermissionSetPatch userPermissionPatch = new PermissionSetPatch(); PermissionSetPatch systemPermissionPatch = new PermissionSetPatch(); // Apply all patch operations individually for (APIPatch patch : patches) { String path = patch.getPath(); // Create connection permission if path has connection prefix if (path.startsWith(CONNECTION_PERMISSION_PATCH_PATH_PREFIX)) { // Get identifier and type from patch operation String identifier = path.substring(CONNECTION_PERMISSION_PATCH_PATH_PREFIX.length()); ObjectPermission.Type type = ObjectPermission.Type.valueOf(patch.getValue()); // Create and update corresponding permission ObjectPermission permission = new ObjectPermission(type, identifier); updatePermissionSet(patch.getOp(), connectionPermissionPatch, permission); } // Create connection group permission if path has connection group prefix else if (path.startsWith(CONNECTION_GROUP_PERMISSION_PATCH_PATH_PREFIX)) { // Get identifier and type from patch operation String identifier = path.substring(CONNECTION_GROUP_PERMISSION_PATCH_PATH_PREFIX.length()); ObjectPermission.Type type = ObjectPermission.Type.valueOf(patch.getValue()); // Create and update corresponding permission ObjectPermission permission = new ObjectPermission(type, identifier); updatePermissionSet(patch.getOp(), connectionGroupPermissionPatch, permission); } // Create active connection permission if path has active connection prefix else if (path.startsWith(ACTIVE_CONNECTION_PERMISSION_PATCH_PATH_PREFIX)) { // Get identifier and type from patch operation String identifier = path.substring(ACTIVE_CONNECTION_PERMISSION_PATCH_PATH_PREFIX.length()); ObjectPermission.Type type = ObjectPermission.Type.valueOf(patch.getValue()); // Create and update corresponding permission ObjectPermission permission = new ObjectPermission(type, identifier); updatePermissionSet(patch.getOp(), activeConnectionPermissionPatch, permission); } // Create user permission if path has user prefix else if (path.startsWith(USER_PERMISSION_PATCH_PATH_PREFIX)) { // Get identifier and type from patch operation String identifier = path.substring(USER_PERMISSION_PATCH_PATH_PREFIX.length()); ObjectPermission.Type type = ObjectPermission.Type.valueOf(patch.getValue()); // Create and update corresponding permission ObjectPermission permission = new ObjectPermission(type, identifier); updatePermissionSet(patch.getOp(), userPermissionPatch, permission); } // Create system permission if path is system path else if (path.equals(SYSTEM_PERMISSION_PATCH_PATH)) { // Get identifier and type from patch operation SystemPermission.Type type = SystemPermission.Type.valueOf(patch.getValue()); // Create and update corresponding permission SystemPermission permission = new SystemPermission(type); updatePermissionSet(patch.getOp(), systemPermissionPatch, permission); } // Otherwise, the path is not supported else throw new GuacamoleClientException("Unsupported patch path: \"" + path + "\""); } // end for each patch operation // Save the permission changes connectionPermissionPatch.apply(user.getConnectionPermissions()); connectionGroupPermissionPatch.apply(user.getConnectionGroupPermissions()); activeConnectionPermissionPatch.apply(user.getActiveConnectionPermissions()); userPermissionPatch.apply(user.getUserPermissions()); systemPermissionPatch.apply(user.getSystemPermissions()); } } ././@LongLink0100644 0000000 0000000 00000000150 12612226251 011626 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUser.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUser.j0100664 0001751 0001751 00000007272 12612226251 035117 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.user; import java.util.Map; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.map.annotate.JsonSerialize; import org.glyptodon.guacamole.net.auth.User; /** * A simple User to expose through the REST endpoints. * * @author James Muehlner */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) public class APIUser { /** * The username of this user. */ private String username; /** * The password of this user. */ private String password; /** * Map of all associated attributes by attribute identifier. */ private Map attributes; /** * Construct a new empty APIUser. */ public APIUser() {} /** * Construct a new APIUser from the provided User. * @param user The User to construct the APIUser from. */ public APIUser(User user) { // Set user information this.username = user.getIdentifier(); this.password = user.getPassword(); // Associate any attributes this.attributes = user.getAttributes(); } /** * Returns the username for this user. * @return The username for this user. */ public String getUsername() { return username; } /** * Set the username for this user. * @param username The username for this user. */ public void setUsername(String username) { this.username = username; } /** * Returns the password for this user. * @return The password for this user. */ public String getPassword() { return password; } /** * Set the password for this user. * @param password The password for this user. */ public void setPassword(String password) { this.password = password; } /** * Returns a map of all attributes associated with this user. Each entry * key is the attribute identifier, while each value is the attribute * value itself. * * @return * The attribute map for this user. */ public Map getAttributes() { return attributes; } /** * Sets the map of all attributes associated with this user. Each entry key * is the attribute identifier, while each value is the attribute value * itself. * * @param attributes * The attribute map for this user. */ public void setAttributes(Map attributes) { this.attributes = attributes; } } ././@LongLink0100644 0000000 0000000 00000000166 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUserPasswordUpdate.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUserPa0100664 0001751 0001751 00000005123 12612226251 035141 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest.user; /** * All the information necessary for the password update operation on a user. * * @author James Muehlner */ public class APIUserPasswordUpdate { /** * The old (current) password of this user. */ private String oldPassword; /** * The new password of this user. */ private String newPassword; /** * Returns the old password for this user. This password must match the * user's current password for the password update operation to succeed. * * @return * The old password for this user. */ public String getOldPassword() { return oldPassword; } /** * Set the old password for this user. This password must match the * user's current password for the password update operation to succeed. * * @param oldPassword * The old password for this user. */ public void setOldPassword(String oldPassword) { this.oldPassword = oldPassword; } /** * Returns the new password that will be assigned to this user. * * @return * The new password for this user. */ public String getNewPassword() { return newPassword; } /** * Set the new password that will be assigned to this user. * * @param newPassword * The new password for this user. */ public void setNewPassword(String newPassword) { this.newPassword = newPassword; } } ././@LongLink0100644 0000000 0000000 00000000146 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIRequest.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIRequest.jav0100664 0001751 0001751 00000006726 12612226251 035205 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.rest; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.ws.rs.core.MultivaluedMap; /** * Wrapper for HttpServletRequest which uses a given MultivaluedMap to provide * the values of all request parameters. * * @author Michael Jumper */ public class APIRequest extends HttpServletRequestWrapper { /** * Map of all request parameter names to their corresponding values. */ private final Map parameters; /** * Wraps the given HttpServletRequest, using the given MultivaluedMap to * provide all request parameters. All HttpServletRequest functions which * do not deal with parameter names and values are delegated to the wrapped * request. * * @param request * The HttpServletRequest to wrap. * * @param parameters * All request parameters. */ public APIRequest(HttpServletRequest request, MultivaluedMap parameters) { super(request); // Copy parameters from given MultivaluedMap this.parameters = new HashMap(parameters.size()); for (Map.Entry> entry : parameters.entrySet()) { // Get parameter name and all corresponding values String name = entry.getKey(); List values = entry.getValue(); // Add parameters to map this.parameters.put(name, values.toArray(new String[values.size()])); } } @Override public String[] getParameterValues(String name) { return parameters.get(name); } @Override public Enumeration getParameterNames() { return Collections.enumeration(parameters.keySet()); } @Override public Map getParameterMap() { return Collections.unmodifiableMap(parameters); } @Override public String getParameter(String name) { // If no such parameter exists, just return null String[] values = getParameterValues(name); if (values == null) return null; // Otherwise, return first value return values[0]; } } ././@LongLink0100644 0000000 0000000 00000000160 12645510255 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/Extension0100664 0001751 0001751 00000042301 12645510255 035452 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.extension; import com.google.inject.Provides; import com.google.inject.servlet.ServletModule; import java.io.File; import java.io.FileFilter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import net.sourceforge.guacamole.net.basic.BasicFileAuthenticationProvider; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleServerException; import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties; import org.glyptodon.guacamole.net.basic.resource.Resource; import org.glyptodon.guacamole.net.basic.resource.ResourceServlet; import org.glyptodon.guacamole.net.basic.resource.SequenceResource; import org.glyptodon.guacamole.net.basic.resource.WebApplicationResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A Guice Module which loads all extensions within the * GUACAMOLE_HOME/extensions directory, if any. * * @author Michael Jumper */ public class ExtensionModule extends ServletModule { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(ExtensionModule.class); /** * The version strings of all Guacamole versions whose extensions are * compatible with this release. */ private static final List ALLOWED_GUACAMOLE_VERSIONS = Collections.unmodifiableList(Arrays.asList( "*", "0.9.9" )); /** * The name of the directory within GUACAMOLE_HOME containing any .jars * which should be included in the classpath of all extensions. */ private static final String LIB_DIRECTORY = "lib"; /** * The name of the directory within GUACAMOLE_HOME containing all * extensions. */ private static final String EXTENSIONS_DIRECTORY = "extensions"; /** * The string that the filenames of all extensions must end with to be * recognized as extensions. */ private static final String EXTENSION_SUFFIX = ".jar"; /** * The Guacamole server environment. */ private final Environment environment; /** * All currently-bound authentication providers, if any. */ private final List boundAuthenticationProviders = new ArrayList(); /** * Service for adding and retrieving language resources. */ private final LanguageResourceService languageResourceService; /** * Returns the classloader that should be used as the parent classloader * for all extensions. If the GUACAMOLE_HOME/lib directory exists, this * will be a classloader that loads classes from within the .jar files in * that directory. Lacking the GUACAMOLE_HOME/lib directory, this will * simply be the classloader associated with the ExtensionModule class. * * @return * The classloader that should be used as the parent classloader for * all extensions. * * @throws GuacamoleException * If an error occurs while retrieving the classloader. */ private ClassLoader getParentClassLoader() throws GuacamoleException { // Retrieve lib directory File libDir = new File(environment.getGuacamoleHome(), LIB_DIRECTORY); // If lib directory does not exist, use default class loader if (!libDir.isDirectory()) return ExtensionModule.class.getClassLoader(); // Return classloader which loads classes from all .jars within the lib directory return DirectoryClassLoader.getInstance(libDir); } /** * Creates a module which loads all extensions within the * GUACAMOLE_HOME/extensions directory. * * @param environment * The environment to use when configuring authentication. */ public ExtensionModule(Environment environment) { this.environment = environment; this.languageResourceService = new LanguageResourceService(environment); } /** * Reads the value of the now-deprecated "auth-provider" property from * guacamole.properties, returning the corresponding AuthenticationProvider * class. If no authentication provider could be read, or the property is * not present, null is returned. * * As this property is deprecated, this function will also log warning * messages if the property is actually specified. * * @return * The value of the deprecated "auth-provider" property, or null if the * property is not present. */ @SuppressWarnings("deprecation") // We must continue to use this property until it is truly no longer supported private Class getAuthProviderProperty() { // Get and bind auth provider instance, if defined via property try { // Use "auth-provider" property if present, but warn about deprecation Class authenticationProvider = environment.getProperty(BasicGuacamoleProperties.AUTH_PROVIDER); if (authenticationProvider != null) logger.warn("The \"auth-provider\" and \"lib-directory\" properties are now deprecated. Please use the \"extensions\" and \"lib\" directories within GUACAMOLE_HOME instead."); return authenticationProvider; } catch (GuacamoleException e) { logger.warn("Value of deprecated \"auth-provider\" property within guacamole.properties is not valid: {}", e.getMessage()); logger.debug("Error reading authentication provider from guacamole.properties.", e); } return null; } /** * Binds the given AuthenticationProvider class such that any service * requiring access to the AuthenticationProvider can obtain it via * injection, along with any other bound AuthenticationProviders. * * @param authenticationProvider * The AuthenticationProvider class to bind. */ private void bindAuthenticationProvider(Class authenticationProvider) { // Bind authentication provider logger.debug("[{}] Binding AuthenticationProvider \"{}\".", boundAuthenticationProviders.size(), authenticationProvider.getName()); boundAuthenticationProviders.add(new AuthenticationProviderFacade(authenticationProvider)); } /** * Binds each of the the given AuthenticationProvider classes such that any * service requiring access to the AuthenticationProvider can obtain it via * injection. * * @param authProviders * The AuthenticationProvider classes to bind. */ private void bindAuthenticationProviders(Collection> authProviders) { // Bind each authentication provider within extension for (Class authenticationProvider : authProviders) bindAuthenticationProvider(authenticationProvider); } /** * Returns a list of all currently-bound AuthenticationProvider instances. * * @return * A List of all currently-bound AuthenticationProvider. The List is * not modifiable. */ @Provides public List getAuthenticationProviders() { return Collections.unmodifiableList(boundAuthenticationProviders); } /** * Serves each of the given resources as a language resource. Language * resources are served from within the "/translations" directory as JSON * files, where the name of each JSON file is the language key. * * @param resources * A map of all language resources to serve, where the key of each * entry in the language key from which the name of the JSON file will * be derived. */ private void serveLanguageResources(Map resources) { // Add all resources to language resource service for (Map.Entry translationResource : resources.entrySet()) { // Get path and resource from path/resource pair String path = translationResource.getKey(); Resource resource = translationResource.getValue(); // Derive key from path String languageKey = languageResourceService.getLanguageKey(path); if (languageKey == null) { logger.warn("Invalid language file name: \"{}\"", path); continue; } // Add language resource languageResourceService.addLanguageResource(languageKey, resource); } } /** * Serves each of the given resources under the given prefix. The path of * each resource relative to the prefix is the key of its entry within the * map. * * @param prefix * The prefix under which each resource should be served. * * @param resources * A map of all resources to serve, where the key of each entry in the * map is the desired path of that resource relative to the prefix. */ private void serveStaticResources(String prefix, Map resources) { // Add all resources under given prefix for (Map.Entry staticResource : resources.entrySet()) { // Get path and resource from path/resource pair String path = staticResource.getKey(); Resource resource = staticResource.getValue(); // Serve within namespace-derived path serve(prefix + path).with(new ResourceServlet(resource)); } } /** * Returns whether the given version of Guacamole is compatible with this * version of Guacamole as far as extensions are concerned. * * @param guacamoleVersion * The version of Guacamole the extension was built for. * * @return * true if the given version of Guacamole is compatible with this * version of Guacamole, false otherwise. */ private boolean isCompatible(String guacamoleVersion) { return ALLOWED_GUACAMOLE_VERSIONS.contains(guacamoleVersion); } /** * Loads all extensions within the GUACAMOLE_HOME/extensions directory, if * any, adding their static resource to the given resoure collections. * * @param javaScriptResources * A modifiable collection of static JavaScript resources which may * receive new JavaScript resources from extensions. * * @param cssResources * A modifiable collection of static CSS resources which may receive * new CSS resources from extensions. */ private void loadExtensions(Collection javaScriptResources, Collection cssResources) { // Retrieve and validate extensions directory File extensionsDir = new File(environment.getGuacamoleHome(), EXTENSIONS_DIRECTORY); if (!extensionsDir.isDirectory()) return; // Retrieve list of all extension files within extensions directory File[] extensionFiles = extensionsDir.listFiles(new FileFilter() { @Override public boolean accept(File file) { return file.isFile() && file.getName().endsWith(EXTENSION_SUFFIX); } }); // Verify contents are accessible if (extensionFiles == null) { logger.warn("Although GUACAMOLE_HOME/" + EXTENSIONS_DIRECTORY + " exists, its contents cannot be read."); return; } // Sort files lexicographically Arrays.sort(extensionFiles); // Load each extension within the extension directory for (File extensionFile : extensionFiles) { logger.debug("Loading extension: \"{}\"", extensionFile.getName()); try { // Load extension from file Extension extension = new Extension(getParentClassLoader(), extensionFile); // Validate Guacamole version of extension if (!isCompatible(extension.getGuacamoleVersion())) { logger.debug("Declared Guacamole version \"{}\" of extension \"{}\" is not compatible with this version of Guacamole.", extension.getGuacamoleVersion(), extensionFile.getName()); throw new GuacamoleServerException("Extension \"" + extension.getName() + "\" is not " + "compatible with this version of Guacamole."); } // Add any JavaScript / CSS resources javaScriptResources.addAll(extension.getJavaScriptResources().values()); cssResources.addAll(extension.getCSSResources().values()); // Attempt to load all authentication providers bindAuthenticationProviders(extension.getAuthenticationProviderClasses()); // Add any translation resources serveLanguageResources(extension.getTranslationResources()); // Add all static resources under namespace-derived prefix String staticResourcePrefix = "/app/ext/" + extension.getNamespace() + "/"; serveStaticResources(staticResourcePrefix, extension.getStaticResources()); // Serve up the small favicon if provided if(extension.getSmallIcon() != null) serve("/images/logo-64.png").with(new ResourceServlet(extension.getSmallIcon())); // Serve up the large favicon if provided if(extension.getLargeIcon()!= null) serve("/images/logo-144.png").with(new ResourceServlet(extension.getLargeIcon())); // Log successful loading of extension by name logger.info("Extension \"{}\" loaded.", extension.getName()); } catch (GuacamoleException e) { logger.error("Extension \"{}\" could not be loaded: {}", extensionFile.getName(), e.getMessage()); logger.debug("Unable to load extension.", e); } } } @Override protected void configureServlets() { // Bind language resource service bind(LanguageResourceService.class).toInstance(languageResourceService); // Load initial language resources from servlet context languageResourceService.addLanguageResources(getServletContext()); // Load authentication provider from guacamole.properties for sake of backwards compatibility Class authProviderProperty = getAuthProviderProperty(); if (authProviderProperty != null) bindAuthenticationProvider(authProviderProperty); // Init JavaScript resources with base guacamole.min.js Collection javaScriptResources = new ArrayList(); javaScriptResources.add(new WebApplicationResource(getServletContext(), "/guacamole.min.js")); // Init CSS resources with base guacamole.min.css Collection cssResources = new ArrayList(); cssResources.add(new WebApplicationResource(getServletContext(), "/guacamole.min.css")); // Load all extensions loadExtensions(javaScriptResources, cssResources); // Always bind basic auth last bindAuthenticationProvider(BasicFileAuthenticationProvider.class); // Dynamically generate app.js and app.css from extensions serve("/app.js").with(new ResourceServlet(new SequenceResource(javaScriptResources))); serve("/app.css").with(new ResourceServlet(new SequenceResource(cssResources))); // Dynamically serve all language resources for (Map.Entry entry : languageResourceService.getLanguageResources().entrySet()) { // Get language key/resource pair String languageKey = entry.getKey(); Resource resource = entry.getValue(); // Serve resource within /translations serve("/translations/" + languageKey + ".json").with(new ResourceServlet(resource)); } } } ././@LongLink0100644 0000000 0000000 00000000162 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionManifest.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/Extension0100664 0001751 0001751 00000030500 12612226251 035442 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.extension; import java.util.Collection; import java.util.Map; import org.codehaus.jackson.annotate.JsonProperty; /** * Java representation of the JSON manifest contained within every Guacamole * extension, identifying an extension and describing its contents. * * @author Michael Jumper */ public class ExtensionManifest { /** * The version of Guacamole for which this extension was built. * Compatibility rules built into the web application will guard against * incompatible extensions being loaded. */ private String guacamoleVersion; /** * The name of the extension associated with this manifest. The extension * name is human-readable, and used for display purposes only. */ private String name; /** * The namespace of the extension associated with this manifest. The * extension namespace is required for internal use, and is used wherever * extension-specific files or resources need to be isolated from those of * other extensions. */ private String namespace; /** * The paths of all JavaScript resources within the .jar of the extension * associated with this manifest. */ private Collection javaScriptPaths; /** * The paths of all CSS resources within the .jar of the extension * associated with this manifest. */ private Collection cssPaths; /** * The paths of all translation JSON files within this extension, if any. */ private Collection translationPaths; /** * The mimetypes of all resources within this extension which are not * already declared as JavaScript, CSS, or translation resources, if any. * The key of each entry is the resource path, while the value is the * corresponding mimetype. */ private Map resourceTypes; /** * The names of all authentication provider classes within this extension, * if any. */ private Collection authProviders; /** * The path to the small favicon. If provided, this will replace the default * Guacamole icon. */ private String smallIcon; /** * The path to the large favicon. If provided, this will replace the default * Guacamole icon. */ private String largeIcon; /** * Returns the version of the Guacamole web application for which the * extension was built, such as "0.9.7". * * @return * The version of the Guacamole web application for which the extension * was built. */ public String getGuacamoleVersion() { return guacamoleVersion; } /** * Sets the version of the Guacamole web application for which the * extension was built, such as "0.9.7". * * @param guacamoleVersion * The version of the Guacamole web application for which the extension * was built. */ public void setGuacamoleVersion(String guacamoleVersion) { this.guacamoleVersion = guacamoleVersion; } /** * Returns the name of the extension associated with this manifest. The * name is human-readable, for display purposes only, and is defined within * the manifest by the "name" property. * * @return * The name of the extension associated with this manifest. */ public String getName() { return name; } /** * Sets the name of the extension associated with this manifest. The name * is human-readable, for display purposes only, and is defined within the * manifest by the "name" property. * * @param name * The name of the extension associated with this manifest. */ public void setName(String name) { this.name = name; } /** * Returns the namespace of the extension associated with this manifest. * The namespace is required for internal use, and is used wherever * extension-specific files or resources need to be isolated from those of * other extensions. It is defined within the manifest by the "namespace" * property. * * @return * The namespace of the extension associated with this manifest. */ public String getNamespace() { return namespace; } /** * Sets the namespace of the extension associated with this manifest. The * namespace is required for internal use, and is used wherever extension- * specific files or resources need to be isolated from those of other * extensions. It is defined within the manifest by the "namespace" * property. * * @param namespace * The namespace of the extension associated with this manifest. */ public void setNamespace(String namespace) { this.namespace = namespace; } /** * Returns the paths to all JavaScript resources within the extension. * These paths are defined within the manifest by the "js" property as an * array of strings, where each string is a path relative to the root of * the extension .jar. * * @return * A collection of paths to all JavaScript resources within the * extension. */ @JsonProperty("js") public Collection getJavaScriptPaths() { return javaScriptPaths; } /** * Sets the paths to all JavaScript resources within the extension. These * paths are defined within the manifest by the "js" property as an array * of strings, where each string is a path relative to the root of the * extension .jar. * * @param javaScriptPaths * A collection of paths to all JavaScript resources within the * extension. */ @JsonProperty("js") public void setJavaScriptPaths(Collection javaScriptPaths) { this.javaScriptPaths = javaScriptPaths; } /** * Returns the paths to all CSS resources within the extension. These paths * are defined within the manifest by the "js" property as an array of * strings, where each string is a path relative to the root of the * extension .jar. * * @return * A collection of paths to all CSS resources within the extension. */ @JsonProperty("css") public Collection getCSSPaths() { return cssPaths; } /** * Sets the paths to all CSS resources within the extension. These paths * are defined within the manifest by the "js" property as an array of * strings, where each string is a path relative to the root of the * extension .jar. * * @param cssPaths * A collection of paths to all CSS resources within the extension. */ @JsonProperty("css") public void setCSSPaths(Collection cssPaths) { this.cssPaths = cssPaths; } /** * Returns the paths to all translation resources within the extension. * These paths are defined within the manifest by the "translations" * property as an array of strings, where each string is a path relative to * the root of the extension .jar. * * @return * A collection of paths to all translation resources within the * extension. */ @JsonProperty("translations") public Collection getTranslationPaths() { return translationPaths; } /** * Sets the paths to all translation resources within the extension. These * paths are defined within the manifest by the "translations" property as * an array of strings, where each string is a path relative to the root of * the extension .jar. * * @param translationPaths * A collection of paths to all translation resources within the * extension. */ @JsonProperty("translations") public void setTranslationPaths(Collection translationPaths) { this.translationPaths = translationPaths; } /** * Returns a map of all resources to their corresponding mimetypes, for all * resources not already declared as JavaScript, CSS, or translation * resources. These paths and corresponding types are defined within the * manifest by the "resources" property as an object, where each property * name is a path relative to the root of the extension .jar, and each * value is a mimetype. * * @return * A map of all resources within the extension to their corresponding * mimetypes. */ @JsonProperty("resources") public Map getResourceTypes() { return resourceTypes; } /** * Sets the map of all resources to their corresponding mimetypes, for all * resources not already declared as JavaScript, CSS, or translation * resources. These paths and corresponding types are defined within the * manifest by the "resources" property as an object, where each property * name is a path relative to the root of the extension .jar, and each * value is a mimetype. * * @param resourceTypes * A map of all resources within the extension to their corresponding * mimetypes. */ @JsonProperty("resources") public void setResourceTypes(Map resourceTypes) { this.resourceTypes = resourceTypes; } /** * Returns the classnames of all authentication provider classes within the * extension. These classnames are defined within the manifest by the * "authProviders" property as an array of strings, where each string is an * authentication provider classname. * * @return * A collection of classnames of all authentication providers within * the extension. */ public Collection getAuthProviders() { return authProviders; } /** * Sets the classnames of all authentication provider classes within the * extension. These classnames are defined within the manifest by the * "authProviders" property as an array of strings, where each string is an * authentication provider classname. * * @param authProviders * A collection of classnames of all authentication providers within * the extension. */ public void setAuthProviders(Collection authProviders) { this.authProviders = authProviders; } /** * Returns the path to the small favicon, relative to the root of the * extension. * * @return * The path to the small favicon. */ public String getSmallIcon() { return smallIcon; } /** * Sets the path to the small favicon. This will replace the default * Guacamole icon. * * @param smallIcon * The path to the small favicon. */ public void setSmallIcon(String smallIcon) { this.smallIcon = smallIcon; } /** * Returns the path to the large favicon, relative to the root of the * extension. * * @return * The path to the large favicon. */ public String getLargeIcon() { return largeIcon; } /** * Sets the path to the large favicon. This will replace the default * Guacamole icon. * * @param largeIcon * The path to the large favicon. */ public void setLargeIcon(String largeIcon) { this.largeIcon = largeIcon; } } ././@LongLink0100644 0000000 0000000 00000000155 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/package-info.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/package-i0100664 0001751 0001751 00000002405 12612226251 035312 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes which represent and facilitate the loading of extensions to the * Guacamole web application. */ package org.glyptodon.guacamole.net.basic.extension; ././@LongLink0100644 0000000 0000000 00000000165 12612226251 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/DirectoryClassLoader.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/Directory0100664 0001751 0001751 00000012332 12612226251 035435 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.extension; import java.io.File; import java.io.FilenameFilter; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.Collection; import org.glyptodon.guacamole.GuacamoleException; /** * A ClassLoader implementation which finds classes within .jar files within a * given directory. * * @author Michael Jumper */ public class DirectoryClassLoader extends URLClassLoader { /** * Returns an instance of DirectoryClassLoader configured to load .jar * files from the given directory. Calling this function multiple times * will not affect previously-returned instances of DirectoryClassLoader. * * @param dir * The directory from which .jar files should be read. * * @return * A DirectoryClassLoader instance which loads classes from the .jar * files in the given directory. * * @throws GuacamoleException * If the given file is not a directory, or the contents of the given * directory cannot be read. */ public static DirectoryClassLoader getInstance(final File dir) throws GuacamoleException { try { // Attempt to create singleton classloader which loads classes from // all .jar's in the lib directory defined in guacamole.properties return AccessController.doPrivileged(new PrivilegedExceptionAction() { @Override public DirectoryClassLoader run() throws GuacamoleException { return new DirectoryClassLoader(dir); } }); } catch (PrivilegedActionException e) { throw (GuacamoleException) e.getException(); } } /** * Returns all .jar files within the given directory as an array of URLs. * * @param dir * The directory to retrieve all .jar files from. * * @return * An array of the URLs of all .jar files within the given directory. * * @throws GuacamoleException * If the given file is not a directory, or the contents of the given * directory cannot be read. */ private static URL[] getJarURLs(File dir) throws GuacamoleException { // Validate directory is indeed a directory if (!dir.isDirectory()) throw new GuacamoleException(dir + " is not a directory."); // Get list of URLs for all .jar's in the lib directory Collection jarURLs = new ArrayList(); File[] files = dir.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { // If it ends with .jar, accept the file return name.endsWith(".jar"); } }); // Verify directory was successfully read if (files == null) throw new GuacamoleException("Unable to read contents of directory " + dir); // Add the URL for each .jar to the jar URL list for (File file : files) { try { jarURLs.add(file.toURI().toURL()); } catch (MalformedURLException e) { throw new GuacamoleException(e); } } // Set delegate classloader to new URLClassLoader which loads from the .jars found above. URL[] urls = new URL[jarURLs.size()]; return jarURLs.toArray(urls); } /** * Creates a new DirectoryClassLoader configured to load .jar files from * the given directory. * * @param dir * The directory from which .jar files should be read. * * @throws GuacamoleException * If the given file is not a directory, or the contents of the given * directory cannot be read. */ private DirectoryClassLoader(File dir) throws GuacamoleException { super(getJarURLs(dir), DirectoryClassLoader.class.getClassLoader()); } } ././@LongLink0100644 0000000 0000000 00000000170 12645510255 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/LanguageResourceService.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/LanguageR0100664 0001751 0001751 00000037265 12645510255 035360 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.extension; import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.ServletContext; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.node.JsonNodeFactory; import org.codehaus.jackson.node.ObjectNode; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties; import org.glyptodon.guacamole.net.basic.resource.ByteArrayResource; import org.glyptodon.guacamole.net.basic.resource.Resource; import org.glyptodon.guacamole.net.basic.resource.WebApplicationResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Service which provides access to all built-in languages as resources, and * allows other resources to be added or overlaid against existing resources. * * @author Michael Jumper */ public class LanguageResourceService { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(LanguageResourceService.class); /** * The path to the translation folder within the webapp. */ private static final String TRANSLATION_PATH = "/translations"; /** * The JSON property for the human readable display name. */ private static final String LANGUAGE_DISPLAY_NAME_KEY = "NAME"; /** * The Jackson parser for parsing the language JSON files. */ private static final ObjectMapper mapper = new ObjectMapper(); /** * The regular expression to use for parsing the language key from the * filename. */ private static final Pattern LANGUAGE_KEY_PATTERN = Pattern.compile(".*/([a-z]+(_[A-Z]+)?)\\.json"); /** * The set of all language keys which are explicitly listed as allowed * within guacamole.properties, or null if all defined languages should be * allowed. */ private final Set allowedLanguages; /** * Map of all language resources by language key. Language keys are * language and country code pairs, separated by an underscore, like * "en_US". The country code and underscore SHOULD be omitted in the case * that only one dialect of that language is defined, or in the case of the * most universal or well-supported of all supported dialects of that * language. */ private final Map resources = new HashMap(); /** * Creates a new service for tracking and parsing available translations * which reads its configuration from the given environment. * * @param environment * The environment from which the configuration properties of this * service should be read. */ public LanguageResourceService(Environment environment) { Set parsedAllowedLanguages; // Parse list of available languages from properties try { parsedAllowedLanguages = environment.getProperty(BasicGuacamoleProperties.ALLOWED_LANGUAGES); logger.debug("Available languages will be restricted to: {}", parsedAllowedLanguages); } // Warn of failure to parse catch (GuacamoleException e) { parsedAllowedLanguages = null; logger.error("Unable to parse list of allowed languages: {}", e.getMessage()); logger.debug("Error parsing list of allowed languages.", e); } this.allowedLanguages = parsedAllowedLanguages; } /** * Derives a language key from the filename within the given path, if * possible. If the filename is not a valid language key, null is returned. * * @param path * The path containing the filename to derive the language key from. * * @return * The derived language key, or null if the filename is not a valid * language key. */ public String getLanguageKey(String path) { // Parse language key from filename Matcher languageKeyMatcher = LANGUAGE_KEY_PATTERN.matcher(path); if (!languageKeyMatcher.matches()) return null; // Return parsed key return languageKeyMatcher.group(1); } /** * Merges the given JSON objects. Any leaf node in overlay will overwrite * the corresponding path in original. * * @param original * The original JSON object to which changes should be applied. * * @param overlay * The JSON object containing changes that should be applied. * * @return * The newly constructed JSON object that is the result of merging * original and overlay. */ private JsonNode mergeTranslations(JsonNode original, JsonNode overlay) { // If we are at a leaf node, the result of merging is simply the overlay if (!overlay.isObject() || original == null) return overlay; // Create mutable copy of original ObjectNode newNode = JsonNodeFactory.instance.objectNode(); Iterator fieldNames = original.getFieldNames(); while (fieldNames.hasNext()) { String fieldName = fieldNames.next(); newNode.put(fieldName, original.get(fieldName)); } // Merge each field fieldNames = overlay.getFieldNames(); while (fieldNames.hasNext()) { String fieldName = fieldNames.next(); newNode.put(fieldName, mergeTranslations(original.get(fieldName), overlay.get(fieldName))); } return newNode; } /** * Parses the given language resource, returning the resulting JsonNode. * If the resource cannot be read because it does not exist, null is * returned. * * @param resource * The language resource to parse. Language resources must have the * mimetype "application/json". * * @return * A JsonNode representing the root of the parsed JSON tree, or null if * the given resource does not exist. * * @throws IOException * If an error occurs while parsing the resource as JSON. */ private JsonNode parseLanguageResource(Resource resource) throws IOException { // Get resource stream InputStream stream = resource.asStream(); if (stream == null) return null; // Parse JSON tree try { JsonNode tree = mapper.readTree(stream); return tree; } // Ensure stream is always closed finally { stream.close(); } } /** * Returns whether a language having the given key should be allowed to be * loaded. If language availability restrictions are imposed through * guacamole.properties, this may return false in some cases. By default, * this function will always return true. Note that just because a language * key is allowed to be loaded does not imply that the language key is * valid. * * @param languageKey * The language key of the language to test. * * @return * true if the given language key should be allowed to be loaded, false * otherwise. */ private boolean isLanguageAllowed(String languageKey) { // If no list is provided, all languages are implicitly available if (allowedLanguages == null) return true; return allowedLanguages.contains(languageKey); } /** * Adds or overlays the given language resource, which need not exist in * the ServletContext. If a language resource is already defined for the * given language key, the strings from the given resource will be overlaid * on top of the existing strings, augmenting or overriding the available * strings for that language. * * @param key * The language key of the resource being added. Language keys are * pairs consisting of a language code followed by an underscore and * country code, such as "en_US". * * @param resource * The language resource to add. This resource must have the mimetype * "application/json". */ public void addLanguageResource(String key, Resource resource) { // Skip loading of language if not allowed if (!isLanguageAllowed(key)) { logger.debug("OMITTING language: \"{}\"", key); return; } // Merge language resources if already defined Resource existing = resources.get(key); if (existing != null) { try { // Read the original language resource JsonNode existingTree = parseLanguageResource(existing); if (existingTree == null) { logger.warn("Base language resource \"{}\" does not exist.", key); return; } // Read new language resource JsonNode resourceTree = parseLanguageResource(resource); if (resourceTree == null) { logger.warn("Overlay language resource \"{}\" does not exist.", key); return; } // Merge the language resources JsonNode mergedTree = mergeTranslations(existingTree, resourceTree); resources.put(key, new ByteArrayResource("application/json", mapper.writeValueAsBytes(mergedTree))); logger.debug("Merged strings with existing language: \"{}\"", key); } catch (IOException e) { logger.error("Unable to merge language resource \"{}\": {}", key, e.getMessage()); logger.debug("Error merging language resource.", e); } } // Otherwise, add new language resource else { resources.put(key, resource); logger.debug("Added language: \"{}\"", key); } } /** * Adds or overlays all languages defined within the /translations * directory of the given ServletContext. If no such language files exist, * nothing is done. If a language is already defined, the strings from the * will be overlaid on top of the existing strings, augmenting or * overriding the available strings for that language. The language key * for each language file is derived from the filename. * * @param context * The ServletContext from which language files should be loaded. */ public void addLanguageResources(ServletContext context) { // Get the paths of all the translation files Set resourcePaths = context.getResourcePaths(TRANSLATION_PATH); // If no translation files found, nothing to add if (resourcePaths == null) return; // Iterate through all the found language files and add them to the map for (Object resourcePathObject : resourcePaths) { // Each resource path is guaranteed to be a string String resourcePath = (String) resourcePathObject; // Parse language key from path String languageKey = getLanguageKey(resourcePath); if (languageKey == null) { logger.warn("Invalid language file name: \"{}\"", resourcePath); continue; } // Add/overlay new resource addLanguageResource( languageKey, new WebApplicationResource(context, "application/json", resourcePath) ); } } /** * Returns a set of all unique language keys currently associated with * language resources stored in this service. The returned set cannot be * modified. * * @return * A set of all unique language keys currently associated with this * service. */ public Set getLanguageKeys() { return Collections.unmodifiableSet(resources.keySet()); } /** * Returns a map of all languages currently associated with this service, * where the key of each map entry is the language key. The returned map * cannot be modified. * * @return * A map of all languages currently associated with this service. */ public Map getLanguageResources() { return Collections.unmodifiableMap(resources); } /** * Returns a mapping of all language keys to their corresponding human- * readable language names. If an error occurs while parsing a language * resource, its key/name pair will simply be omitted. The returned map * cannot be modified. * * @return * A map of all language keys and their corresponding human-readable * names. */ public Map getLanguageNames() { Map languageNames = new HashMap(); // For each language key/resource pair for (Map.Entry entry : resources.entrySet()) { // Get language key and resource String languageKey = entry.getKey(); Resource resource = entry.getValue(); // Get stream for resource InputStream resourceStream = resource.asStream(); if (resourceStream == null) { logger.warn("Expected language resource does not exist: \"{}\".", languageKey); continue; } // Get name node of language try { JsonNode tree = mapper.readTree(resourceStream); JsonNode nameNode = tree.get(LANGUAGE_DISPLAY_NAME_KEY); // Attempt to read language name from node String languageName; if (nameNode == null || (languageName = nameNode.getTextValue()) == null) { logger.warn("Root-level \"" + LANGUAGE_DISPLAY_NAME_KEY + "\" string missing or invalid in language \"{}\"", languageKey); languageName = languageKey; } // Add language key/name pair to map languageNames.put(languageKey, languageName); } // Continue with next language if unable to read catch (IOException e) { logger.warn("Unable to read language resource \"{}\".", languageKey); logger.debug("Error reading language resource.", e); } } return Collections.unmodifiableMap(languageNames); } } ././@LongLink0100644 0000000 0000000 00000000152 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/Extension.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/Extension0100664 0001751 0001751 00000043242 12612226251 035451 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.extension; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; import org.codehaus.jackson.JsonParseException; import org.codehaus.jackson.map.ObjectMapper; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleServerException; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.basic.resource.ClassPathResource; import org.glyptodon.guacamole.net.basic.resource.Resource; /** * A Guacamole extension, which may provide custom authentication, static * files, theming/branding, etc. * * @author Michael Jumper */ public class Extension { /** * The Jackson parser for parsing the language JSON files. */ private static final ObjectMapper mapper = new ObjectMapper(); /** * The name of the manifest file that describes the contents of a * Guacamole extension. */ private static final String MANIFEST_NAME = "guac-manifest.json"; /** * The parsed manifest file of this extension, describing the location of * resources within the extension. */ private final ExtensionManifest manifest; /** * The classloader to use when reading resources from this extension, * including classes and static files. */ private final ClassLoader classLoader; /** * Map of all JavaScript resources defined within the extension, where each * key is the path to that resource within the extension. */ private final Map javaScriptResources; /** * Map of all CSS resources defined within the extension, where each key is * the path to that resource within the extension. */ private final Map cssResources; /** * Map of all translation resources defined within the extension, where * each key is the path to that resource within the extension. */ private final Map translationResources; /** * Map of all resources defined within the extension which are not already * associated as JavaScript, CSS, or translation resources, where each key * is the path to that resource within the extension. */ private final Map staticResources; /** * The collection of all AuthenticationProvider classes defined within the * extension. */ private final Collection> authenticationProviderClasses; /** * The resource for the small favicon for the extension. If provided, this * will replace the default Guacamole icon. */ private final Resource smallIcon; /** * The resource foe the large favicon for the extension. If provided, this * will replace the default Guacamole icon. */ private final Resource largeIcon; /** * Returns a new map of all resources corresponding to the collection of * paths provided. Each resource will be associated with the given * mimetype, and stored in the map using its path as the key. * * @param mimetype * The mimetype to associate with each resource. * * @param paths * The paths corresponding to the resources desired. * * @return * A new, unmodifiable map of resources corresponding to the * collection of paths provided, where the key of each entry in the * map is the path for the resource stored in that entry. */ private Map getClassPathResources(String mimetype, Collection paths) { // If no paths are provided, just return an empty map if (paths == null) return Collections.emptyMap(); // Add classpath resource for each path provided Map resources = new HashMap(paths.size()); for (String path : paths) resources.put(path, new ClassPathResource(classLoader, mimetype, path)); // Callers should not rely on modifying the result return Collections.unmodifiableMap(resources); } /** * Returns a new map of all resources corresponding to the map of resource * paths provided. Each resource will be associated with the mimetype * stored in the given map using its path as the key. * * @param resourceTypes * A map of all paths to their corresponding mimetypes. * * @return * A new, unmodifiable map of resources corresponding to the * collection of paths provided, where the key of each entry in the * map is the path for the resource stored in that entry. */ private Map getClassPathResources(Map resourceTypes) { // If no paths are provided, just return an empty map if (resourceTypes == null) return Collections.emptyMap(); // Add classpath resource for each path/mimetype pair provided Map resources = new HashMap(resourceTypes.size()); for (Map.Entry resource : resourceTypes.entrySet()) { // Get path and mimetype from entry String path = resource.getKey(); String mimetype = resource.getValue(); // Store as path/resource pair resources.put(path, new ClassPathResource(classLoader, mimetype, path)); } // Callers should not rely on modifying the result return Collections.unmodifiableMap(resources); } /** * Retrieve the AuthenticationProvider subclass having the given name. If * the class having the given name does not exist or isn't actually a * subclass of AuthenticationProvider, an exception will be thrown. * * @param name * The name of the AuthenticationProvider class to retrieve. * * @return * The subclass of AuthenticationProvider having the given name. * * @throws GuacamoleException * If no such class exists, or if the class with the given name is not * a subclass of AuthenticationProvider. */ @SuppressWarnings("unchecked") // We check this ourselves with isAssignableFrom() private Class getAuthenticationProviderClass(String name) throws GuacamoleException { try { // Get authentication provider class Class authenticationProviderClass = classLoader.loadClass(name); // Verify the located class is actually a subclass of AuthenticationProvider if (!AuthenticationProvider.class.isAssignableFrom(authenticationProviderClass)) throw new GuacamoleServerException("Authentication providers MUST extend the AuthenticationProvider class."); // Return located class return (Class) authenticationProviderClass; } catch (ClassNotFoundException e) { throw new GuacamoleException("Authentication provider class not found.", e); } } /** * Returns a new collection of all AuthenticationProvider subclasses having * the given names. If any class does not exist or isn't actually a * subclass of AuthenticationProvider, an exception will be thrown, and * no further AuthenticationProvider classes will be loaded. * * @param names * The names of the AuthenticationProvider classes to retrieve. * * @return * A new collection of all AuthenticationProvider subclasses having the * given names. * * @throws GuacamoleException * If any given class does not exist, or if any given class is not a * subclass of AuthenticationProvider. */ private Collection> getAuthenticationProviderClasses(Collection names) throws GuacamoleException { // If no classnames are provided, just return an empty list if (names == null) return Collections.>emptyList(); // Define all auth provider classes Collection> classes = new ArrayList>(names.size()); for (String name : names) classes.add(getAuthenticationProviderClass(name)); // Callers should not rely on modifying the result return Collections.unmodifiableCollection(classes); } /** * Loads the given file as an extension, which must be a .jar containing * a guac-manifest.json file describing its contents. * * @param parent * The classloader to use as the parent for the isolated classloader of * this extension. * * @param file * The file to load as an extension. * * @throws GuacamoleException * If the provided file is not a .jar file, does not contain the * guac-manifest.json, or if guac-manifest.json is invalid and cannot * be parsed. */ public Extension(final ClassLoader parent, final File file) throws GuacamoleException { try { // Open extension ZipFile extension = new ZipFile(file); try { // Retrieve extension manifest ZipEntry manifestEntry = extension.getEntry(MANIFEST_NAME); if (manifestEntry == null) throw new GuacamoleServerException("Extension " + file.getName() + " is missing " + MANIFEST_NAME); // Parse manifest manifest = mapper.readValue(extension.getInputStream(manifestEntry), ExtensionManifest.class); if (manifest == null) throw new GuacamoleServerException("Contents of " + MANIFEST_NAME + " must be a valid JSON object."); } // Always close zip file, if possible finally { extension.close(); } try { // Create isolated classloader for this extension classLoader = AccessController.doPrivileged(new PrivilegedExceptionAction() { @Override public ClassLoader run() throws GuacamoleException { try { // Classloader must contain only the extension itself return new URLClassLoader(new URL[]{file.toURI().toURL()}, parent); } catch (MalformedURLException e) { throw new GuacamoleException(e); } } }); } // Rethrow any GuacamoleException catch (PrivilegedActionException e) { throw (GuacamoleException) e.getException(); } } // Abort load if not a valid zip file catch (ZipException e) { throw new GuacamoleServerException("Extension is not a valid zip file: " + file.getName(), e); } // Abort if manifest cannot be parsed (invalid JSON) catch (JsonParseException e) { throw new GuacamoleServerException(MANIFEST_NAME + " is not valid JSON: " + file.getName(), e); } // Abort if zip file cannot be read at all due to I/O errors catch (IOException e) { throw new GuacamoleServerException("Unable to read extension: " + file.getName(), e); } // Define static resources cssResources = getClassPathResources("text/css", manifest.getCSSPaths()); javaScriptResources = getClassPathResources("text/javascript", manifest.getJavaScriptPaths()); translationResources = getClassPathResources("application/json", manifest.getTranslationPaths()); staticResources = getClassPathResources(manifest.getResourceTypes()); // Define authentication providers authenticationProviderClasses = getAuthenticationProviderClasses(manifest.getAuthProviders()); // Get small icon resource if provided if (manifest.getSmallIcon() != null) smallIcon = new ClassPathResource(classLoader, "image/png", manifest.getSmallIcon()); else smallIcon = null; // Get large icon resource if provided if (manifest.getLargeIcon() != null) largeIcon = new ClassPathResource(classLoader, "image/png", manifest.getLargeIcon()); else largeIcon = null; } /** * Returns the version of the Guacamole web application for which this * extension was built. * * @return * The version of the Guacamole web application for which this * extension was built. */ public String getGuacamoleVersion() { return manifest.getGuacamoleVersion(); } /** * Returns the name of this extension, as declared in the extension's * manifest. * * @return * The name of this extension. */ public String getName() { return manifest.getName(); } /** * Returns the namespace of this extension, as declared in the extension's * manifest. * * @return * The namespace of this extension. */ public String getNamespace() { return manifest.getNamespace(); } /** * Returns a map of all declared JavaScript resources associated with this * extension, where the key of each entry in the map is the path to that * resource within the extension .jar. JavaScript resources are declared * within the extension manifest. * * @return * All declared JavaScript resources associated with this extension. */ public Map getJavaScriptResources() { return javaScriptResources; } /** * Returns a map of all declared CSS resources associated with this * extension, where the key of each entry in the map is the path to that * resource within the extension .jar. CSS resources are declared within * the extension manifest. * * @return * All declared CSS resources associated with this extension. */ public Map getCSSResources() { return cssResources; } /** * Returns a map of all declared translation resources associated with this * extension, where the key of each entry in the map is the path to that * resource within the extension .jar. Translation resources are declared * within the extension manifest. * * @return * All declared translation resources associated with this extension. */ public Map getTranslationResources() { return translationResources; } /** * Returns a map of all declared resources associated with this extension, * where these resources are not already associated as JavaScript, CSS, or * translation resources. The key of each entry in the map is the path to * that resource within the extension .jar. Static resources are declared * within the extension manifest. * * @return * All declared static resources associated with this extension. */ public Map getStaticResources() { return staticResources; } /** * Returns all declared authentication providers classes associated with * this extension. Authentication providers are declared within the * extension manifest. * * @return * All declared authentication provider classes with this extension. */ public Collection> getAuthenticationProviderClasses() { return authenticationProviderClasses; } /** * Returns the resource for the small favicon for the extension. If * provided, this will replace the default Guacamole icon. * * @return * The resource for the small favicon. */ public Resource getSmallIcon() { return smallIcon; } /** * Returns the resource for the large favicon for the extension. If * provided, this will replace the default Guacamole icon. * * @return * The resource for the large favicon. */ public Resource getLargeIcon() { return largeIcon; } } ././@LongLink0100644 0000000 0000000 00000000175 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/AuthenticationProviderFacade.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/Authentic0100664 0001751 0001751 00000021350 12612226251 035415 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.extension; import java.lang.reflect.InvocationTargetException; import java.util.UUID; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.AuthenticatedUser; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo; import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Provides a safe wrapper around an AuthenticationProvider subclass, such that * authentication attempts can cleanly fail, and errors can be properly logged, * even if the AuthenticationProvider cannot be instantiated. * * @author Michael Jumper */ public class AuthenticationProviderFacade implements AuthenticationProvider { /** * Logger for this class. */ private Logger logger = LoggerFactory.getLogger(AuthenticationProviderFacade.class); /** * The underlying authentication provider, or null if the authentication * provider could not be instantiated. */ private final AuthenticationProvider authProvider; /** * The identifier to provide for the underlying authentication provider if * the authentication provider could not be loaded. */ private final String facadeIdentifier = UUID.randomUUID().toString(); /** * Creates a new AuthenticationProviderFacade which delegates all function * calls to an instance of the given AuthenticationProvider subclass. If * an instance of the given class cannot be created, creation of this * facade will still succeed, but its use will result in errors being * logged, and all authentication attempts will fail. * * @param authProviderClass * The AuthenticationProvider subclass to instantiate. */ public AuthenticationProviderFacade(Class authProviderClass) { AuthenticationProvider instance = null; try { // Attempt to instantiate the authentication provider instance = authProviderClass.getConstructor().newInstance(); } catch (NoSuchMethodException e) { logger.error("The authentication extension in use is not properly defined. " + "Please contact the developers of the extension or, if you " + "are the developer, turn on debug-level logging."); logger.debug("AuthenticationProvider is missing a default constructor.", e); } catch (SecurityException e) { logger.error("The Java security mananager is preventing authentication extensions " + "from being loaded. Please check the configuration of Java or your " + "servlet container."); logger.debug("Creation of AuthenticationProvider disallowed by security manager.", e); } catch (InstantiationException e) { logger.error("The authentication extension in use is not properly defined. " + "Please contact the developers of the extension or, if you " + "are the developer, turn on debug-level logging."); logger.debug("AuthenticationProvider cannot be instantiated.", e); } catch (IllegalAccessException e) { logger.error("The authentication extension in use is not properly defined. " + "Please contact the developers of the extension or, if you " + "are the developer, turn on debug-level logging."); logger.debug("Default constructor of AuthenticationProvider is not public.", e); } catch (IllegalArgumentException e) { logger.error("The authentication extension in use is not properly defined. " + "Please contact the developers of the extension or, if you " + "are the developer, turn on debug-level logging."); logger.debug("Default constructor of AuthenticationProvider cannot accept zero arguments.", e); } catch (InvocationTargetException e) { // Obtain causing error - create relatively-informative stub error if cause is unknown Throwable cause = e.getCause(); if (cause == null) cause = new GuacamoleException("Error encountered during initialization."); logger.error("Authentication extension failed to start: {}", cause.getMessage()); logger.debug("AuthenticationProvider instantiation failed.", e); } // Associate instance, if any authProvider = instance; } @Override public String getIdentifier() { // Ignore auth attempts if no auth provider could be loaded if (authProvider == null) { logger.warn("The authentication system could not be loaded. Please check for errors earlier in the logs."); return facadeIdentifier; } // Delegate to underlying auth provider return authProvider.getIdentifier(); } @Override public AuthenticatedUser authenticateUser(Credentials credentials) throws GuacamoleException { // Ignore auth attempts if no auth provider could be loaded if (authProvider == null) { logger.warn("Authentication attempt denied because the authentication system could not be loaded. Please check for errors earlier in the logs."); throw new GuacamoleInvalidCredentialsException("Permission denied.", CredentialsInfo.USERNAME_PASSWORD); } // Delegate to underlying auth provider return authProvider.authenticateUser(credentials); } @Override public AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser, Credentials credentials) throws GuacamoleException { // Ignore auth attempts if no auth provider could be loaded if (authProvider == null) { logger.warn("Reauthentication attempt denied because the authentication system could not be loaded. Please check for errors earlier in the logs."); throw new GuacamoleInvalidCredentialsException("Permission denied.", CredentialsInfo.USERNAME_PASSWORD); } // Delegate to underlying auth provider return authProvider.updateAuthenticatedUser(authenticatedUser, credentials); } @Override public UserContext getUserContext(AuthenticatedUser authenticatedUser) throws GuacamoleException { // Ignore auth attempts if no auth provider could be loaded if (authProvider == null) { logger.warn("User data retrieval attempt denied because the authentication system could not be loaded. Please check for errors earlier in the logs."); return null; } // Delegate to underlying auth provider return authProvider.getUserContext(authenticatedUser); } @Override public UserContext updateUserContext(UserContext context, AuthenticatedUser authenticatedUser) throws GuacamoleException { // Ignore auth attempts if no auth provider could be loaded if (authProvider == null) { logger.warn("User data refresh attempt denied because the authentication system could not be loaded. Please check for errors earlier in the logs."); return null; } // Delegate to underlying auth provider return authProvider.updateUserContext(context, authenticatedUser); } } guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/package-info.java0100664 0001751 0001751 00000002420 12471273273 034727 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes specific to the general-purpose web application implemented by * the Guacamole project using the Guacamole APIs. */ package org.glyptodon.guacamole.net.basic; guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelLoader.java0100664 0001751 0001751 00000003324 12612226251 034772 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic; import com.google.inject.Module; /** * Generic means of loading a tunnel without adding explicit dependencies within * the main ServletModule, as not all servlet containers may have the classes * required by all tunnel implementations. * * @author Michael Jumper */ public interface TunnelLoader extends Module { /** * Checks whether this type of tunnel is supported by the servlet container. * * @return true if this type of tunnel is supported and can be loaded * without errors, false otherwise. */ public boolean isSupported(); } ././@LongLink0100644 0000000 0000000 00000000147 12632406624 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.ja0100664 0001751 0001751 00000016471 12632406624 035325 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.AuthenticatedUser; import org.glyptodon.guacamole.net.auth.UserContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Contains Guacamole-specific user information which is tied to the current * session, such as the UserContext and current clipboard state. * * @author Michael Jumper */ public class GuacamoleSession { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(GuacamoleSession.class); /** * The user associated with this session. */ private AuthenticatedUser authenticatedUser; /** * All UserContexts associated with this session. Each * AuthenticationProvider may provide its own UserContext. */ private List userContexts; /** * All currently-active tunnels, indexed by tunnel UUID. */ private final Map tunnels = new ConcurrentHashMap(); /** * The last time this session was accessed. */ private long lastAccessedTime; /** * Creates a new Guacamole session associated with the given * AuthenticatedUser and UserContexts. * * @param environment * The environment of the Guacamole server associated with this new * session. * * @param authenticatedUser * The authenticated user to associate this session with. * * @param userContexts * The List of UserContexts to associate with this session. * * @throws GuacamoleException * If an error prevents the session from being created. */ public GuacamoleSession(Environment environment, AuthenticatedUser authenticatedUser, List userContexts) throws GuacamoleException { this.lastAccessedTime = System.currentTimeMillis(); this.authenticatedUser = authenticatedUser; this.userContexts = userContexts; } /** * Returns the authenticated user associated with this session. * * @return * The authenticated user associated with this session. */ public AuthenticatedUser getAuthenticatedUser() { return authenticatedUser; } /** * Replaces the authenticated user associated with this session with the * given authenticated user. * * @param authenticatedUser * The authenticated user to associated with this session. */ public void setAuthenticatedUser(AuthenticatedUser authenticatedUser) { this.authenticatedUser = authenticatedUser; } /** * Returns a list of all UserContexts associated with this session. Each * AuthenticationProvider currently loaded by Guacamole may provide its own * UserContext for any successfully-authenticated user. * * @return * An unmodifiable list of all UserContexts associated with this * session. */ public List getUserContexts() { return Collections.unmodifiableList(userContexts); } /** * Replaces all UserContexts associated with this session with the given * List of UserContexts. * * @param userContexts * The List of UserContexts to associate with this session. */ public void setUserContexts(List userContexts) { this.userContexts = userContexts; } /** * Returns whether this session has any associated active tunnels. * * @return true if this session has any associated active tunnels, * false otherwise. */ public boolean hasTunnels() { return !tunnels.isEmpty(); } /** * Returns a map of all active tunnels associated with this session, where * each key is the String representation of the tunnel's UUID. Changes to * this map immediately affect the set of tunnels associated with this * session. A tunnel need not be present here to be used by the user * associated with this session, but tunnels not in this set will not * be taken into account when determining whether a session is in use. * * @return A map of all active tunnels associated with this session. */ public Map getTunnels() { return tunnels; } /** * Associates the given tunnel with this session, such that it is taken * into account when determining session activity. * * @param tunnel The tunnel to associate with this session. */ public void addTunnel(GuacamoleTunnel tunnel) { tunnels.put(tunnel.getUUID().toString(), tunnel); } /** * Disassociates the tunnel having the given UUID from this session. * * @param uuid The UUID of the tunnel to disassociate from this session. * @return true if the tunnel existed and was removed, false otherwise. */ public boolean removeTunnel(String uuid) { return tunnels.remove(uuid) != null; } /** * Updates this session, marking it as accessed. */ public void access() { lastAccessedTime = System.currentTimeMillis(); } /** * Returns the time this session was last accessed, as the number of * milliseconds since midnight January 1, 1970 GMT. Session access must * be explicitly marked through calls to the access() function. * * @return The time this session was last accessed. */ public long getLastAccessedTime() { return lastAccessedTime; } /** * Closes all associated tunnels and prevents any further use of this * session. */ public void invalidate() { // Close all associated tunnels, if possible for (GuacamoleTunnel tunnel : tunnels.values()) { try { tunnel.close(); } catch (GuacamoleException e) { logger.debug("Unable to close tunnel.", e); } } } } ././@LongLink0100644 0000000 0000000 00000000162 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicGuacamoleTunnelServlet.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicGuacamoleTunne0100664 0001751 0001751 00000004610 12612226251 035326 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic; import com.google.inject.Inject; import com.google.inject.Singleton; import javax.servlet.http.HttpServletRequest; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.servlet.GuacamoleHTTPTunnelServlet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Connects users to a tunnel associated with the authorized connection * having the given ID. * * @author Michael Jumper */ @Singleton public class BasicGuacamoleTunnelServlet extends GuacamoleHTTPTunnelServlet { /** * Service for handling tunnel requests. */ @Inject private TunnelRequestService tunnelRequestService; /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(BasicGuacamoleTunnelServlet.class); @Override protected GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException { // Attempt to create HTTP tunnel GuacamoleTunnel tunnel = tunnelRequestService.createTunnel(new HTTPTunnelRequest(request)); // If successful, warn of lack of WebSocket logger.info("Using HTTP tunnel (not WebSocket). Performance may be sub-optimal."); return tunnel; } } guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelModule.java0100664 0001751 0001751 00000007423 12612226251 035015 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic; import com.google.inject.servlet.ServletModule; import java.lang.reflect.InvocationTargetException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Module which loads tunnel implementations. * * @author Michael Jumper */ public class TunnelModule extends ServletModule { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(TunnelModule.class); /** * Classnames of all implementation-specific WebSocket tunnel modules. */ private static final String[] WEBSOCKET_MODULES = { "org.glyptodon.guacamole.net.basic.websocket.WebSocketTunnelModule", "org.glyptodon.guacamole.net.basic.websocket.jetty8.WebSocketTunnelModule", "org.glyptodon.guacamole.net.basic.websocket.jetty9.WebSocketTunnelModule", "org.glyptodon.guacamole.net.basic.websocket.tomcat.WebSocketTunnelModule" }; private boolean loadWebSocketModule(String classname) { try { // Attempt to find WebSocket module Class module = Class.forName(classname); // Create loader TunnelLoader loader = (TunnelLoader) module.getConstructor().newInstance(); // Install module, if supported if (loader.isSupported()) { install(loader); return true; } } // If no such class or constructor, etc., then this particular // WebSocket support is not present catch (ClassNotFoundException e) {} catch (NoClassDefFoundError e) {} catch (NoSuchMethodException e) {} // Log errors which indicate bugs catch (InstantiationException e) { logger.debug("Error instantiating WebSocket module.", e); } catch (IllegalAccessException e) { logger.debug("Error instantiating WebSocket module.", e); } catch (InvocationTargetException e) { logger.debug("Error instantiating WebSocket module.", e); } // Load attempt failed return false; } @Override protected void configureServlets() { bind(TunnelRequestService.class); // Set up HTTP tunnel serve("/tunnel").with(BasicGuacamoleTunnelServlet.class); // Try to load each WebSocket tunnel in sequence for (String classname : WEBSOCKET_MODULES) { if (loadWebSocketModule(classname)) { logger.debug("WebSocket module loaded: {}", classname); return; } } // Warn of lack of WebSocket logger.info("WebSocket support NOT present. Only HTTP will be used."); } } ././@LongLink0100644 0000000 0000000 00000000153 12645510255 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestServic0100664 0001751 0001751 00000031656 12645510255 035467 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic; import com.google.inject.Inject; import com.google.inject.Singleton; import java.util.List; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.GuacamoleUnauthorizedException; import org.glyptodon.guacamole.net.DelegatingGuacamoleTunnel; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Utility class that takes a standard request from the Guacamole JavaScript * client and produces the corresponding GuacamoleTunnel. The implementation * of this utility is specific to the form of request used by the upstream * Guacamole web application, and is not necessarily useful to applications * that use purely the Guacamole API. * * @author Michael Jumper * @author Vasily Loginov */ @Singleton public class TunnelRequestService { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(TunnelRequestService.class); /** * A service for authenticating users from auth tokens. */ @Inject private AuthenticationService authenticationService; /** * Service for convenient retrieval of objects. */ @Inject private ObjectRetrievalService retrievalService; /** * Reads and returns the client information provided within the given * request. * * @param request * The request describing tunnel to create. * * @return GuacamoleClientInformation * An object containing information about the client sending the tunnel * request. * * @throws GuacamoleException * If the parameters of the tunnel request are invalid. */ protected GuacamoleClientInformation getClientInformation(TunnelRequest request) throws GuacamoleException { // Get client information GuacamoleClientInformation info = new GuacamoleClientInformation(); // Set width if provided Integer width = request.getWidth(); if (width != null) info.setOptimalScreenWidth(width); // Set height if provided Integer height = request.getHeight(); if (height != null) info.setOptimalScreenHeight(height); // Set resolution if provided Integer dpi = request.getDPI(); if (dpi != null) info.setOptimalResolution(dpi); // Add audio mimetypes List audioMimetypes = request.getAudioMimetypes(); if (audioMimetypes != null) info.getAudioMimetypes().addAll(audioMimetypes); // Add video mimetypes List videoMimetypes = request.getVideoMimetypes(); if (videoMimetypes != null) info.getVideoMimetypes().addAll(videoMimetypes); // Add image mimetypes List imageMimetypes = request.getImageMimetypes(); if (imageMimetypes != null) info.getImageMimetypes().addAll(imageMimetypes); return info; } /** * Creates a new tunnel using which is connected to the connection or * connection group identifier by the given ID. Client information * is specified in the {@code info} parameter. * * @param context * The UserContext associated with the user for whom the tunnel is * being created. * * @param type * The type of object being connected to (connection or group). * * @param id * The id of the connection or group being connected to. * * @param info * Information describing the connected Guacamole client. * * @return * A new tunnel, connected as required by the request. * * @throws GuacamoleException * If an error occurs while creating the tunnel. */ protected GuacamoleTunnel createConnectedTunnel(UserContext context, final TunnelRequest.Type type, String id, GuacamoleClientInformation info) throws GuacamoleException { // Create connected tunnel from identifier GuacamoleTunnel tunnel = null; switch (type) { // Connection identifiers case CONNECTION: { // Get connection directory Directory directory = context.getConnectionDirectory(); // Get authorized connection Connection connection = directory.get(id); if (connection == null) { logger.info("Connection \"{}\" does not exist for user \"{}\".", id, context.self().getIdentifier()); throw new GuacamoleSecurityException("Requested connection is not authorized."); } // Connect tunnel tunnel = connection.connect(info); logger.info("User \"{}\" connected to connection \"{}\".", context.self().getIdentifier(), id); break; } // Connection group identifiers case CONNECTION_GROUP: { // Get connection group directory Directory directory = context.getConnectionGroupDirectory(); // Get authorized connection group ConnectionGroup group = directory.get(id); if (group == null) { logger.info("Connection group \"{}\" does not exist for user \"{}\".", id, context.self().getIdentifier()); throw new GuacamoleSecurityException("Requested connection group is not authorized."); } // Connect tunnel tunnel = group.connect(info); logger.info("User \"{}\" connected to group \"{}\".", context.self().getIdentifier(), id); break; } // Type is guaranteed to be one of the above default: assert(false); } return tunnel; } /** * Associates the given tunnel with the given session, returning a wrapped * version of the same tunnel which automatically handles closure and * removal from the session. * * @param tunnel * The connected tunnel to wrap and monitor. * * @param authToken * The authentication token associated with the given session. If * provided, this token will be automatically invalidated (and the * corresponding session destroyed) if tunnel errors imply that the * user is no longer authorized. * * @param session * The Guacamole session to associate the tunnel with. * * @param type * The type of object being connected to (connection or group). * * @param id * The id of the connection or group being connected to. * * @return * A new tunnel, associated with the given session, which delegates all * functionality to the given tunnel while monitoring and automatically * handling closure. * * @throws GuacamoleException * If an error occurs while obtaining the tunnel. */ protected GuacamoleTunnel createAssociatedTunnel(GuacamoleTunnel tunnel, final String authToken, final GuacamoleSession session, final TunnelRequest.Type type, final String id) throws GuacamoleException { // Monitor tunnel closure and data GuacamoleTunnel monitoredTunnel = new DelegatingGuacamoleTunnel(tunnel) { /** * The time the connection began, measured in milliseconds since * midnight, January 1, 1970 UTC. */ private final long connectionStartTime = System.currentTimeMillis(); @Override public void close() throws GuacamoleException { long connectionEndTime = System.currentTimeMillis(); long duration = connectionEndTime - connectionStartTime; // Log closure switch (type) { // Connection identifiers case CONNECTION: logger.info("User \"{}\" disconnected from connection \"{}\". Duration: {} milliseconds", session.getAuthenticatedUser().getIdentifier(), id, duration); break; // Connection group identifiers case CONNECTION_GROUP: logger.info("User \"{}\" disconnected from connection group \"{}\". Duration: {} milliseconds", session.getAuthenticatedUser().getIdentifier(), id, duration); break; // Type is guaranteed to be one of the above default: assert(false); } try { // Close and clean up tunnel session.removeTunnel(getUUID().toString()); super.close(); } // Ensure any associated session is invalidated if unauthorized catch (GuacamoleUnauthorizedException e) { // If there is an associated auth token, invalidate it if (authenticationService.destroyGuacamoleSession(authToken)) logger.debug("Implicitly invalidated session for token \"{}\".", authToken); // Continue with exception processing throw e; } } }; // Associate tunnel with session session.addTunnel(monitoredTunnel); return monitoredTunnel; } /** * Creates a new tunnel using the parameters and credentials present in * the given request. * * @param request * The request describing the tunnel to create. * * @return * The created tunnel, or null if the tunnel could not be created. * * @throws GuacamoleException * If an error occurs while creating the tunnel. */ public GuacamoleTunnel createTunnel(TunnelRequest request) throws GuacamoleException { // Parse request parameters String authToken = request.getAuthenticationToken(); String id = request.getIdentifier(); TunnelRequest.Type type = request.getType(); String authProviderIdentifier = request.getAuthenticationProviderIdentifier(); GuacamoleClientInformation info = getClientInformation(request); GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); try { // Create connected tunnel using provided connection ID and client information GuacamoleTunnel tunnel = createConnectedTunnel(userContext, type, id, info); // Associate tunnel with session return createAssociatedTunnel(tunnel, authToken, session, type, id); } // Ensure any associated session is invalidated if unauthorized catch (GuacamoleUnauthorizedException e) { // If there is an associated auth token, invalidate it if (authenticationService.destroyGuacamoleSession(authToken)) logger.debug("Implicitly invalidated session for token \"{}\".", authToken); // Continue with exception processing throw e; } } } guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequest.java0100664 0001751 0001751 00000030076 12645510255 035226 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic; import java.util.List; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; /** * A request object which provides only the functions absolutely required to * retrieve and connect to a tunnel. * * @author Michael Jumper */ public abstract class TunnelRequest { /** * The name of the request parameter containing the user's authentication * token. */ public static final String AUTH_TOKEN_PARAMETER = "token"; /** * The name of the parameter containing the identifier of the * AuthenticationProvider associated with the UserContext containing the * object to which a tunnel is being requested. */ public static final String AUTH_PROVIDER_IDENTIFIER_PARAMETER = "GUAC_DATA_SOURCE"; /** * The name of the parameter specifying the type of object to which a * tunnel is being requested. Currently, this may be "c" for a Guacamole * connection, or "g" for a Guacamole connection group. */ public static final String TYPE_PARAMETER = "GUAC_TYPE"; /** * The name of the parameter containing the unique identifier of the object * to which a tunnel is being requested. */ public static final String IDENTIFIER_PARAMETER = "GUAC_ID"; /** * The name of the parameter containing the desired display width, in * pixels. */ public static final String WIDTH_PARAMETER = "GUAC_WIDTH"; /** * The name of the parameter containing the desired display height, in * pixels. */ public static final String HEIGHT_PARAMETER = "GUAC_HEIGHT"; /** * The name of the parameter containing the desired display resolution, in * DPI. */ public static final String DPI_PARAMETER = "GUAC_DPI"; /** * The name of the parameter specifying one supported audio mimetype. This * will normally appear multiple times within a single tunnel request - * once for each mimetype. */ public static final String AUDIO_PARAMETER = "GUAC_AUDIO"; /** * The name of the parameter specifying one supported video mimetype. This * will normally appear multiple times within a single tunnel request - * once for each mimetype. */ public static final String VIDEO_PARAMETER = "GUAC_VIDEO"; /** * The name of the parameter specifying one supported image mimetype. This * will normally appear multiple times within a single tunnel request - * once for each mimetype. */ public static final String IMAGE_PARAMETER = "GUAC_IMAGE"; /** * All supported object types that can be used as the destination of a * tunnel. */ public static enum Type { /** * A Guacamole connection. */ CONNECTION("c"), /** * A Guacamole connection group. */ CONNECTION_GROUP("g"); /** * The parameter value which denotes a destination object of this type. */ final String PARAMETER_VALUE; /** * Defines a Type having the given corresponding parameter value. * * @param value * The parameter value which denotes a destination object of this * type. */ Type(String value) { PARAMETER_VALUE = value; } }; /** * Returns the value of the parameter having the given name. * * @param name * The name of the parameter to return. * * @return * The value of the parameter having the given name, or null if no such * parameter was specified. */ public abstract String getParameter(String name); /** * Returns a list of all values specified for the given parameter. * * @param name * The name of the parameter to return. * * @return * All values of the parameter having the given name , or null if no * such parameter was specified. */ public abstract List getParameterValues(String name); /** * Returns the value of the parameter having the given name, throwing an * exception if the parameter is missing. * * @param name * The name of the parameter to return. * * @return * The value of the parameter having the given name. * * @throws GuacamoleException * If the parameter is not present in the request. */ public String getRequiredParameter(String name) throws GuacamoleException { // Pull requested parameter, aborting if absent String value = getParameter(name); if (value == null) throw new GuacamoleClientException("Parameter \"" + name + "\" is required."); return value; } /** * Returns the integer value of the parameter having the given name, * throwing an exception if the parameter cannot be parsed. * * @param name * The name of the parameter to return. * * @return * The integer value of the parameter having the given name, or null if * the parameter is missing. * * @throws GuacamoleException * If the parameter is not a valid integer. */ public Integer getIntegerParameter(String name) throws GuacamoleException { // Pull requested parameter String value = getParameter(name); if (value == null) return null; // Attempt to parse as an integer try { return Integer.parseInt(value); } // Rethrow any parsing error as a GuacamoleClientException catch (NumberFormatException e) { throw new GuacamoleClientException("Parameter \"" + name + "\" must be a valid integer.", e); } } /** * Returns the authentication token associated with this tunnel request. * * @return * The authentication token associated with this tunnel request, or * null if no authentication token is present. */ public String getAuthenticationToken() { return getParameter(AUTH_TOKEN_PARAMETER); } /** * Returns the identifier of the AuthenticationProvider associated with the * UserContext from which the connection or connection group is to be * retrieved when the tunnel is created. In the context of the REST API and * the JavaScript side of the web application, this is referred to as the * data source identifier. * * @return * The identifier of the AuthenticationProvider associated with the * UserContext from which the connection or connection group is to be * retrieved when the tunnel is created. * * @throws GuacamoleException * If the identifier was not present in the request. */ public String getAuthenticationProviderIdentifier() throws GuacamoleException { return getRequiredParameter(AUTH_PROVIDER_IDENTIFIER_PARAMETER); } /** * Returns the type of object for which the tunnel is being requested. * * @return * The type of object for which the tunnel is being requested. * * @throws GuacamoleException * If the type was not present in the request, or if the type requested * is in the wrong format. */ public Type getType() throws GuacamoleException { String type = getRequiredParameter(TYPE_PARAMETER); // For each possible object type for (Type possibleType : Type.values()) { // Match against defined parameter value if (type.equals(possibleType.PARAMETER_VALUE)) return possibleType; } throw new GuacamoleClientException("Illegal identifier - unknown type."); } /** * Returns the identifier of the destination of the tunnel being requested. * As there are multiple types of destination objects available, and within * multiple data sources, the associated object type and data source are * also necessary to determine what this identifier refers to. * * @return * The identifier of the destination of the tunnel being requested. * * @throws GuacamoleException * If the identifier was not present in the request. */ public String getIdentifier() throws GuacamoleException { return getRequiredParameter(IDENTIFIER_PARAMETER); } /** * Returns the display width desired for the Guacamole session over the * tunnel being requested. * * @return * The display width desired for the Guacamole session over the tunnel * being requested, or null if no width was given. * * @throws GuacamoleException * If the width specified was not a valid integer. */ public Integer getWidth() throws GuacamoleException { return getIntegerParameter(WIDTH_PARAMETER); } /** * Returns the display height desired for the Guacamole session over the * tunnel being requested. * * @return * The display height desired for the Guacamole session over the tunnel * being requested, or null if no width was given. * * @throws GuacamoleException * If the height specified was not a valid integer. */ public Integer getHeight() throws GuacamoleException { return getIntegerParameter(HEIGHT_PARAMETER); } /** * Returns the display resolution desired for the Guacamole session over * the tunnel being requested, in DPI. * * @return * The display resolution desired for the Guacamole session over the * tunnel being requested, or null if no resolution was given. * * @throws GuacamoleException * If the resolution specified was not a valid integer. */ public Integer getDPI() throws GuacamoleException { return getIntegerParameter(DPI_PARAMETER); } /** * Returns a list of all audio mimetypes declared as supported within the * tunnel request. * * @return * A list of all audio mimetypes declared as supported within the * tunnel request, or null if no mimetypes were specified. */ public List getAudioMimetypes() { return getParameterValues(AUDIO_PARAMETER); } /** * Returns a list of all video mimetypes declared as supported within the * tunnel request. * * @return * A list of all video mimetypes declared as supported within the * tunnel request, or null if no mimetypes were specified. */ public List getVideoMimetypes() { return getParameterValues(VIDEO_PARAMETER); } /** * Returns a list of all image mimetypes declared as supported within the * tunnel request. * * @return * A list of all image mimetypes declared as supported within the * tunnel request, or null if no mimetypes were specified. */ public List getImageMimetypes() { return getParameterValues(IMAGE_PARAMETER); } } guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/log/LogModule.java0100664 0001751 0001751 00000006513 12612226251 035051 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.log; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.util.StatusPrinter; import com.google.inject.AbstractModule; import java.io.File; import org.glyptodon.guacamole.environment.Environment; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Initializes the logging subsystem. * * @author Michael Jumper */ public class LogModule extends AbstractModule { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(LogModule.class); /** * The Guacamole server environment. */ private final Environment environment; /** * Creates a new LogModule which uses the given environment to determine * the logging configuration. * * @param environment * The environment to use when configuring logging. */ public LogModule(Environment environment) { this.environment = environment; } @Override protected void configure() { // Only load logback configuration if GUACAMOLE_HOME exists File guacamoleHome = environment.getGuacamoleHome(); if (!guacamoleHome.isDirectory()) return; // Check for custom logback.xml File logbackConfiguration = new File(guacamoleHome, "logback.xml"); if (!logbackConfiguration.exists()) return; logger.info("Loading logback configuration from \"{}\".", logbackConfiguration); LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); context.reset(); try { // Initialize logback JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(context); configurator.doConfigure(logbackConfiguration); // Dump any errors that occur during logback init StatusPrinter.printInCaseOfErrorsOrWarnings(context); } catch (JoranException e) { logger.error("Initialization of logback failed: {}", e.getMessage()); logger.debug("Unable to load logback configuration..", e); } } } ././@LongLink0100644 0000000 0000000 00000000145 12471273273 011643 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/ClipboardState.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/ClipboardState.java0100664 0001751 0001751 00000010707 12471273273 035312 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic; /** * Provides central storage for a cross-connection clipboard state. This * clipboard state is shared only for a single HTTP session. Multiple HTTP * sessions will all have their own state. * * @author Michael Jumper */ public class ClipboardState { /** * The maximum number of bytes to track. */ private static final int MAXIMUM_LENGTH = 262144; /** * The mimetype of the current contents. */ private String mimetype = "text/plain"; /** * The mimetype of the pending contents. */ private String pending_mimetype = "text/plain"; /** * The current contents. */ private byte[] contents = new byte[0]; /** * The pending clipboard contents. */ private final byte[] pending = new byte[MAXIMUM_LENGTH]; /** * The length of the pending data, in bytes. */ private int pending_length = 0; /** * The timestamp of the last contents update. */ private long last_update = 0; /** * Returns the current clipboard contents. * @return The current clipboard contents */ public synchronized byte[] getContents() { return contents; } /** * Returns the mimetype of the current clipboard contents. * @return The mimetype of the current clipboard contents. */ public synchronized String getMimetype() { return mimetype; } /** * Begins a new update of the clipboard contents. The actual contents will * not be saved until commit() is called. * * @param mimetype The mimetype of the contents being added. */ public synchronized void begin(String mimetype) { pending_length = 0; this.pending_mimetype = mimetype; } /** * Appends the given data to the clipboard contents. * * @param data The raw data to append. */ public synchronized void append(byte[] data) { // Calculate size of copy int length = data.length; int remaining = pending.length - pending_length; if (remaining < length) length = remaining; // Append data System.arraycopy(data, 0, pending, pending_length, length); pending_length += length; } /** * Commits the pending contents to the clipboard, notifying any threads * waiting for clipboard updates. */ public synchronized void commit() { // Commit contents mimetype = pending_mimetype; contents = new byte[pending_length]; System.arraycopy(pending, 0, contents, 0, pending_length); // Notify of update last_update = System.currentTimeMillis(); this.notifyAll(); } /** * Wait up to the given timeout for new clipboard data. * * @param timeout The amount of time to wait, in milliseconds. * @return true if the contents were updated within the timeframe given, * false otherwise. */ public synchronized boolean waitForContents(int timeout) { // Wait for new contents if it's been a while if (System.currentTimeMillis() - last_update > timeout) { try { this.wait(timeout); return true; } catch (InterruptedException e) { /* ignore */ } } return false; } } ././@LongLink0100644 0000000 0000000 00000000153 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleClassLoader.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleClassLoade0100664 0001751 0001751 00000014370 12612226251 035311 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic; import java.io.File; import java.io.FilenameFilter; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.Collection; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.environment.LocalEnvironment; import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties; /** * A ClassLoader implementation which finds classes within a configurable * directory. This directory is set within guacamole.properties. This class * is deprecated in favor of DirectoryClassLoader, which is automatically * configured based on the presence/absence of GUACAMOLE_HOME/lib. * * @author Michael Jumper */ @Deprecated public class GuacamoleClassLoader extends ClassLoader { /** * Class loader which will load classes from the classpath specified * in guacamole.properties. */ private URLClassLoader classLoader = null; /** * Any exception that occurs while the class loader is being instantiated. */ private static GuacamoleException exception = null; /** * Singleton instance of the GuacamoleClassLoader. */ private static GuacamoleClassLoader instance = null; static { try { // Attempt to create singleton classloader which loads classes from // all .jar's in the lib directory defined in guacamole.properties instance = AccessController.doPrivileged(new PrivilegedExceptionAction() { @Override public GuacamoleClassLoader run() throws GuacamoleException { // TODONT: This should be injected, but GuacamoleClassLoader will be removed soon. Environment environment = new LocalEnvironment(); return new GuacamoleClassLoader( environment.getProperty(BasicGuacamoleProperties.LIB_DIRECTORY) ); } }); } catch (PrivilegedActionException e) { // On error, record exception exception = (GuacamoleException) e.getException(); } } /** * Creates a new GuacamoleClassLoader which reads classes from the given * directory. * * @param libDirectory The directory to load classes from. * @throws GuacamoleException If the file given is not a director, or if * an error occurs while constructing the URL * for the backing classloader. */ private GuacamoleClassLoader(File libDirectory) throws GuacamoleException { // If no directory provided, just direct requests to parent classloader if (libDirectory == null) return; // Validate directory is indeed a directory if (!libDirectory.isDirectory()) throw new GuacamoleException(libDirectory + " is not a directory."); // Get list of URLs for all .jar's in the lib directory Collection jarURLs = new ArrayList(); File[] files = libDirectory.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { // If it ends with .jar, accept the file return name.endsWith(".jar"); } }); // Verify directory was successfully read if (files == null) throw new GuacamoleException("Unable to read contents of directory " + libDirectory); // Add the URL for each .jar to the jar URL list for (File file : files) { try { jarURLs.add(file.toURI().toURL()); } catch (MalformedURLException e) { throw new GuacamoleException(e); } } // Set delegate classloader to new URLClassLoader which loads from the // .jars found above. URL[] urls = new URL[jarURLs.size()]; classLoader = new URLClassLoader( jarURLs.toArray(urls), getClass().getClassLoader() ); } /** * Returns an instance of a GuacamoleClassLoader which finds classes * within the directory configured in guacamole.properties. * * @return An instance of a GuacamoleClassLoader. * @throws GuacamoleException If no instance could be returned due to an * error. */ public static GuacamoleClassLoader getInstance() throws GuacamoleException { // If instance could not be created, rethrow original exception if (exception != null) throw exception; return instance; } @Override public Class findClass(String name) throws ClassNotFoundException { // If no classloader, use default loader if (classLoader == null) return Class.forName(name); // Otherwise, delegate return classLoader.loadClass(name); } } ././@LongLink0100644 0000000 0000000 00000000162 12645510255 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicServletContextListener.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicServletContext0100664 0001751 0001751 00000007067 12645510255 035427 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Stage; import com.google.inject.servlet.GuiceServletContextListener; import javax.servlet.ServletContextEvent; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.environment.LocalEnvironment; import org.glyptodon.guacamole.net.basic.extension.ExtensionModule; import org.glyptodon.guacamole.net.basic.log.LogModule; import org.glyptodon.guacamole.net.basic.rest.RESTServiceModule; import org.glyptodon.guacamole.net.basic.rest.auth.BasicTokenSessionMap; import org.glyptodon.guacamole.net.basic.rest.auth.TokenSessionMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A ServletContextListener to listen for initialization of the servlet context * in order to set up dependency injection. * * @author James Muehlner */ public class BasicServletContextListener extends GuiceServletContextListener { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(BasicServletContextListener.class); /** * The Guacamole server environment. */ private Environment environment; /** * Singleton instance of a TokenSessionMap. */ private TokenSessionMap sessionMap; @Override public void contextInitialized(ServletContextEvent servletContextEvent) { try { environment = new LocalEnvironment(); sessionMap = new BasicTokenSessionMap(environment); } catch (GuacamoleException e) { logger.error("Unable to read guacamole.properties: {}", e.getMessage()); logger.debug("Error reading guacamole.properties.", e); throw new RuntimeException(e); } super.contextInitialized(servletContextEvent); } @Override protected Injector getInjector() { return Guice.createInjector(Stage.PRODUCTION, new EnvironmentModule(environment), new LogModule(environment), new ExtensionModule(environment), new RESTServiceModule(sessionMap), new TunnelModule() ); } @Override public void contextDestroyed(ServletContextEvent servletContextEvent) { super.contextDestroyed(servletContextEvent); // Shutdown TokenSessionMap if (sessionMap != null) sessionMap.shutdown(); } } ././@LongLink0100644 0000000 0000000 00000000167 12612226251 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/WebSocketTunnelRequest.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/WebSocket0100664 0001751 0001751 00000004565 12612226251 035342 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.websocket; import java.util.List; import java.util.Map; import javax.websocket.server.HandshakeRequest; import org.glyptodon.guacamole.net.basic.TunnelRequest; /** * WebSocket-specific implementation of TunnelRequest. * * @author Michael Jumper */ public class WebSocketTunnelRequest extends TunnelRequest { /** * All parameters passed via HTTP to the WebSocket handshake. */ private final Map> handshakeParameters; /** * Creates a TunnelRequest implementation which delegates parameter and * session retrieval to the given HandshakeRequest. * * @param request The HandshakeRequest to wrap. */ public WebSocketTunnelRequest(HandshakeRequest request) { this.handshakeParameters = request.getParameterMap(); } @Override public String getParameter(String name) { // Pull list of values, if present List values = getParameterValues(name); if (values == null || values.isEmpty()) return null; // Return first parameter value arbitrarily return values.get(0); } @Override public List getParameterValues(String name) { return handshakeParameters.get(name); } } ././@LongLink0100644 0000000 0000000 00000000166 12632431174 011641 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/WebSocketTunnelModule.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/WebSocket0100664 0001751 0001751 00000007213 12632431174 035337 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.websocket; import com.google.inject.Provider; import com.google.inject.servlet.ServletModule; import java.util.Arrays; import javax.websocket.DeploymentException; import javax.websocket.server.ServerContainer; import javax.websocket.server.ServerEndpointConfig; import org.glyptodon.guacamole.net.basic.TunnelLoader; import org.glyptodon.guacamole.net.basic.TunnelRequestService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Loads the JSR-356 WebSocket tunnel implementation. * * @author Michael Jumper */ public class WebSocketTunnelModule extends ServletModule implements TunnelLoader { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(WebSocketTunnelModule.class); @Override public boolean isSupported() { try { // Attempt to find WebSocket servlet Class.forName("javax.websocket.Endpoint"); // Support found return true; } // If no such servlet class, this particular WebSocket support // is not present catch (ClassNotFoundException e) {} catch (NoClassDefFoundError e) {} // Support not found return false; } @Override public void configureServlets() { logger.info("Loading JSR-356 WebSocket support..."); // Get container ServerContainer container = (ServerContainer) getServletContext().getAttribute("javax.websocket.server.ServerContainer"); if (container == null) { logger.warn("ServerContainer attribute required by JSR-356 is missing. Cannot load JSR-356 WebSocket support."); return; } Provider tunnelRequestServiceProvider = getProvider(TunnelRequestService.class); // Build configuration for WebSocket tunnel ServerEndpointConfig config = ServerEndpointConfig.Builder.create(BasicGuacamoleWebSocketTunnelEndpoint.class, "/websocket-tunnel") .configurator(new BasicGuacamoleWebSocketTunnelEndpoint.Configurator(tunnelRequestServiceProvider)) .subprotocols(Arrays.asList(new String[]{"guacamole"})) .build(); try { // Add configuration to container container.addEndpoint(config); } catch (DeploymentException e) { logger.error("Unable to deploy WebSocket tunnel.", e); } } } ././@LongLink0100644 0000000 0000000 00000000214 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/BasicGuacamoleWebSocketTunnelServlet.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/Ba0100664 0001751 0001751 00000003723 12612226251 035221 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.websocket.jetty9; import com.google.inject.Inject; import com.google.inject.Singleton; import org.eclipse.jetty.websocket.servlet.WebSocketServlet; import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; import org.glyptodon.guacamole.net.basic.TunnelRequestService; /** * A WebSocketServlet partial re-implementation of GuacamoleTunnelServlet. * * @author Michael Jumper */ @Singleton public class BasicGuacamoleWebSocketTunnelServlet extends WebSocketServlet { /** * Service for handling tunnel requests. */ @Inject private TunnelRequestService tunnelRequestService; @Override public void configure(WebSocketServletFactory factory) { // Register WebSocket implementation factory.setCreator(new BasicGuacamoleWebSocketCreator(tunnelRequestService)); } } ././@LongLink0100644 0000000 0000000 00000000210 12645510255 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/GuacamoleWebSocketTunnelListener.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/Gu0100664 0001751 0001751 00000020323 12645510255 035253 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.websocket.jetty9; import java.io.IOException; import org.eclipse.jetty.websocket.api.CloseStatus; import org.eclipse.jetty.websocket.api.RemoteEndpoint; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.WebSocketListener; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleConnectionClosedException; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.io.GuacamoleReader; import org.glyptodon.guacamole.io.GuacamoleWriter; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.protocol.GuacamoleStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * WebSocket listener implementation which provides a Guacamole tunnel * * @author Michael Jumper */ public abstract class GuacamoleWebSocketTunnelListener implements WebSocketListener { /** * The default, minimum buffer size for instructions. */ private static final int BUFFER_SIZE = 8192; /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(BasicGuacamoleWebSocketTunnelServlet.class); /** * The underlying GuacamoleTunnel. WebSocket reads/writes will be handled * as reads/writes to this tunnel. */ private GuacamoleTunnel tunnel; /** * Sends the given status on the given WebSocket connection and closes the * connection. * * @param session The outbound WebSocket connection to close. * @param guac_status The status to send. */ private void closeConnection(Session session, GuacamoleStatus guac_status) { try { int code = guac_status.getWebSocketCode(); String message = Integer.toString(guac_status.getGuacamoleStatusCode()); session.close(new CloseStatus(code, message)); } catch (IOException e) { logger.debug("Unable to close WebSocket connection.", e); } } /** * Returns a new tunnel for the given session. How this tunnel is created * or retrieved is implementation-dependent. * * @param session The session associated with the active WebSocket * connection. * @return A connected tunnel, or null if no such tunnel exists. * @throws GuacamoleException If an error occurs while retrieving the * tunnel, or if access to the tunnel is denied. */ protected abstract GuacamoleTunnel createTunnel(Session session) throws GuacamoleException; @Override public void onWebSocketConnect(final Session session) { try { // Get tunnel tunnel = createTunnel(session); if (tunnel == null) { closeConnection(session, GuacamoleStatus.RESOURCE_NOT_FOUND); return; } } catch (GuacamoleException e) { logger.error("Creation of WebSocket tunnel to guacd failed: {}", e.getMessage()); logger.debug("Error connecting WebSocket tunnel.", e); closeConnection(session, e.getStatus()); return; } // Prepare read transfer thread Thread readThread = new Thread() { /** * Remote (client) side of this connection */ private final RemoteEndpoint remote = session.getRemote(); @Override public void run() { StringBuilder buffer = new StringBuilder(BUFFER_SIZE); GuacamoleReader reader = tunnel.acquireReader(); char[] readMessage; try { try { // Attempt to read while ((readMessage = reader.read()) != null) { // Buffer message buffer.append(readMessage); // Flush if we expect to wait or buffer is getting full if (!reader.available() || buffer.length() >= BUFFER_SIZE) { remote.sendString(buffer.toString()); buffer.setLength(0); } } // No more data closeConnection(session, GuacamoleStatus.SUCCESS); } // Catch any thrown guacamole exception and attempt // to pass within the WebSocket connection, logging // each error appropriately. catch (GuacamoleClientException e) { logger.info("WebSocket connection terminated: {}", e.getMessage()); logger.debug("WebSocket connection terminated due to client error.", e); closeConnection(session, e.getStatus()); } catch (GuacamoleConnectionClosedException e) { logger.debug("Connection to guacd closed.", e); closeConnection(session, GuacamoleStatus.SUCCESS); } catch (GuacamoleException e) { logger.error("Connection to guacd terminated abnormally: {}", e.getMessage()); logger.debug("Internal error during connection to guacd.", e); closeConnection(session, e.getStatus()); } } catch (IOException e) { logger.debug("I/O error prevents further reads.", e); } } }; readThread.start(); } @Override public void onWebSocketText(String message) { // Ignore inbound messages if there is no associated tunnel if (tunnel == null) return; GuacamoleWriter writer = tunnel.acquireWriter(); try { // Write received message writer.write(message.toCharArray()); } catch (GuacamoleConnectionClosedException e) { logger.debug("Connection to guacd closed.", e); } catch (GuacamoleException e) { logger.debug("WebSocket tunnel write failed.", e); } tunnel.releaseWriter(); } @Override public void onWebSocketBinary(byte[] payload, int offset, int length) { throw new UnsupportedOperationException("Binary WebSocket messages are not supported."); } @Override public void onWebSocketError(Throwable t) { logger.debug("WebSocket tunnel closing due to error.", t); try { if (tunnel != null) tunnel.close(); } catch (GuacamoleException e) { logger.debug("Unable to close connection to guacd.", e); } } @Override public void onWebSocketClose(int statusCode, String reason) { try { if (tunnel != null) tunnel.close(); } catch (GuacamoleException e) { logger.debug("Unable to close connection to guacd.", e); } } } ././@LongLink0100644 0000000 0000000 00000000206 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/BasicGuacamoleWebSocketCreator.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/Ba0100664 0001751 0001751 00000005234 12612226251 035220 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.websocket.jetty9; import org.eclipse.jetty.websocket.api.UpgradeRequest; import org.eclipse.jetty.websocket.api.UpgradeResponse; import org.eclipse.jetty.websocket.servlet.WebSocketCreator; import org.glyptodon.guacamole.net.basic.TunnelRequestService; /** * WebSocketCreator which selects the appropriate WebSocketListener * implementation if the "guacamole" subprotocol is in use. * * @author Michael Jumper */ public class BasicGuacamoleWebSocketCreator implements WebSocketCreator { /** * Service for handling tunnel requests. */ private final TunnelRequestService tunnelRequestService; /** * Creates a new WebSocketCreator which uses the given TunnelRequestService * to create new GuacamoleTunnels for inbound requests. * * @param tunnelRequestService The service to use for inbound tunnel * requests. */ public BasicGuacamoleWebSocketCreator(TunnelRequestService tunnelRequestService) { this.tunnelRequestService = tunnelRequestService; } @Override public Object createWebSocket(UpgradeRequest request, UpgradeResponse response) { // Validate and use "guacamole" subprotocol for (String subprotocol : request.getSubProtocols()) { if ("guacamole".equals(subprotocol)) { response.setAcceptedSubProtocol(subprotocol); return new BasicGuacamoleWebSocketTunnelListener(tunnelRequestService); } } // Invalid protocol return null; } } ././@LongLink0100644 0000000 0000000 00000000176 12612226251 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/WebSocketTunnelRequest.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/We0100664 0001751 0001751 00000005002 12612226251 035242 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.websocket.jetty9; import java.util.Arrays; import java.util.List; import java.util.Map; import org.eclipse.jetty.websocket.api.UpgradeRequest; import org.glyptodon.guacamole.net.basic.TunnelRequest; /** * Jetty 9 WebSocket-specific implementation of TunnelRequest. * * @author Michael Jumper */ public class WebSocketTunnelRequest extends TunnelRequest { /** * All parameters passed via HTTP to the WebSocket handshake. */ private final Map handshakeParameters; /** * Creates a TunnelRequest implementation which delegates parameter and * session retrieval to the given UpgradeRequest. * * @param request The UpgradeRequest to wrap. */ public WebSocketTunnelRequest(UpgradeRequest request) { this.handshakeParameters = request.getParameterMap(); } @Override public String getParameter(String name) { // Pull list of values, if present List values = getParameterValues(name); if (values == null || values.isEmpty()) return null; // Return first parameter value arbitrarily return values.get(0); } @Override public List getParameterValues(String name) { String[] values = handshakeParameters.get(name); if (values == null) return null; return Arrays.asList(values); } } ././@LongLink0100644 0000000 0000000 00000000175 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/WebSocketTunnelModule.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/We0100664 0001751 0001751 00000004557 12612226251 035260 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.websocket.jetty9; import com.google.inject.servlet.ServletModule; import org.glyptodon.guacamole.net.basic.TunnelLoader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Loads the Jetty 9 WebSocket tunnel implementation. * * @author Michael Jumper */ public class WebSocketTunnelModule extends ServletModule implements TunnelLoader { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(WebSocketTunnelModule.class); @Override public boolean isSupported() { try { // Attempt to find WebSocket servlet Class.forName("org.glyptodon.guacamole.net.basic.websocket.jetty9.BasicGuacamoleWebSocketTunnelServlet"); // Support found return true; } // If no such servlet class, this particular WebSocket support // is not present catch (ClassNotFoundException e) {} catch (NoClassDefFoundError e) {} // Support not found return false; } @Override public void configureServlets() { logger.info("Loading Jetty 9 WebSocket support..."); serve("/websocket-tunnel").with(BasicGuacamoleWebSocketTunnelServlet.class); } } ././@LongLink0100644 0000000 0000000 00000000215 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/BasicGuacamoleWebSocketTunnelListener.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/Ba0100664 0001751 0001751 00000004567 12612226251 035230 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.websocket.jetty9; import org.eclipse.jetty.websocket.api.Session; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.basic.TunnelRequestService; /** * WebSocket listener implementation which properly parses connection IDs * included in the connection request. * * @author Michael Jumper */ public class BasicGuacamoleWebSocketTunnelListener extends GuacamoleWebSocketTunnelListener { /** * Service for handling tunnel requests. */ private final TunnelRequestService tunnelRequestService; /** * Creates a new WebSocketListener which uses the given TunnelRequestService * to create new GuacamoleTunnels for inbound requests. * * @param tunnelRequestService The service to use for inbound tunnel * requests. */ public BasicGuacamoleWebSocketTunnelListener(TunnelRequestService tunnelRequestService) { this.tunnelRequestService = tunnelRequestService; } @Override protected GuacamoleTunnel createTunnel(Session session) throws GuacamoleException { return tunnelRequestService.createTunnel(new WebSocketTunnelRequest(session.getUpgradeRequest())); } } ././@LongLink0100644 0000000 0000000 00000000164 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/package-info.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty9/pa0100664 0001751 0001751 00000002475 12612226251 035302 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Jetty 9 WebSocket tunnel implementation. The classes here require at least * Jetty 9, prior to Jetty 9.1 (when support for JSR 356 was implemented). */ package org.glyptodon.guacamole.net.basic.websocket.jetty9; ././@LongLink0100644 0000000 0000000 00000000155 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/package-info.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/package-i0100664 0001751 0001751 00000002430 12612226251 035262 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Standard WebSocket tunnel implementation. The classes here require a recent * servlet container that supports JSR 356. */ package org.glyptodon.guacamole.net.basic.websocket; ././@LongLink0100644 0000000 0000000 00000000206 12645510255 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/BasicGuacamoleWebSocketTunnelEndpoint.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/BasicGuac0100664 0001751 0001751 00000011534 12645510255 035275 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.websocket; import com.google.inject.Provider; import java.util.Map; import javax.websocket.EndpointConfig; import javax.websocket.HandshakeResponse; import javax.websocket.Session; import javax.websocket.server.HandshakeRequest; import javax.websocket.server.ServerEndpointConfig; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.basic.TunnelRequest; import org.glyptodon.guacamole.net.basic.TunnelRequestService; import org.glyptodon.guacamole.websocket.GuacamoleWebSocketTunnelEndpoint; /** * Tunnel implementation which uses WebSocket as a tunnel backend, rather than * HTTP, properly parsing connection IDs included in the connection request. */ public class BasicGuacamoleWebSocketTunnelEndpoint extends GuacamoleWebSocketTunnelEndpoint { /** * Unique string which shall be used to store the TunnelRequest * associated with a WebSocket connection. */ private static final String TUNNEL_REQUEST_PROPERTY = "WS_GUAC_TUNNEL_REQUEST"; /** * Unique string which shall be used to store the TunnelRequestService to * be used for processing TunnelRequests. */ private static final String TUNNEL_REQUEST_SERVICE_PROPERTY = "WS_GUAC_TUNNEL_REQUEST_SERVICE"; /** * Configurator implementation which stores the requested GuacamoleTunnel * within the user properties. The GuacamoleTunnel will be later retrieved * during the connection process. */ public static class Configurator extends ServerEndpointConfig.Configurator { /** * Provider which provides instances of a service for handling * tunnel requests. */ private final Provider tunnelRequestServiceProvider; /** * Creates a new Configurator which uses the given tunnel request * service provider to retrieve the necessary service to handle new * connections requests. * * @param tunnelRequestServiceProvider * The tunnel request service provider to use for all new * connections. */ public Configurator(Provider tunnelRequestServiceProvider) { this.tunnelRequestServiceProvider = tunnelRequestServiceProvider; } @Override public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) { super.modifyHandshake(config, request, response); // Store tunnel request and tunnel request service for retrieval // upon WebSocket open Map userProperties = config.getUserProperties(); userProperties.clear(); userProperties.put(TUNNEL_REQUEST_PROPERTY, new WebSocketTunnelRequest(request)); userProperties.put(TUNNEL_REQUEST_SERVICE_PROPERTY, tunnelRequestServiceProvider.get()); } } @Override protected GuacamoleTunnel createTunnel(Session session, EndpointConfig config) throws GuacamoleException { Map userProperties = config.getUserProperties(); // Get original tunnel request TunnelRequest tunnelRequest = (TunnelRequest) userProperties.get(TUNNEL_REQUEST_PROPERTY); if (tunnelRequest == null) return null; // Get tunnel request service TunnelRequestService tunnelRequestService = (TunnelRequestService) userProperties.get(TUNNEL_REQUEST_SERVICE_PROPERTY); if (tunnelRequestService == null) return null; // Create and return tunnel return tunnelRequestService.createTunnel(tunnelRequest); } } ././@LongLink0100644 0000000 0000000 00000000207 12645510255 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/GuacamoleWebSocketTunnelServlet.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/Gu0100664 0001751 0001751 00000023435 12645510255 035321 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.websocket.tomcat; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.util.List; import javax.servlet.http.HttpServletRequest; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.io.GuacamoleReader; import org.glyptodon.guacamole.io.GuacamoleWriter; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.apache.catalina.websocket.StreamInbound; import org.apache.catalina.websocket.WebSocketServlet; import org.apache.catalina.websocket.WsOutbound; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleConnectionClosedException; import org.glyptodon.guacamole.net.basic.HTTPTunnelRequest; import org.glyptodon.guacamole.net.basic.TunnelRequest; import org.glyptodon.guacamole.protocol.GuacamoleStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A WebSocketServlet partial re-implementation of GuacamoleTunnelServlet. * * @author Michael Jumper */ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet { /** * The default, minimum buffer size for instructions. */ private static final int BUFFER_SIZE = 8192; /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(GuacamoleWebSocketTunnelServlet.class); /** * Sends the given status on the given WebSocket connection and closes the * connection. * * @param outbound The outbound WebSocket connection to close. * @param guac_status The status to send. */ public void closeConnection(WsOutbound outbound, GuacamoleStatus guac_status) { try { byte[] message = Integer.toString(guac_status.getGuacamoleStatusCode()).getBytes("UTF-8"); outbound.close(guac_status.getWebSocketCode(), ByteBuffer.wrap(message)); } catch (IOException e) { logger.debug("Unable to close WebSocket tunnel.", e); } } @Override protected String selectSubProtocol(List subProtocols) { // Search for expected protocol for (String protocol : subProtocols) if ("guacamole".equals(protocol)) return "guacamole"; // Otherwise, fail return null; } @Override public StreamInbound createWebSocketInbound(String protocol, HttpServletRequest request) { final TunnelRequest tunnelRequest = new HTTPTunnelRequest(request); // Return new WebSocket which communicates through tunnel return new StreamInbound() { /** * The GuacamoleTunnel associated with the connected WebSocket. If * the WebSocket has not yet been connected, this will be null. */ private GuacamoleTunnel tunnel = null; @Override protected void onTextData(Reader reader) throws IOException { // Ignore inbound messages if there is no associated tunnel if (tunnel == null) return; GuacamoleWriter writer = tunnel.acquireWriter(); // Write all available data try { char[] buffer = new char[BUFFER_SIZE]; int num_read; while ((num_read = reader.read(buffer)) > 0) writer.write(buffer, 0, num_read); } catch (GuacamoleConnectionClosedException e) { logger.debug("Connection to guacd closed.", e); } catch (GuacamoleException e) { logger.debug("WebSocket tunnel write failed.", e); } tunnel.releaseWriter(); } @Override public void onOpen(final WsOutbound outbound) { try { tunnel = doConnect(tunnelRequest); } catch (GuacamoleException e) { logger.error("Creation of WebSocket tunnel to guacd failed: {}", e.getMessage()); logger.debug("Error connecting WebSocket tunnel.", e); closeConnection(outbound, e.getStatus()); return; } // Do not start connection if tunnel does not exist if (tunnel == null) { closeConnection(outbound, GuacamoleStatus.RESOURCE_NOT_FOUND); return; } Thread readThread = new Thread() { @Override public void run() { StringBuilder buffer = new StringBuilder(BUFFER_SIZE); GuacamoleReader reader = tunnel.acquireReader(); char[] readMessage; try { try { // Attempt to read while ((readMessage = reader.read()) != null) { // Buffer message buffer.append(readMessage); // Flush if we expect to wait or buffer is getting full if (!reader.available() || buffer.length() >= BUFFER_SIZE) { outbound.writeTextMessage(CharBuffer.wrap(buffer)); buffer.setLength(0); } } // No more data closeConnection(outbound, GuacamoleStatus.SUCCESS); } // Catch any thrown guacamole exception and attempt // to pass within the WebSocket connection, logging // each error appropriately. catch (GuacamoleClientException e) { logger.info("WebSocket connection terminated: {}", e.getMessage()); logger.debug("WebSocket connection terminated due to client error.", e); closeConnection(outbound, e.getStatus()); } catch (GuacamoleConnectionClosedException e) { logger.debug("Connection to guacd closed.", e); closeConnection(outbound, GuacamoleStatus.SUCCESS); } catch (GuacamoleException e) { logger.error("Connection to guacd terminated abnormally: {}", e.getMessage()); logger.debug("Internal error during connection to guacd.", e); closeConnection(outbound, e.getStatus()); } } catch (IOException e) { logger.debug("I/O error prevents further reads.", e); } } }; readThread.start(); } @Override public void onClose(int i) { try { if (tunnel != null) tunnel.close(); } catch (GuacamoleException e) { logger.debug("Unable to close connection to guacd.", e); } } @Override protected void onBinaryData(InputStream in) throws IOException { throw new UnsupportedOperationException("Not supported yet."); } }; } /** * Called whenever the JavaScript Guacamole client makes a connection * request. It it up to the implementor of this function to define what * conditions must be met for a tunnel to be configured and returned as a * result of this connection request (whether some sort of credentials must * be specified, for example). * * @param request * The TunnelRequest associated with the connection request received. * Any parameters specified along with the connection request can be * read from this object. * * @return * A newly constructed GuacamoleTunnel if successful, null otherwise. * * @throws GuacamoleException * If an error occurs while constructing the GuacamoleTunnel, or if the * conditions required for connection are not met. */ protected abstract GuacamoleTunnel doConnect(TunnelRequest request) throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000214 12645510255 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/BasicGuacamoleWebSocketTunnelServlet.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/Ba0100664 0001751 0001751 00000004023 12645510255 035260 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.websocket.tomcat; import com.google.inject.Inject; import com.google.inject.Singleton; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.basic.TunnelRequestService; import org.glyptodon.guacamole.net.basic.TunnelRequest; /** * Tunnel servlet implementation which uses WebSocket as a tunnel backend, * rather than HTTP, properly parsing connection IDs included in the connection * request. */ @Singleton public class BasicGuacamoleWebSocketTunnelServlet extends GuacamoleWebSocketTunnelServlet { /** * Service for handling tunnel requests. */ @Inject private TunnelRequestService tunnelRequestService; @Override protected GuacamoleTunnel doConnect(TunnelRequest request) throws GuacamoleException { return tunnelRequestService.createTunnel(request); }; } ././@LongLink0100644 0000000 0000000 00000000175 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/WebSocketTunnelModule.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/We0100664 0001751 0001751 00000004560 12612226251 035311 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.websocket.tomcat; import com.google.inject.servlet.ServletModule; import org.glyptodon.guacamole.net.basic.TunnelLoader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Loads the Jetty 9 WebSocket tunnel implementation. * * @author Michael Jumper */ public class WebSocketTunnelModule extends ServletModule implements TunnelLoader { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(WebSocketTunnelModule.class); @Override public boolean isSupported() { try { // Attempt to find WebSocket servlet Class.forName("org.glyptodon.guacamole.net.basic.websocket.tomcat.BasicGuacamoleWebSocketTunnelServlet"); // Support found return true; } // If no such servlet class, this particular WebSocket support // is not present catch (ClassNotFoundException e) {} catch (NoClassDefFoundError e) {} // Support not found return false; } @Override public void configureServlets() { logger.info("Loading Tomcat 7 WebSocket support..."); serve("/websocket-tunnel").with(BasicGuacamoleWebSocketTunnelServlet.class); } } ././@LongLink0100644 0000000 0000000 00000000164 12471273273 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/package-info.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/tomcat/pa0100664 0001751 0001751 00000002522 12471273273 035343 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Tomcat WebSocket tunnel implementation. The classes here require at least * Tomcat 7.0, and may change significantly as there is no common WebSocket * API for Java yet. */ package org.glyptodon.guacamole.net.basic.websocket.tomcat; ././@LongLink0100644 0000000 0000000 00000000207 12645510255 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/GuacamoleWebSocketTunnelServlet.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/Gu0100664 0001751 0001751 00000021564 12645510255 035262 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.websocket.jetty8; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.io.GuacamoleReader; import org.glyptodon.guacamole.io.GuacamoleWriter; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.eclipse.jetty.websocket.WebSocket; import org.eclipse.jetty.websocket.WebSocket.Connection; import org.eclipse.jetty.websocket.WebSocketServlet; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleConnectionClosedException; import org.glyptodon.guacamole.net.basic.HTTPTunnelRequest; import org.glyptodon.guacamole.net.basic.TunnelRequest; import org.glyptodon.guacamole.protocol.GuacamoleStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A WebSocketServlet partial re-implementation of GuacamoleTunnelServlet. * * @author Michael Jumper */ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(GuacamoleWebSocketTunnelServlet.class); /** * The default, minimum buffer size for instructions. */ private static final int BUFFER_SIZE = 8192; /** * Sends the given status on the given WebSocket connection and closes the * connection. * * @param connection The WebSocket connection to close. * @param guac_status The status to send. */ public static void closeConnection(Connection connection, GuacamoleStatus guac_status) { connection.close(guac_status.getWebSocketCode(), Integer.toString(guac_status.getGuacamoleStatusCode())); } @Override public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) { final TunnelRequest tunnelRequest = new HTTPTunnelRequest(request); // Return new WebSocket which communicates through tunnel return new WebSocket.OnTextMessage() { /** * The GuacamoleTunnel associated with the connected WebSocket. If * the WebSocket has not yet been connected, this will be null. */ private GuacamoleTunnel tunnel = null; @Override public void onMessage(String string) { // Ignore inbound messages if there is no associated tunnel if (tunnel == null) return; GuacamoleWriter writer = tunnel.acquireWriter(); // Write message received try { writer.write(string.toCharArray()); } catch (GuacamoleConnectionClosedException e) { logger.debug("Connection to guacd closed.", e); } catch (GuacamoleException e) { logger.debug("WebSocket tunnel write failed.", e); } tunnel.releaseWriter(); } @Override public void onOpen(final Connection connection) { try { tunnel = doConnect(tunnelRequest); } catch (GuacamoleException e) { logger.error("Creation of WebSocket tunnel to guacd failed: {}", e.getMessage()); logger.debug("Error connecting WebSocket tunnel.", e); closeConnection(connection, e.getStatus()); return; } // Do not start connection if tunnel does not exist if (tunnel == null) { closeConnection(connection, GuacamoleStatus.RESOURCE_NOT_FOUND); return; } Thread readThread = new Thread() { @Override public void run() { StringBuilder buffer = new StringBuilder(BUFFER_SIZE); GuacamoleReader reader = tunnel.acquireReader(); char[] readMessage; try { try { // Attempt to read while ((readMessage = reader.read()) != null) { // Buffer message buffer.append(readMessage); // Flush if we expect to wait or buffer is getting full if (!reader.available() || buffer.length() >= BUFFER_SIZE) { connection.sendMessage(buffer.toString()); buffer.setLength(0); } } // No more data closeConnection(connection, GuacamoleStatus.SUCCESS); } // Catch any thrown guacamole exception and attempt // to pass within the WebSocket connection, logging // each error appropriately. catch (GuacamoleClientException e) { logger.info("WebSocket connection terminated: {}", e.getMessage()); logger.debug("WebSocket connection terminated due to client error.", e); closeConnection(connection, e.getStatus()); } catch (GuacamoleConnectionClosedException e) { logger.debug("Connection to guacd closed.", e); closeConnection(connection, GuacamoleStatus.SUCCESS); } catch (GuacamoleException e) { logger.error("Connection to guacd terminated abnormally: {}", e.getMessage()); logger.debug("Internal error during connection to guacd.", e); closeConnection(connection, e.getStatus()); } } catch (IOException e) { logger.debug("WebSocket tunnel read failed due to I/O error.", e); } } }; readThread.start(); } @Override public void onClose(int i, String string) { try { if (tunnel != null) tunnel.close(); } catch (GuacamoleException e) { logger.debug("Unable to close connection to guacd.", e); } } }; } /** * Called whenever the JavaScript Guacamole client makes a connection * request. It it up to the implementor of this function to define what * conditions must be met for a tunnel to be configured and returned as a * result of this connection request (whether some sort of credentials must * be specified, for example). * * @param request * The TunnelRequest associated with the connection request received. * Any parameters specified along with the connection request can be * read from this object. * * @return * A newly constructed GuacamoleTunnel if successful, null otherwise. * * @throws GuacamoleException * If an error occurs while constructing the GuacamoleTunnel, or if the * conditions required for connection are not met. */ protected abstract GuacamoleTunnel doConnect(TunnelRequest request) throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000214 12645510255 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/BasicGuacamoleWebSocketTunnelServlet.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/Ba0100664 0001751 0001751 00000004022 12645510255 035217 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.websocket.jetty8; import com.google.inject.Inject; import com.google.inject.Singleton; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.basic.TunnelRequestService; import org.glyptodon.guacamole.net.basic.TunnelRequest; /** * Tunnel servlet implementation which uses WebSocket as a tunnel backend, * rather than HTTP, properly parsing connection IDs included in the connection * request. */ @Singleton public class BasicGuacamoleWebSocketTunnelServlet extends GuacamoleWebSocketTunnelServlet { /** * Service for handling tunnel requests. */ @Inject private TunnelRequestService tunnelRequestService; @Override protected GuacamoleTunnel doConnect(TunnelRequest request) throws GuacamoleException { return tunnelRequestService.createTunnel(request); } } ././@LongLink0100644 0000000 0000000 00000000175 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/WebSocketTunnelModule.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/We0100664 0001751 0001751 00000004557 12612226251 035257 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.basic.websocket.jetty8; import com.google.inject.servlet.ServletModule; import org.glyptodon.guacamole.net.basic.TunnelLoader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Loads the Jetty 8 WebSocket tunnel implementation. * * @author Michael Jumper */ public class WebSocketTunnelModule extends ServletModule implements TunnelLoader { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(WebSocketTunnelModule.class); @Override public boolean isSupported() { try { // Attempt to find WebSocket servlet Class.forName("org.glyptodon.guacamole.net.basic.websocket.jetty8.BasicGuacamoleWebSocketTunnelServlet"); // Support found return true; } // If no such servlet class, this particular WebSocket support // is not present catch (ClassNotFoundException e) {} catch (NoClassDefFoundError e) {} // Support not found return false; } @Override public void configureServlets() { logger.info("Loading Jetty 8 WebSocket support..."); serve("/websocket-tunnel").with(BasicGuacamoleWebSocketTunnelServlet.class); } } ././@LongLink0100644 0000000 0000000 00000000164 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/package-info.javaguacamole-client-0.9.9/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/websocket/jetty8/pa0100664 0001751 0001751 00000002362 12612226251 035274 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Jetty 8 WebSocket tunnel implementation. The classes here require Jetty 8. */ package org.glyptodon.guacamole.net.basic.websocket.jetty8; ././@LongLink0100644 0000000 0000000 00000000170 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/BasicFileAuthenticationProvider.javaguacamole-client-0.9.9/guacamole/src/main/java/net/sourceforge/guacamole/net/basic/BasicFileAuthenti0100664 0001751 0001751 00000017642 12612226251 035314 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package net.sourceforge.guacamole.net.basic; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Map; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.environment.LocalEnvironment; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.simple.SimpleAuthenticationProvider; import org.glyptodon.guacamole.net.basic.auth.Authorization; import org.glyptodon.guacamole.net.basic.auth.UserMapping; import org.glyptodon.guacamole.xml.DocumentHandler; import org.glyptodon.guacamole.net.basic.xml.usermapping.UserMappingTagHandler; import org.glyptodon.guacamole.properties.FileGuacamoleProperty; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLReaderFactory; /** * Authenticates users against a static list of username/password pairs. * Each username/password may be associated with multiple configurations. * This list is stored in an XML file which is reread if modified. * * @author Michael Jumper, Michal Kotas */ public class BasicFileAuthenticationProvider extends SimpleAuthenticationProvider { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(BasicFileAuthenticationProvider.class); /** * The time the user mapping file was last modified. If the file has never * been read, and thus no modification time exists, this will be * Long.MIN_VALUE. */ private long lastModified = Long.MIN_VALUE; /** * The parsed UserMapping read when the user mapping file was last parsed. */ private UserMapping cachedUserMapping; /** * Guacamole server environment. */ private final Environment environment; /** * The XML file to read the user mapping from. */ public static final FileGuacamoleProperty BASIC_USER_MAPPING = new FileGuacamoleProperty() { @Override public String getName() { return "basic-user-mapping"; } }; /** * The default filename to use for the user mapping, if not defined within * guacamole.properties. */ public static final String DEFAULT_USER_MAPPING = "user-mapping.xml"; /** * Creates a new BasicFileAuthenticationProvider that authenticates users * against simple, monolithic XML file. * * @throws GuacamoleException * If a required property is missing, or an error occurs while parsing * a property. */ public BasicFileAuthenticationProvider() throws GuacamoleException { environment = new LocalEnvironment(); } @Override public String getIdentifier() { return "default"; } /** * Returns a UserMapping containing all authorization data given within * the XML file specified by the "basic-user-mapping" property in * guacamole.properties. If the XML file has been modified or has not yet * been read, this function may reread the file. * * @return * A UserMapping containing all authorization data within the user * mapping XML file, or null if the file cannot be found/parsed. */ private UserMapping getUserMapping() { // Get user mapping file, defaulting to GUACAMOLE_HOME/user-mapping.xml File userMappingFile; try { userMappingFile = environment.getProperty(BASIC_USER_MAPPING); if (userMappingFile == null) userMappingFile = new File(environment.getGuacamoleHome(), DEFAULT_USER_MAPPING); } // Abort if property cannot be parsed catch (GuacamoleException e) { logger.warn("Unable to read user mapping filename from properties: {}", e.getMessage()); logger.debug("Error parsing user mapping property.", e); return null; } // Abort if user mapping does not exist if (!userMappingFile.exists()) { logger.debug("User mapping file \"{}\" does not exist and will not be read.", userMappingFile); return null; } // Refresh user mapping if file has changed if (lastModified < userMappingFile.lastModified()) { logger.debug("Reading user mapping file: \"{}\"", userMappingFile); // Parse document try { // Get handler for root element UserMappingTagHandler userMappingHandler = new UserMappingTagHandler(); // Set up document handler DocumentHandler contentHandler = new DocumentHandler( "user-mapping", userMappingHandler); // Set up XML parser XMLReader parser = XMLReaderFactory.createXMLReader(); parser.setContentHandler(contentHandler); // Read and parse file InputStream input = new BufferedInputStream(new FileInputStream(userMappingFile)); parser.parse(new InputSource(input)); input.close(); // Store mod time and user mapping lastModified = userMappingFile.lastModified(); cachedUserMapping = userMappingHandler.asUserMapping(); } // If the file is unreadable, return no mapping catch (IOException e) { logger.warn("Unable to read user mapping file \"{}\": {}", userMappingFile, e.getMessage()); logger.debug("Error reading user mapping file.", e); return null; } // If the file cannot be parsed, return no mapping catch (SAXException e) { logger.warn("User mapping file \"{}\" is not valid: {}", userMappingFile, e.getMessage()); logger.debug("Error parsing user mapping file.", e); return null; } } // Return (possibly cached) user mapping return cachedUserMapping; } @Override public Map getAuthorizedConfigurations(Credentials credentials) throws GuacamoleException { // Abort authorization if no user mapping exists UserMapping userMapping = getUserMapping(); if (userMapping == null) return null; // Validate and return info for given user and pass Authorization auth = userMapping.getAuthorization(credentials.getUsername()); if (auth != null && auth.validate(credentials.getUsername(), credentials.getPassword())) return auth.getConfigurations(); // Unauthorized return null; } } guacamole-client-0.9.9/guacamole/pom.xml0100664 0001751 0001751 00000030067 12645510255 022014 0ustar00mjumpermjumper0000000 0000000 4.0.0 org.glyptodon.guacamole guacamole war 0.9.9 guacamole http://guac-dev.org/ The Guacamole web application, providing authentication and an HTML5 remote desktop client. The MIT License http://www.opensource.org/licenses/mit-license.php repo https://github.com/glyptodon/guacamole-client scm:git:git://github.com/glyptodon/guacamole-client.git mike.jumper Michael Jumper mike.jumper@guac-dev.org james.muehlner James Muehlner james.muehlner@guac-dev.org UTF-8 org.apache.maven.plugins maven-compiler-plugin 3.3 1.6 1.6 -Xlint:all -Werror true org.apache.maven.plugins maven-war-plugin 2.6 src/main/webapp true translations/*.json index.html org.glyptodon.guacamole guacamole-common-js zip default-cli process-resources exploded com.keithbranton.mojo angular-maven-plugin 0.3.2 generate-resources html2js ${basedir}/src/main/webapp/app/ **/*.html ${basedir}/src/main/webapp/generated/templates-main/templates.js app com.samaxes.maven minify-maven-plugin 1.6.1 default-cli UTF-8 ${project.build.directory}/${project.build.finalName} / / guacamole.css license.txt app/**/*.css / / guacamole.js lib/jquery/jquery.js lib/angular/angular.min.js lib/angular-module-shim/angular-module-shim.js lib/messageformat/messageformat.js license.txt guacamole-common-js/all.js lib/**/*.js app/**/*.js generated/**/*.js **/*.test.js CLOSURE minify javax.servlet servlet-api 2.5 provided javax.websocket javax.websocket-api 1.0 provided org.slf4j slf4j-api 1.7.7 ch.qos.logback logback-classic 1.1.2 org.glyptodon.guacamole guacamole-common 0.9.9 org.glyptodon.guacamole guacamole-ext 0.9.9 org.glyptodon.guacamole guacamole-common-js 0.9.9 zip runtime org.eclipse.jetty jetty-websocket 8.1.1.v20120215 provided org.eclipse.jetty jetty-parent 20 provided pom org.eclipse.jetty.websocket websocket-api 9.0.7.v20131107 provided org.eclipse.jetty.websocket websocket-servlet 9.0.7.v20131107 provided org.apache.tomcat tomcat-catalina 7.0.37 provided org.apache.tomcat tomcat-coyote 7.0.37 provided com.google.inject guice 3.0 com.google.inject.extensions guice-servlet 3.0 com.sun.jersey jersey-server 1.17.1 com.sun.jersey.contribs jersey-guice 1.17.1 javax.annotation jsr250-api 1.0 commons-codec commons-codec 1.4 com.sun.jersey jersey-json 1.17.1 guacamole-client-0.9.9/guacamole/ChangeLog0100664 0001751 0001751 00000004256 12424302752 022246 0ustar00mjumpermjumper0000000 0000000 2013-07-02 Michael Jumper * Update rdp.xml with new "enable-printing" parameter 2013-06-25 Michael Jumper * Add support for notifications and file download 2013-04-24 James Muehlner * Fix handling of checkbox parameters (ticket #307) 2013-01-09 Michael Jumper * Rewrite parser for user-mapping.xml * Fix truncation of parameter values 2012-12-09 Michael Jumper * Sort connection IDs alphabetically 2012-10-24 Michael Jumper * Implement auto-scaling * Send optimal width/height * Detect and send supported audio mimetypes 2012-10-15 Michael Jumper * Migrate to guacamole-common 0.6.2 * Migrate to guacamole-common-js 0.6.2 2012-08-11 Michael Jumper * Migrate to guacamole-common 0.6.1 * Migrate to guacamole-common-js 0.6.1 2012-06-03 Michael Jumper * Fix clipboard blur bug 2012-05-04 Michael Jumper * Touch-specific menus and keyboard support * Logout prompt * Multiple connections per user in user-mapping.xml * Touch-related UI usability improvements * Support for single sign-on * Support for authentication involving parameters in URLs * Test for WebSocket support (pluggable WebSocket support not yet stable) * UI style improvements 2011-12-11 Michael Jumper * Improved UI usability * Support for multiple connections per user * Real support for authentication providers * Logout button * Connection type icons (thanks to Tango Desktop Project) * Fixed Ctrl-Alt-Delete bug (ticket #57) * Fixed arrow key rendering (Chrome-specific issue) * Fixed exception in XMLReader.parse() (ticket #66) 2011-07-13 Michael Jumper * Migrated to new tunnel API * Major cleanup of UI * Fixed corrupt mouse cursor image * Improved JavaScript style * Logging (via SLF4J) 2011-03-02 Michael Jumper * Initial release of modern 0.3.0+ series guacamole-client-0.9.9/guacamole/LICENSE0100664 0001751 0001751 00000002041 12471273273 021476 0ustar00mjumpermjumper0000000 0000000 Copyright (C) 2013 Glyptodon LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. guacamole-client-0.9.9/guacamole-ext/README0100664 0001751 0001751 00000005522 12424302752 022147 0ustar00mjumpermjumper0000000 0000000 ------------------------------------------------------------ About this README ------------------------------------------------------------ This README is intended to provide quick and to-the-point documentation for technical users intending to compile parts of Guacamole themselves. Distribution-specific packages are available from the files section of the main project page: http://sourceforge.net/projects/guacamole/files/ Distribution-specific documentation is provided on the Guacamole wiki: http://guac-dev.org/ ------------------------------------------------------------ What is guacamole-common-auth? ------------------------------------------------------------ guacamole-common-auth is a Java library used by the Guacamole web application to allow its built-in authentication to be extended or modified and reused. guacamole-common-auth provides an interface for retrieving a set of authorized connection configurations for a given set of arbitrary credentials. Classes implementing this interface can be referenced in guacamole.properties to allow different authentication mechanisms (such as LDAP) to be used. ------------------------------------------------------------ Compiling and installing guacamole-common-auth ------------------------------------------------------------ guacamole-common-auth is built using Maven. Building guacamole-common-auth compiles all classes and packages them into a redistributable .jar file. This .jar file can be easily included in other Maven-based projects (like Guacamole). Note that prebuilt versions of guacamole-common-auth are available from the main guac-dev.org Maven repository which is referenced in all Maven projects in Guacamole. Unless you want to make changes to guacamole-common-auth or you want to use a newer, unreleased version (such as the unstable branch), you do not need to build this manually. You can let Maven download it for you when you build Guacamole. 1) Run mvn package $ mvn package Maven will download any needed dependencies for building the .jar file. Once all dependencies have been downloaded, the .jar file will be created in the target/ subdirectory of the current directory. 2) Run mvn install $ mvn install DO NOT RUN THIS AS ROOT! Maven will install guacamole-common-auth to your user's local Maven repository where it can be used in future builds. It will not install into a system-wide repository and does not require root privileges. Once installed, building other Maven projects that depend on guacamole-common-auth (such as Guacamole) will be possible. ------------------------------------------------------------ Reporting problems ------------------------------------------------------------ Please report any bugs encountered by opening a new ticket at the Trac system hosted at: http://guac-dev.org/trac/ ././@LongLink0100644 0000000 0000000 00000000146 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/resources/org/glyptodon/guacamole/protocols/telnet.jsonguacamole-client-0.9.9/guacamole-ext/src/main/resources/org/glyptodon/guacamole/protocols/telnet.jso0100664 0001751 0001751 00000002740 12612226251 035610 0ustar00mjumpermjumper0000000 0000000 { "name" : "telnet", "forms" : [ { "name" : "network", "fields" : [ { "name" : "hostname", "type" : "TEXT" }, { "name" : "port", "type" : "NUMERIC" } ] }, { "name" : "authentication", "fields" : [ { "name" : "username", "type" : "USERNAME" }, { "name" : "password", "type" : "PASSWORD" }, { "name" : "password-regex", "type" : "TEXT" } ] }, { "name" : "display", "fields" : [ { "name" : "color-scheme", "type" : "ENUM", "options" : [ "", "black-white", "gray-black", "green-black", "white-black" ] }, { "name" : "font-name", "type" : "TEXT" }, { "name" : "font-size", "type" : "ENUM", "options" : [ "", "8", "9", "10", "11", "12", "14", "18", "24", "30", "36", "48", "60", "72", "96" ] } ] } ] } guacamole-client-0.9.9/guacamole-ext/src/main/resources/org/glyptodon/guacamole/protocols/rdp.json0100664 0001751 0001751 00000016134 12645510255 035270 0ustar00mjumpermjumper0000000 0000000 { "name" : "rdp", "forms" : [ { "name" : "network", "fields" : [ { "name" : "hostname", "type" : "TEXT" }, { "name" : "port", "type" : "NUMERIC" } ] }, { "name" : "authentication", "fields" : [ { "name" : "username", "type" : "USERNAME" }, { "name" : "password", "type" : "PASSWORD" }, { "name" : "domain", "type" : "TEXT" }, { "name" : "security", "type" : "ENUM", "options" : [ "", "rdp", "tls", "nla", "any" ] }, { "name" : "disable-auth", "type" : "BOOLEAN", "options" : [ "true" ] }, { "name" : "ignore-cert", "type" : "BOOLEAN", "options" : [ "true" ] } ] }, { "name" : "basic-parameters", "fields" : [ { "name" : "initial-program", "type" : "TEXT" }, { "name" : "client-name", "type" : "TEXT" }, { "name" : "server-layout", "type" : "ENUM", "options" : [ "", "en-us-qwerty", "fr-fr-azerty", "de-de-qwertz", "it-it-qwerty", "sv-se-qwerty", "failsafe" ] }, { "name" : "console", "type" : "BOOLEAN", "options" : [ "true" ] } ] }, { "name" : "display", "fields" : [ { "name" : "width", "type" : "NUMERIC" }, { "name" : "height", "type" : "NUMERIC" }, { "name" : "dpi", "type" : "NUMERIC" }, { "name" : "color-depth", "type" : "ENUM", "options" : [ "", "8", "16", "24", "32" ] } ] }, { "name" : "device-redirection", "fields" : [ { "name" : "console-audio", "type" : "BOOLEAN", "options" : [ "true" ] }, { "name" : "disable-audio", "type" : "BOOLEAN", "options" : [ "true" ] }, { "name" : "enable-printing", "type" : "BOOLEAN", "options" : [ "true" ] }, { "name" : "enable-drive", "type" : "BOOLEAN", "options" : [ "true" ] }, { "name" : "drive-path", "type" : "TEXT" }, { "name" : "create-drive-path", "type" : "BOOLEAN", "options" : [ "true" ] }, { "name" : "static-channels", "type" : "TEXT" } ] }, { "name" : "performance", "fields" : [ { "name" : "enable-wallpaper", "type" : "BOOLEAN", "options" : [ "true" ] }, { "name" : "enable-theming", "type" : "BOOLEAN", "options" : [ "true" ] }, { "name" : "enable-font-smoothing", "type" : "BOOLEAN", "options" : [ "true" ] }, { "name" : "enable-full-window-drag", "type" : "BOOLEAN", "options" : [ "true" ] }, { "name" : "enable-desktop-composition", "type" : "BOOLEAN", "options" : [ "true" ] }, { "name" : "enable-menu-animations", "type" : "BOOLEAN", "options" : [ "true" ] } ] }, { "name" : "remoteapp", "fields" : [ { "name" : "remote-app", "type" : "TEXT" }, { "name" : "remote-app-dir", "type" : "TEXT" }, { "name" : "remote-app-args", "type" : "TEXT" } ] }, { "name" : "preconnection-pdu", "fields" : [ { "name" : "preconnection-id", "type" : "NUMERIC" }, { "name" : "preconnection-blob", "type" : "TEXT" } ] }, { "name" : "sftp", "fields" : [ { "name" : "enable-sftp", "type" : "BOOLEAN", "options" : [ "true" ] }, { "name" : "sftp-hostname", "type" : "TEXT" }, { "name" : "sftp-port", "type" : "NUMERIC" }, { "name" : "sftp-username", "type" : "USERNAME" }, { "name" : "sftp-password", "type" : "PASSWORD" }, { "name" : "sftp-private-key", "type" : "MULTILINE" }, { "name" : "sftp-passphrase", "type" : "PASSWORD" }, { "name" : "sftp-directory", "type" : "TEXT" } ] } ] } guacamole-client-0.9.9/guacamole-ext/src/main/resources/org/glyptodon/guacamole/protocols/ssh.json0100664 0001751 0001751 00000004053 12645510255 035275 0ustar00mjumpermjumper0000000 0000000 { "name" : "ssh", "forms" : [ { "name" : "network", "fields" : [ { "name" : "hostname", "type" : "TEXT" }, { "name" : "port", "type" : "NUMERIC" } ] }, { "name" : "authentication", "fields" : [ { "name" : "username", "type" : "USERNAME" }, { "name" : "password", "type" : "PASSWORD" }, { "name" : "private-key", "type" : "MULTILINE" }, { "name" : "passphrase", "type" : "PASSWORD" } ] }, { "name" : "display", "fields" : [ { "name" : "color-scheme", "type" : "ENUM", "options" : [ "", "black-white", "gray-black", "green-black", "white-black" ] }, { "name" : "font-name", "type" : "TEXT" }, { "name" : "font-size", "type" : "ENUM", "options" : [ "", "8", "9", "10", "11", "12", "14", "18", "24", "30", "36", "48", "60", "72", "96" ] } ] }, { "name" : "session", "fields" : [ { "name" : "command", "type" : "TEXT" } ] }, { "name" : "sftp", "fields" : [ { "name" : "enable-sftp", "type" : "BOOLEAN", "options" : [ "true" ] } ] } ] }guacamole-client-0.9.9/guacamole-ext/src/main/resources/org/glyptodon/guacamole/protocols/vnc.json0100664 0001751 0001751 00000006714 12612226251 035266 0ustar00mjumpermjumper0000000 0000000 { "name" : "vnc", "forms" : [ { "name" : "network", "fields" : [ { "name" : "hostname", "type" : "TEXT" }, { "name" : "port", "type" : "NUMERIC" } ] }, { "name" : "authentication", "fields" : [ { "name" : "password", "type" : "PASSWORD" } ] }, { "name" : "display", "fields" : [ { "name" : "read-only", "type" : "BOOLEAN", "options" : [ "true" ] }, { "name" : "swap-red-blue", "type" : "BOOLEAN", "options" : [ "true" ] }, { "name" : "cursor", "type" : "ENUM", "options" : [ "", "local", "remote" ] }, { "name" : "color-depth", "type" : "ENUM", "options" : [ "", "8", "16", "24", "32" ] } ] }, { "name" : "clipboard", "fields" : [ { "name" : "clipboard-encoding", "type" : "ENUM", "options" : [ "", "ISO8859-1", "UTF-8", "UTF-16", "CP1252" ] } ] }, { "name" : "repeater", "fields" : [ { "name" : "dest-host", "type" : "TEXT" }, { "name" : "dest-port", "type" : "NUMERIC" } ] }, { "name" : "sftp", "fields" : [ { "name" : "enable-sftp", "type" : "BOOLEAN", "options" : [ "true" ] }, { "name" : "sftp-hostname", "type" : "TEXT" }, { "name" : "sftp-port", "type" : "NUMERIC" }, { "name" : "sftp-username", "type" : "USERNAME" }, { "name" : "sftp-password", "type" : "PASSWORD" }, { "name" : "sftp-private-key", "type" : "MULTILINE" }, { "name" : "sftp-passphrase", "type" : "PASSWORD" }, { "name" : "sftp-directory", "type" : "TEXT" } ] }, { "name" : "audio", "fields" : [ { "name" : "enable-audio", "type" : "BOOLEAN", "options" : [ "true" ] }, { "name" : "audio-servername", "type" : "TEXT" } ] } ] } guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/token/TokenFilter.java0100664 0001751 0001751 00000016772 12612226251 034706 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.token; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Filtering object which replaces tokens of the form "${TOKEN_NAME}" with * their corresponding values. Unknown tokens are not replaced. If TOKEN_NAME * is a valid token, the literal value "${TOKEN_NAME}" can be included by using * "$${TOKEN_NAME}". * * @author Michael Jumper */ public class TokenFilter { /** * Regular expression which matches individual tokens, with additional * capturing groups for convenient retrieval of leading text, the possible * escape character preceding the token, the name of the token, and the * entire token itself. */ private final Pattern tokenPattern = Pattern.compile("(.*?)(^|.)(\\$\\{([A-Za-z0-9_]*)\\})"); /** * The index of the capturing group within tokenPattern which matches * non-token text preceding a possible token. */ private static final int LEADING_TEXT_GROUP = 1; /** * The index of the capturing group within tokenPattern which matches the * character immediately preceding a possible token, possibly denoting that * the token should instead be interpreted as a literal. */ private static final int ESCAPE_CHAR_GROUP = 2; /** * The index of the capturing group within tokenPattern which matches the * entire token, including the leading "${" and terminating "}" strings. */ private static final int TOKEN_GROUP = 3; /** * The index of the capturing group within tokenPattern which matches only * the token name contained within the "${" and "}" strings. */ private static final int TOKEN_NAME_GROUP = 4; /** * The values of all known tokens. */ private final Map tokenValues = new HashMap(); /** * Sets the token having the given name to the given value. Any existing * value for that token is replaced. * * @param name * The name of the token to set. * * @param value * The value to set the token to. */ public void setToken(String name, String value) { tokenValues.put(name, value); } /** * Returns the value of the token with the given name, or null if no such * token has been set. * * @param name * The name of the token to return. * * @return * The value of the token with the given name, or null if no such * token exists. */ public String getToken(String name) { return tokenValues.get(name); } /** * Removes the value of the token with the given name. If no such token * exists, this function has no effect. * * @param name * The name of the token whose value should be removed. */ public void unsetToken(String name) { tokenValues.remove(name); } /** * Returns a map of all tokens, with each key being a token name, and each * value being the corresponding token value. Changes to this map will * directly affect the tokens associated with this filter. * * @return * A map of all token names and their corresponding values. */ public Map getTokens() { return tokenValues; } /** * Replaces all current token values with the contents of the given map, * where each map key represents a token name, and each map value * represents a token value. * * @param tokens * A map containing the token names and corresponding values to * assign. */ public void setTokens(Map tokens) { tokenValues.clear(); tokenValues.putAll(tokens); } /** * Filters the given string, replacing any tokens with their corresponding * values. * * @param input * The string to filter. * * @return * A copy of the input string, with any tokens replaced with their * corresponding values. */ public String filter(String input) { StringBuilder output = new StringBuilder(); Matcher tokenMatcher = tokenPattern.matcher(input); // Track last regex match int endOfLastMatch = 0; // For each possible token while (tokenMatcher.find()) { // Pull possible leading text and first char before possible token String literal = tokenMatcher.group(LEADING_TEXT_GROUP); String escape = tokenMatcher.group(ESCAPE_CHAR_GROUP); // Append leading non-token text output.append(literal); // If char before token is '$', the token itself is escaped if ("$".equals(escape)) { String notToken = tokenMatcher.group(TOKEN_GROUP); output.append(notToken); } // If char is not '$', interpret as a token else { // The char before the token, if any, is a literal output.append(escape); // Pull token value String tokenName = tokenMatcher.group(TOKEN_NAME_GROUP); String tokenValue = getToken(tokenName); // If token is unknown, interpret as literal if (tokenValue == null) { String notToken = tokenMatcher.group(TOKEN_GROUP); output.append(notToken); } // Otherwise, substitute value else output.append(tokenValue); } // Update last regex match endOfLastMatch = tokenMatcher.end(); } // Append any remaining non-token text output.append(input.substring(endOfLastMatch)); return output.toString(); } /** * Given an arbitrary map containing String values, replace each non-null * value with the corresponding filtered value. * * @param map * The map whose values should be filtered. */ public void filterValues(Map map) { // For each map entry for (Map.Entry entry : map.entrySet()) { // If value is non-null, filter value through this TokenFilter String value = entry.getValue(); if (value != null) entry.setValue(filter(value)); } } } ././@LongLink0100644 0000000 0000000 00000000145 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/token/StandardTokens.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/token/StandardTokens.java0100664 0001751 0001751 00000005414 12612226251 035373 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.token; import org.glyptodon.guacamole.net.auth.Credentials; /** * Utility class which provides access to standardized token names, as well as * facilities for generating those tokens from common objects. * * @author Michael Jumper */ public class StandardTokens { /** * The name of the username token added via addStandardTokens(). */ private static final String USERNAME_TOKEN = "GUAC_USERNAME"; /** * The name of the password token added via addStandardTokens(). */ private static final String PASSWORD_TOKEN = "GUAC_PASSWORD"; /** * This utility class should not be instantiated. */ private StandardTokens() {} /** * Adds the standard username (GUAC_USERNAME) and password (GUAC_PASSWORD) * tokens to the given TokenFilter using the values from the given * Credentials object. If either the username or password are not set * within the given credentials, the corresponding token(s) will remain * unset. * * @param filter * The TokenFilter to add standard username/password tokens to. * * @param credentials * The Credentials containing the username/password to add. * */ public static void addStandardTokens(TokenFilter filter, Credentials credentials) { // Add username token String username = credentials.getUsername(); if (username != null) filter.setToken(USERNAME_TOKEN, username); // Add password token String password = credentials.getPassword(); if (password != null) filter.setToken(PASSWORD_TOKEN, password); } } ././@LongLink0100644 0000000 0000000 00000000147 12612226251 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/protocols/ProtocolInfo.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/protocols/ProtocolInfo.ja0100664 0001751 0001751 00000007216 12612226251 035443 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.protocols; import java.util.ArrayList; import java.util.Collection; import org.glyptodon.guacamole.form.Form; /** * Describes a protocol and all forms associated with it, as required by * a protocol plugin for guacd. This class allows known forms for a * protocol to be exposed to the user as friendly fields. * * @author Michael Jumper */ public class ProtocolInfo { /** * The unique name associated with this protocol. */ private String name; /** * A collection of all associated protocol forms. */ private Collection forms; /** * Creates a new ProtocolInfo with no associated name or forms. */ public ProtocolInfo() { this.forms = new ArrayList(); } /** * Creates a new ProtocolInfo having the given name, but without any forms. * * @param name * The unique name associated with the protocol. */ public ProtocolInfo(String name) { this.name = name; this.forms = new ArrayList(); } /** * Creates a new ProtocolInfo having the given name and forms. * * @param name * The unique name associated with the protocol. * * @param forms * The forms to associate with the protocol. */ public ProtocolInfo(String name, Collection forms) { this.name = name; this.forms = forms; } /** * Returns the unique name of this protocol. The protocol name is the * value required by the corresponding protocol plugin for guacd. * * @return The unique name of this protocol. */ public String getName() { return name; } /** * Sets the unique name of this protocol. The protocol name is the value * required by the corresponding protocol plugin for guacd. * * @param name The unique name of this protocol. */ public void setName(String name) { this.name = name; } /** * Returns a mutable collection of the protocol forms associated with * this protocol. Changes to this collection affect the forms exposed * to the user. * * @return A mutable collection of protocol forms. */ public Collection getForms() { return forms; } /** * Sets the collection of protocol forms associated with this * protocol. * * @param forms * The collection of forms to associate with this protocol. */ public void setForms(Collection forms) { this.forms = forms; } } guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/xml/DocumentHandler.java0100664 0001751 0001751 00000015164 12612226251 035206 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.xml; import java.util.Deque; import java.util.LinkedList; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; /** * A simple ContentHandler implementation which digests SAX document events and * produces simpler tag-level events, maintaining its own stack for the * convenience of the tag handlers. * * @author Mike Jumper */ public class DocumentHandler extends DefaultHandler { /** * The name of the root element of the document. */ private String rootElementName; /** * The handler which will be used to handle element events for the root * element of the document. */ private TagHandler root; /** * The stack of all states applicable to the current parser state. Each * element of the stack references the TagHandler for the element being * parsed at that level of the document, where the current element is * last in the stack, and the root element is first. */ private Deque stack = new LinkedList(); /** * Creates a new DocumentHandler which will use the given TagHandler * to handle the root element. * * @param rootElementName The name of the root element of the document * being handled. * @param root The TagHandler to use for the root element. */ public DocumentHandler(String rootElementName, TagHandler root) { this.root = root; this.rootElementName = rootElementName; } /** * Returns the current element state. The current element state is the * state of the element the parser is currently within. * * @return The current element state. */ private DocumentHandlerState getCurrentState() { // If no state, return null if (stack.isEmpty()) return null; return stack.getLast(); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { // Get current state DocumentHandlerState current = getCurrentState(); // Handler for tag just read TagHandler handler; // If no stack, use root handler if (current == null) { // Validate element name if (!localName.equals(rootElementName)) throw new SAXException("Root element must be '" + rootElementName + "'"); handler = root; } // Otherwise, get handler from parent else { TagHandler parent_handler = current.getTagHandler(); handler = parent_handler.childElement(localName); } // If no handler returned, the element was not expected if (handler == null) throw new SAXException("Unexpected element: '" + localName + "'"); // Initialize handler handler.init(attributes); // Append new element state to stack stack.addLast(new DocumentHandlerState(handler)); } @Override public void endElement(String uri, String localName, String qName) throws SAXException { // Pop last element from stack DocumentHandlerState completed = stack.removeLast(); // Finish element by sending text content completed.getTagHandler().complete( completed.getTextContent().toString()); } @Override public void characters(char[] ch, int start, int length) throws SAXException { // Get current state DocumentHandlerState current = getCurrentState(); if (current == null) throw new SAXException("Character data not allowed outside XML document."); // Append received chunk to text content current.getTextContent().append(ch, start, length); } /** * The current state of the DocumentHandler. */ private static class DocumentHandlerState { /** * The current text content of the current element being parsed. */ private StringBuilder textContent = new StringBuilder(); /** * The TagHandler which must handle document events related to the * element currently being parsed. */ private TagHandler tagHandler; /** * Creates a new DocumentHandlerState which will maintain the state * of parsing of the current element, as well as contain the TagHandler * which will receive events related to that element. * * @param tagHandler The TagHandler which should receive any events * related to the element being parsed. */ public DocumentHandlerState(TagHandler tagHandler) { this.tagHandler = tagHandler; } /** * Returns the mutable StringBuilder which contains the current text * content of the element being parsed. * * @return The mutable StringBuilder which contains the current text * content of the element being parsed. */ public StringBuilder getTextContent() { return textContent; } /** * Returns the TagHandler which must handle any events relating to the * element being parsed. * * @return The TagHandler which must handle any events relating to the * element being parsed. */ public TagHandler getTagHandler() { return tagHandler; } } } guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/xml/package-info.java0100664 0001751 0001751 00000002343 12612226251 034451 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes driving the SAX-based XML parser used by the Guacamole web * application. */ package org.glyptodon.guacamole.xml; guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/xml/TagHandler.java0100664 0001751 0001751 00000005521 12612226251 034137 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.xml; import org.xml.sax.Attributes; import org.xml.sax.SAXException; /** * A simple element-level event handler for events triggered by the * SAX-driven DocumentHandler parser. * * @author Mike Jumper */ public interface TagHandler { /** * Called when a child element of the current element is parsed. * * @param localName The local name of the child element seen. * @return The TagHandler which should handle all element-level events * related to the child element. * @throws SAXException If the child element being parsed was not expected, * or some other error prevents a proper TagHandler * from being constructed for the child element. */ public TagHandler childElement(String localName) throws SAXException; /** * Called when the element corresponding to this TagHandler is first seen, * just after an instance is created. * * @param attributes The attributes of the element seen. * @throws SAXException If an error prevents a the TagHandler from being * from being initialized. */ public void init(Attributes attributes) throws SAXException; /** * Called when this element, and all child elements, have been fully parsed, * and the entire text content of this element (if any) is available. * * @param textContent The full text content of this element, if any. * @throws SAXException If the text content received is not valid for any * reason, or the child elements parsed are not * correct. */ public void complete(String textContent) throws SAXException; } ././@LongLink0100644 0000000 0000000 00000000155 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/environment/LocalEnvironment.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/environment/LocalEnvironm0100664 0001751 0001751 00000025302 12612226251 035521 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.environment; import java.io.File; import java.io.FileInputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.Properties; import org.codehaus.jackson.map.ObjectMapper; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleServerException; import org.glyptodon.guacamole.properties.GuacamoleProperty; import org.glyptodon.guacamole.protocols.ProtocolInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The environment of the locally-running Guacamole instance, describing * available protocols, configuration parameters, and the GUACAMOLE_HOME * directory. * * @author Michael Jumper */ public class LocalEnvironment implements Environment { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(LocalEnvironment.class); /** * Array of all known protocol names. */ private static final String[] KNOWN_PROTOCOLS = new String[]{ "vnc", "rdp", "ssh", "telnet"}; /** * All properties read from guacamole.properties. */ private final Properties properties; /** * The location of GUACAMOLE_HOME, which may not truly exist. */ private final File guacHome; /** * The map of all available protocols. */ private final Map availableProtocols; /** * The Jackson parser for parsing JSON files. */ private static final ObjectMapper mapper = new ObjectMapper(); /** * Creates a new Environment, initializing that environment based on the * location of GUACAMOLE_HOME and the contents of guacamole.properties. * * @throws GuacamoleException If an error occurs while determining the * environment of this Guacamole instance. */ public LocalEnvironment() throws GuacamoleException { // Determine location of GUACAMOLE_HOME guacHome = findGuacamoleHome(); // Read properties properties = new Properties(); try { InputStream stream = null; // If not a directory, load from classpath if (!guacHome.isDirectory()) stream = LocalEnvironment.class.getResourceAsStream("/guacamole.properties"); // Otherwise, try to load from file else { File propertiesFile = new File(guacHome, "guacamole.properties"); if (propertiesFile.exists()) stream = new FileInputStream(propertiesFile); } // Load properties from stream, if any, always closing stream when done if (stream != null) { try { properties.load(stream); } finally { stream.close(); } } // Notify if we're proceeding without guacamole.properties else logger.info("No guacamole.properties file found within GUACAMOLE_HOME or the classpath. Using defaults."); } catch (IOException e) { logger.warn("The guacamole.properties file within GUACAMOLE_HOME cannot be read: {}", e.getMessage()); logger.debug("Error reading guacamole.properties.", e); } // Read all protocols availableProtocols = readProtocols(); } /** * Locates the Guacamole home directory by checking, in order: * the guacamole.home system property, the GUACAMOLE_HOME environment * variable, and finally the .guacamole directory in the home directory of * the user running the servlet container. * * @return The File representing the Guacamole home directory, which may * or may not exist, and may turn out to not be a directory. */ private static File findGuacamoleHome() { // Attempt to find Guacamole home File guacHome; // Use system property by default String desiredDir = System.getProperty("guacamole.home"); // Failing that, try the GUACAMOLE_HOME environment variable if (desiredDir == null) desiredDir = System.getenv("GUACAMOLE_HOME"); // If successful, use explicitly specified directory if (desiredDir != null) guacHome = new File(desiredDir); // If not explicitly specified, use ~/.guacamole else guacHome = new File(System.getProperty("user.home"), ".guacamole"); // Return discovered directory return guacHome; } /** * Parses the given JSON file, returning the parsed ProtocolInfo. The JSON * format is conveniently and intentionally identical to a serialized * ProtocolInfo object, which is identical to the JSON format used by the * protocol REST service built into the Guacamole web application. * * @param input * An input stream containing JSON describing the forms and parameters * associated with a protocol supported by Guacamole. * * @return * A new ProtocolInfo object which contains the forms and parameters * described by the JSON file parsed. * * @throws IOException * If an error occurs while parsing the JSON file. */ private ProtocolInfo readProtocol(InputStream input) throws IOException { return mapper.readValue(input, ProtocolInfo.class); } /** * Reads through all pre-defined protocols and any protocols within the * "protocols" subdirectory of GUACAMOLE_HOME, returning a map containing * each of these protocols. The key of each entry will be the name of that * protocol, as would be passed to guacd during connection. * * @return * A map of all available protocols. */ private Map readProtocols() { // Map of all available protocols Map protocols = new HashMap(); // Get protcols directory File protocol_directory = new File(getGuacamoleHome(), "protocols"); // Read protocols from directory if it exists if (protocol_directory.isDirectory()) { // Get all JSON files File[] files = protocol_directory.listFiles( new FilenameFilter() { @Override public boolean accept(File file, String string) { return string.endsWith(".json"); } } ); // Warn if directory contents are not available if (files == null) { logger.error("Unable to read contents of \"{}\".", protocol_directory.getAbsolutePath()); files = new File[0]; } // Load each protocol from each file for (File file : files) { try { // Parse protocol FileInputStream stream = new FileInputStream(file); ProtocolInfo protocol = readProtocol(stream); stream.close(); // Store protocol protocols.put(protocol.getName(), protocol); } catch (IOException e) { logger.error("Unable to read connection parameter information from \"{}\": {}", file.getAbsolutePath(), e.getMessage()); logger.debug("Error reading protocol JSON.", e); } } } // If known protocols are not already defined, read from classpath for (String protocol : KNOWN_PROTOCOLS) { // If protocol not defined yet, attempt to load from classpath if (!protocols.containsKey(protocol)) { InputStream stream = LocalEnvironment.class.getResourceAsStream( "/org/glyptodon/guacamole/protocols/" + protocol + ".json"); // Parse JSON if available if (stream != null) { try { protocols.put(protocol, readProtocol(stream)); } catch (IOException e) { logger.error("Unable to read pre-defined connection parameter information for protocol \"{}\": {}", protocol, e.getMessage()); logger.debug("Error reading pre-defined protocol JSON.", e); } } } } // Protocols map now fully populated return protocols; } @Override public File getGuacamoleHome() { return guacHome; } @Override public Type getProperty(GuacamoleProperty property) throws GuacamoleException { return property.parseValue(properties.getProperty(property.getName())); } @Override public Type getProperty(GuacamoleProperty property, Type defaultValue) throws GuacamoleException { Type value = getProperty(property); if (value == null) return defaultValue; return value; } @Override public Type getRequiredProperty(GuacamoleProperty property) throws GuacamoleException { Type value = getProperty(property); if (value == null) throw new GuacamoleServerException("Property " + property.getName() + " is required."); return value; } @Override public Map getProtocols() { return availableProtocols; } @Override public ProtocolInfo getProtocol(String name) { return availableProtocols.get(name); } } ././@LongLink0100644 0000000 0000000 00000000150 12612226251 011626 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/environment/Environment.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/environment/Environment.j0100664 0001751 0001751 00000014246 12612226251 035512 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.environment; import java.io.File; import java.util.Map; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.properties.BooleanGuacamoleProperty; import org.glyptodon.guacamole.properties.GuacamoleProperty; import org.glyptodon.guacamole.properties.IntegerGuacamoleProperty; import org.glyptodon.guacamole.properties.StringGuacamoleProperty; import org.glyptodon.guacamole.protocols.ProtocolInfo; /** * The environment of an arbitrary Guacamole instance, describing available * protocols, configuration parameters, and the GUACAMOLE_HOME directory. * * @author Michael Jumper */ public interface Environment { /** * The hostname of the server where guacd (the Guacamole proxy server) is * running. */ public static final StringGuacamoleProperty GUACD_HOSTNAME = new StringGuacamoleProperty() { @Override public String getName() { return "guacd-hostname"; } }; /** * The port that guacd (the Guacamole proxy server) is listening on. */ public static final IntegerGuacamoleProperty GUACD_PORT = new IntegerGuacamoleProperty() { @Override public String getName() { return "guacd-port"; } }; /** * Whether guacd requires SSL/TLS on connections. */ public static final BooleanGuacamoleProperty GUACD_SSL = new BooleanGuacamoleProperty() { @Override public String getName() { return "guacd-ssl"; } }; /** * Returns the Guacamole home directory as determined when this Environment * object was created. The Guacamole home directory is found by checking, in * order: the guacamole.home system property, the GUACAMOLE_HOME environment * variable, and finally the .guacamole directory in the home directory of * the user running the servlet container. * * @return The File representing the Guacamole home directory, which may * or may not exist, and may turn out to not be a directory. */ public File getGuacamoleHome(); /** * Returns a map of all available protocols, where each key is the name of * that protocol as would be passed to guacd during connection. * * @return A map of all available protocols. */ public Map getProtocols(); /** * Returns the protocol having the given name. The name must be the * protocol name as would be passed to guacd during connection. * * @param name The name of the protocol. * @return The protocol having the given name, or null if no such * protocol is registered. */ public ProtocolInfo getProtocol(String name); /** * Given a GuacamoleProperty, parses and returns the value set for that * property in guacamole.properties, if any. * * @param The type that the given property is parsed into. * @param property The property to read from guacamole.properties. * @return The parsed value of the property as read from * guacamole.properties. * @throws GuacamoleException If an error occurs while parsing the value * for the given property in * guacamole.properties. */ public Type getProperty(GuacamoleProperty property) throws GuacamoleException; /** * Given a GuacamoleProperty, parses and returns the value set for that * property in guacamole.properties, if any. If no value is found, the * provided default value is returned. * * @param The type that the given property is parsed into. * @param property The property to read from guacamole.properties. * @param defaultValue The value to return if no value was given in * guacamole.properties. * @return The parsed value of the property as read from * guacamole.properties, or the provided default value if no value * was found. * @throws GuacamoleException If an error occurs while parsing the value * for the given property in * guacamole.properties. */ public Type getProperty(GuacamoleProperty property, Type defaultValue) throws GuacamoleException; /** * Given a GuacamoleProperty, parses and returns the value set for that * property in guacamole.properties. An exception is thrown if the value * is not provided. * * @param The type that the given property is parsed into. * @param property The property to read from guacamole.properties. * @return The parsed value of the property as read from * guacamole.properties. * @throws GuacamoleException If an error occurs while parsing the value * for the given property in * guacamole.properties, or if the property is * not specified. */ public Type getRequiredProperty(GuacamoleProperty property) throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000155 12471273273 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/properties/GuacamoleProperty.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/properties/GuacamolePrope0100664 0001751 0001751 00000004156 12471273273 035521 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.properties; import org.glyptodon.guacamole.GuacamoleException; /** * An abstract representation of a property in the guacamole.properties file, * which parses into a specific type. * * @author Michael Jumper * @param The type this GuacamoleProperty will parse into. */ public interface GuacamoleProperty { /** * Returns the name of the property in guacamole.properties that this * GuacamoleProperty will parse. * * @return The name of the property in guacamole.properties that this * GuacamoleProperty will parse. */ public String getName(); /** * Parses the given string value into the type associated with this * GuacamoleProperty. * * @param value The string value to parse. * @return The parsed value. * @throws GuacamoleException If an error occurs while parsing the * provided value. */ public Type parseValue(String value) throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000157 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/properties/GuacamoleProperties.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/properties/GuacamolePrope0100664 0001751 0001751 00000017025 12612226251 035507 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.properties; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Properties; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleServerException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Simple utility class for reading properties from the guacamole.properties * file. The guacamole.properties file is preferably located in the servlet * container's user's home directory, in a subdirectory called .guacamole, or * in the directory set by the system property: guacamole.home. * * If none of those locations are possible, guacamole.properties will also * be read from the root of the classpath. * * @deprecated * @author Michael Jumper */ public class GuacamoleProperties { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(GuacamoleProperties.class); static { // Warn about deprecation logger.warn("GuacamoleProperties is deprecated. Please use Environment instead."); } /** * GuacamoleProperties is a utility class and cannot be instantiated. */ private GuacamoleProperties() {} /** * The hostname of the server where guacd (the Guacamole proxy server) is * running. */ public static final StringGuacamoleProperty GUACD_HOSTNAME = new StringGuacamoleProperty() { @Override public String getName() { return "guacd-hostname"; } }; /** * The port that guacd (the Guacamole proxy server) is listening on. */ public static final IntegerGuacamoleProperty GUACD_PORT = new IntegerGuacamoleProperty() { @Override public String getName() { return "guacd-port"; } }; /** * Whether guacd requires SSL/TLS on connections. */ public static final BooleanGuacamoleProperty GUACD_SSL = new BooleanGuacamoleProperty() { @Override public String getName() { return "guacd-ssl"; } }; /** * All properties read from guacamole.properties when this class was first * used. */ private static final Properties properties; /** * Any error encountered when reading guacamole.properties was last * attempted. */ private static GuacamoleException exception; static { properties = new Properties(); try { // Attempt to find Guacamole home File guacHome = GuacamoleHome.getDirectory(); InputStream stream; // If not a directory, load from classpath if (!guacHome.isDirectory()) { // Read from classpath stream = GuacamoleProperties.class.getResourceAsStream("/guacamole.properties"); if (stream == null) throw new IOException( "guacamole.properties not loaded from " + guacHome + " (not a directory), and guacamole.properties could" + " not be found as a resource in the classpath."); } // Otherwise, try to load from file else stream = new FileInputStream(new File(guacHome, "guacamole.properties")); // Load properties, always close stream try { properties.load(stream); } finally { stream.close(); } } catch (IOException e) { exception = new GuacamoleServerException("Error reading guacamole.properties", e); } } /** * Given a GuacamoleProperty, parses and returns the value set for that * property in guacamole.properties, if any. * * @param The type that the given property is parsed into. * @param property The property to read from guacamole.properties. * @return The parsed value of the property as read from * guacamole.properties. * @throws GuacamoleException If an error occurs while parsing the value * for the given property in * guacamole.properties. */ public static Type getProperty(GuacamoleProperty property) throws GuacamoleException { if (exception != null) throw exception; return property.parseValue(properties.getProperty(property.getName())); } /** * Given a GuacamoleProperty, parses and returns the value set for that * property in guacamole.properties, if any. If no value is found, the * provided default value is returned. * * @param The type that the given property is parsed into. * @param property The property to read from guacamole.properties. * @param defaultValue The value to return if no value was given in * guacamole.properties. * @return The parsed value of the property as read from * guacamole.properties, or the provided default value if no value * was found. * @throws GuacamoleException If an error occurs while parsing the value * for the given property in * guacamole.properties. */ public static Type getProperty(GuacamoleProperty property, Type defaultValue) throws GuacamoleException { Type value = getProperty(property); if (value == null) return defaultValue; return value; } /** * Given a GuacamoleProperty, parses and returns the value set for that * property in guacamole.properties. An exception is thrown if the value * is not provided. * * @param The type that the given property is parsed into. * @param property The property to read from guacamole.properties. * @return The parsed value of the property as read from * guacamole.properties. * @throws GuacamoleException If an error occurs while parsing the value * for the given property in * guacamole.properties, or if the property is * not specified. */ public static Type getRequiredProperty(GuacamoleProperty property) throws GuacamoleException { Type value = getProperty(property); if (value == null) throw new GuacamoleServerException("Property " + property.getName() + " is required."); return value; } } ././@LongLink0100644 0000000 0000000 00000000164 12471273273 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/properties/BooleanGuacamoleProperty.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/properties/BooleanGuacamo0100664 0001751 0001751 00000004050 12471273273 035463 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.properties; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleServerException; /** * A GuacamoleProperty whose value is an boolean. Legal true values are "true", * or "false". Case does not matter. * * @author Michael Jumper */ public abstract class BooleanGuacamoleProperty implements GuacamoleProperty { @Override public Boolean parseValue(String value) throws GuacamoleException { // If no property provided, return null. if (value == null) return null; // If "true", return true if (value.equalsIgnoreCase("true")) return true; // If "false", return false if (value.equalsIgnoreCase("false")) return false; // Otherwise, fail throw new GuacamoleServerException("Property \"" + getName() + "\" must be either \"true\" or \"false\"."); } } ././@LongLink0100644 0000000 0000000 00000000151 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/properties/GuacamoleHome.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/properties/GuacamoleHome.0100664 0001751 0001751 00000005616 12612226251 035373 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.properties; import java.io.File; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Abstract representation of the Guacamole configuration directory. * * @deprecated * @author Michael Jumper */ public class GuacamoleHome { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(GuacamoleHome.class); static { // Warn about deprecation logger.warn("GuacamoleHome is deprecated. Please use Environment instead."); } /** * GuacamoleHome is a utility class and cannot be instantiated. */ private GuacamoleHome() {} /** * Returns the Guacamole home directory by checking, in order: * the guacamole.home system property, the GUACAMOLE_HOME environment * variable, and finally the .guacamole directory in the home directory of * the user running the servlet container. * * @return The File representing the Guacamole home directory, which may * or may not exist, and may turn out to not be a directory. */ public static File getDirectory() { // Attempt to find Guacamole home File guacHome; // Use system property by default String desiredDir = System.getProperty("guacamole.home"); // Failing that, try the GUACAMOLE_HOME environment variable if (desiredDir == null) desiredDir = System.getenv("GUACAMOLE_HOME"); // If successful, use explicitly specified directory if (desiredDir != null) guacHome = new File(desiredDir); // If not explicitly specified, use ~/.guacamole else guacHome = new File(System.getProperty("user.home"), ".guacamole"); // Return discovered directory return guacHome; } } ././@LongLink0100644 0000000 0000000 00000000161 12471273273 011641 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/properties/FileGuacamoleProperty.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/properties/FileGuacamoleP0100664 0001751 0001751 00000003145 12471273273 035430 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.properties; import java.io.File; import org.glyptodon.guacamole.GuacamoleException; /** * A GuacamoleProperty whose value is a filename. * * @author Michael Jumper */ public abstract class FileGuacamoleProperty implements GuacamoleProperty { @Override public File parseValue(String value) throws GuacamoleException { // If no property provided, return null. if (value == null) return null; return new File(value); } } ././@LongLink0100644 0000000 0000000 00000000163 12471273273 011643 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/properties/StringGuacamoleProperty.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/properties/StringGuacamol0100664 0001751 0001751 00000002751 12471273273 035534 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.properties; import org.glyptodon.guacamole.GuacamoleException; /** * A GuacamoleProperty whose value is a simple string. * * @author Michael Jumper */ public abstract class StringGuacamoleProperty implements GuacamoleProperty { @Override public String parseValue(String value) throws GuacamoleException { return value; } } ././@LongLink0100644 0000000 0000000 00000000150 12471273273 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/properties/package-info.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/properties/package-info.j0100664 0001751 0001751 00000002376 12471273273 035374 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides classes for reading properties from the web-application-wide * guacamole.properties file. */ package org.glyptodon.guacamole.properties; ././@LongLink0100644 0000000 0000000 00000000164 12471273273 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/properties/IntegerGuacamoleProperty.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/properties/IntegerGuacamo0100664 0001751 0001751 00000003565 12471273273 035513 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.properties; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleServerException; /** * A GuacamoleProperty whose value is an integer. * * @author Michael Jumper */ public abstract class IntegerGuacamoleProperty implements GuacamoleProperty { @Override public Integer parseValue(String value) throws GuacamoleException { // If no property provided, return null. if (value == null) return null; try { Integer integer = new Integer(value); return integer; } catch (NumberFormatException e) { throw new GuacamoleServerException("Property \"" + getName() + "\" must be an integer.", e); } } } ././@LongLink0100644 0000000 0000000 00000000161 12471273273 011641 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/properties/LongGuacamoleProperty.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/properties/LongGuacamoleP0100664 0001751 0001751 00000003544 12471273273 035453 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.properties; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleServerException; /** * A GuacamoleProperty whose value is an long. * * @author James Muehlner */ public abstract class LongGuacamoleProperty implements GuacamoleProperty { @Override public Long parseValue(String value) throws GuacamoleException { // If no property provided, return null. if (value == null) return null; try { Long longValue = new Long(value); return longValue; } catch (NumberFormatException e) { throw new GuacamoleServerException("Property \"" + getName() + "\" must be an long.", e); } } } ././@LongLink0100644 0000000 0000000 00000000201 12471273273 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/listener/AuthenticationSuccessListener.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/listener/Authen0100664 0001751 0001751 00000005001 12471273273 035370 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.event.listener; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.event.AuthenticationSuccessEvent; /** * A listener whose hooks will fire immediately before and after a user's * authentication attempt succeeds. If a user successfully authenticates, * the authenticationSucceeded() hook has the opportunity to cancel the * authentication and force it to fail. * * @author Michael Jumper */ public interface AuthenticationSuccessListener { /** * Event hook which fires immediately after a user's authentication attempt * succeeds. The return value of this hook dictates whether the * successful authentication attempt is canceled. * * @param e The AuthenticationFailureEvent describing the authentication * failure that just occurred. * @return true if the successful authentication attempt should be * allowed, or false if the attempt should be denied, causing * the attempt to effectively fail. * @throws GuacamoleException If an error occurs while handling the * authentication success event. Throwing an * exception will also cancel the authentication * success. */ boolean authenticationSucceeded(AuthenticationSuccessEvent e) throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000167 12471273273 011647 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/listener/TunnelCloseListener.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/listener/Tunnel0100664 0001751 0001751 00000004343 12471273273 035421 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.event.listener; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.event.TunnelCloseEvent; /** * A listener whose tunnelClosed() hook will fire immediately after an * existing tunnel is closed. * * @author Michael Jumper */ public interface TunnelCloseListener { /** * Event hook which fires immediately after an existing tunnel is closed. * The return value of this hook dictates whether the tunnel is allowed to * be closed. * * @param e The TunnelCloseEvent describing the tunnel being closed and * any associated credentials. * @return true if the tunnel should be allowed to be closed, or false * if the attempt should be denied, causing the attempt to * effectively fail. * @throws GuacamoleException If an error occurs while handling the * tunnel close event. Throwing an exception * will also stop the tunnel from being closed. */ boolean tunnelClosed(TunnelCloseEvent e) throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000201 12471273273 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/listener/AuthenticationFailureListener.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/listener/Authen0100664 0001751 0001751 00000004244 12471273273 035400 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.event.listener; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.event.AuthenticationFailureEvent; /** * A listener whose authenticationFailed() hook will fire immediately * after a user's authentication attempt fails. Note that this hook cannot * be used to cancel the authentication failure. * * @author Michael Jumper */ public interface AuthenticationFailureListener { /** * Event hook which fires immediately after a user's authentication attempt * fails. * * @param e The AuthenticationFailureEvent describing the authentication * failure that just occurred. * @throws GuacamoleException If an error occurs while handling the * authentication failure event. Note that * throwing an exception will NOT cause the * authentication failure to be canceled. */ void authenticationFailed(AuthenticationFailureEvent e) throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000160 12471273273 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/listener/package-info.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/listener/packag0100664 0001751 0001751 00000002745 12471273273 035406 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides classes for hooking into various events that take place as * users log into and use the Guacamole web application. These event * hooks can be used to take action upon occurrence of an event and, * in some cases, prevent the web application from allowing the * event to continue for the user that triggered it. */ package org.glyptodon.guacamole.net.event.listener; ././@LongLink0100644 0000000 0000000 00000000171 12471273273 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/listener/TunnelConnectListener.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/listener/Tunnel0100664 0001751 0001751 00000004460 12471273273 035421 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.event.listener; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.event.TunnelConnectEvent; /** * A listener whose tunnelConnected() hook will fire immediately after a new * tunnel is connected. * * @author Michael Jumper */ public interface TunnelConnectListener { /** * Event hook which fires immediately after a new tunnel is connected. * The return value of this hook dictates whether the tunnel is made visible * to the session. * * @param e The TunnelConnectEvent describing the tunnel being connected and * any associated credentials. * @return true if the tunnel should be allowed to be connected, or false * if the attempt should be denied, causing the attempt to * effectively fail. * @throws GuacamoleException If an error occurs while handling the * tunnel connect event. Throwing an exception * will also stop the tunnel from being made * visible to the session. */ boolean tunnelConnected(TunnelConnectEvent e) throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000146 12471273273 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/TunnelEvent.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/TunnelEvent.jav0100664 0001751 0001751 00000003070 12471273273 035351 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.event; import org.glyptodon.guacamole.net.GuacamoleTunnel; /** * Abstract basis for events associated with tunnels. * * @author Michael Jumper */ public interface TunnelEvent { /** * Returns the tunnel associated with this event, if any. * * @return The tunnel associated with this event, if any, or null if no * tunnel is associated with this event. */ GuacamoleTunnel getTunnel(); } ././@LongLink0100644 0000000 0000000 00000000165 12471273273 011645 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/AuthenticationSuccessEvent.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/AuthenticationS0100664 0001751 0001751 00000004752 12471273273 035435 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.event; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.UserContext; /** * An event which is triggered whenever a user's credentials pass * authentication. The credentials that passed authentication are included * within this event, and can be retrieved using getCredentials(). * * @author Michael Jumper */ public class AuthenticationSuccessEvent implements UserEvent, CredentialEvent { /** * The UserContext associated with the request that is connecting the * tunnel, if any. */ private UserContext context; /** * The credentials which passed authentication. */ private Credentials credentials; /** * Creates a new AuthenticationSuccessEvent which represents a successful * authentication attempt with the given credentials. * * @param context The UserContext created as a result of successful * authentication. * @param credentials The credentials which passed authentication. */ public AuthenticationSuccessEvent(UserContext context, Credentials credentials) { this.context = context; this.credentials = credentials; } @Override public UserContext getUserContext() { return context; } @Override public Credentials getCredentials() { return credentials; } } ././@LongLink0100644 0000000 0000000 00000000155 12471273273 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/TunnelConnectEvent.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/TunnelConnectEv0100664 0001751 0001751 00000005735 12471273273 035407 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.event; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.UserContext; /** * An event which is triggered whenever a tunnel is being connected. The tunnel * being connected can be accessed through getTunnel(), and the UserContext * associated with the request which is connecting the tunnel can be retrieved * with getUserContext(). * * @author Michael Jumper */ public class TunnelConnectEvent implements UserEvent, CredentialEvent, TunnelEvent { /** * The UserContext associated with the request that is connecting the * tunnel, if any. */ private UserContext context; /** * The credentials associated with the request that is connecting the * tunnel, if any. */ private Credentials credentials; /** * The tunnel being connected. */ private GuacamoleTunnel tunnel; /** * Creates a new TunnelConnectEvent which represents the connecting of the * given tunnel via a request associated with the given credentials. * * @param context The UserContext associated with the request connecting * the tunnel. * @param credentials The credentials associated with the request connecting * the tunnel. * @param tunnel The tunnel being connected. */ public TunnelConnectEvent(UserContext context, Credentials credentials, GuacamoleTunnel tunnel) { this.context = context; this.credentials = credentials; this.tunnel = tunnel; } @Override public UserContext getUserContext() { return context; } @Override public Credentials getCredentials() { return credentials; } @Override public GuacamoleTunnel getTunnel() { return tunnel; } } ././@LongLink0100644 0000000 0000000 00000000152 12471273273 011641 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/CredentialEvent.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/CredentialEvent0100664 0001751 0001751 00000003216 12471273273 035401 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.event; import org.glyptodon.guacamole.net.auth.Credentials; /** * Abstract basis for events which may have associated user credentials when * triggered. * * @author Michael Jumper */ public interface CredentialEvent { /** * Returns the current credentials of the user triggering the event, if any. * * @return The current credentials of the user triggering the event, if * any, or null if no credentials are associated with the event. */ Credentials getCredentials(); } ././@LongLink0100644 0000000 0000000 00000000153 12471273273 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/TunnelCloseEvent.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/TunnelCloseEven0100664 0001751 0001751 00000005701 12471273273 035377 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.event; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.UserContext; /** * An event which is triggered whenever a tunnel is being closed. The tunnel * being closed can be accessed through getTunnel(), and the UserContext * associated with the request which is closing the tunnel can be retrieved * with getUserContext(). * * @author Michael Jumper */ public class TunnelCloseEvent implements UserEvent, CredentialEvent, TunnelEvent { /** * The UserContext associated with the request that is closing the * tunnel, if any. */ private UserContext context; /** * The credentials associated with the request that connected the * tunnel, if any. */ private Credentials credentials; /** * The tunnel being closed. */ private GuacamoleTunnel tunnel; /** * Creates a new TunnelCloseEvent which represents the closing of the * given tunnel via a request associated with the given credentials. * * @param context The UserContext associated with the request closing * the tunnel. * @param credentials The credentials associated with the request that * connected the tunnel. * @param tunnel The tunnel being closed. */ public TunnelCloseEvent(UserContext context, Credentials credentials, GuacamoleTunnel tunnel) { this.context = context; this.credentials = credentials; this.tunnel = tunnel; } @Override public UserContext getUserContext() { return context; } @Override public Credentials getCredentials() { return credentials; } @Override public GuacamoleTunnel getTunnel() { return tunnel; } } ././@LongLink0100644 0000000 0000000 00000000147 12471273273 011645 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/package-info.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/package-info.ja0100664 0001751 0001751 00000002671 12471273273 035246 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides classes for storing information about events that are * triggered when users log into and use the Guacamole web application. * These event classes are most useful when used with hooks implemented * using listener classes. * * @see org.glyptodon.guacamole.net.event.listener */ package org.glyptodon.guacamole.net.event; guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/UserEvent.java0100664 0001751 0001751 00000003205 12471273273 035163 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.event; import org.glyptodon.guacamole.net.auth.UserContext; /** * Abstract basis for events which may have an associated UserContext when * triggered. * * @author Michael Jumper */ public interface UserEvent { /** * Returns the current UserContext of the user triggering the event, if any. * * @return The current UserContext of the user triggering the event, if * any, or null if no UserContext is associated with the event. */ UserContext getUserContext(); } ././@LongLink0100644 0000000 0000000 00000000165 12471273273 011645 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/AuthenticationFailureEvent.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/event/AuthenticationF0100664 0001751 0001751 00000004031 12471273273 035406 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.event; import org.glyptodon.guacamole.net.auth.Credentials; /** * An event which is triggered whenever a user's credentials fail to be * authenticated. The credentials that failed to be authenticated are included * within this event, and can be retrieved using getCredentials(). * * @author Michael Jumper */ public class AuthenticationFailureEvent implements CredentialEvent { /** * The credentials which failed authentication. */ private Credentials credentials; /** * Creates a new AuthenticationFailureEvent which represents the failure * to authenticate the given credentials. * * @param credentials The credentials which failed authentication. */ public AuthenticationFailureEvent(Credentials credentials) { this.credentials = credentials; } @Override public Credentials getCredentials() { return credentials; } } ././@LongLink0100644 0000000 0000000 00000000146 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/AbstractUser.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/AbstractUser.jav0100664 0001751 0001751 00000005264 12612226251 035322 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth; /** * Basic implementation of a Guacamole user which uses the username to * determine equality. Username comparison is case-sensitive. * * @author Michael Jumper */ public abstract class AbstractUser implements User { /** * The name of this user. */ private String username; /** * This user's password. Note that while this provides a means for the * password to be set, the data stored in this String is not necessarily * the user's actual password. It may be hashed, it may be arbitrary. */ private String password; @Override public String getIdentifier() { return username; } @Override public void setIdentifier(String username) { this.username = username; } @Override public String getPassword() { return password; } @Override public void setPassword(String password) { this.password = password; } @Override public int hashCode() { if (username == null) return 0; return username.hashCode(); } @Override public boolean equals(Object obj) { // Not equal if null or not a User if (obj == null) return false; if (!(obj instanceof AbstractUser)) return false; // Get username String objUsername = ((AbstractUser) obj).username; // If null, equal only if this username is null if (objUsername == null) return username == null; // Otherwise, equal only if strings are identical return objUsername.equals(username); } } ././@LongLink0100644 0000000 0000000 00000000172 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleConnectionDirectory.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleCon0100664 0001751 0001751 00000006220 12612226251 035314 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.simple; import java.util.Collection; import java.util.HashMap; import java.util.Map; import org.glyptodon.guacamole.net.auth.Connection; /** * An extremely simple read-only implementation of a Directory of * GuacamoleConfigurations which provides access to a pre-defined Map of * GuacamoleConfigurations. * * @author Michael Jumper */ public class SimpleConnectionDirectory extends SimpleDirectory { /** * The Map of Connections to provide access to. */ private final Map connections = new HashMap(); /** * Creates a new SimpleConnectionDirectory which provides access to the * connections contained within the given Map. * * @param connections * A Collection of all connections that should be present in this * connection directory. */ public SimpleConnectionDirectory(Collection connections) { // Add all given connections for (Connection connection : connections) this.connections.put(connection.getIdentifier(), connection); // Use the connection map to back the underlying directory super.setObjects(this.connections); } /** * An internal method for modifying the Connections in this Directory. * Returns the previous connection for the given identifier, if found. * * @param connection The connection to add or update the Directory with. * @return The previous connection for the connection identifier, if found. */ public Connection putConnection(Connection connection) { return connections.put(connection.getIdentifier(), connection); } /** * An internal method for removing a Connection from this Directory. * @param identifier The identifier of the Connection to remove. * @return The previous connection for the given identifier, if found. */ public Connection removeConnection(String identifier) { return connections.remove(identifier); } } ././@LongLink0100644 0000000 0000000 00000000161 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleConnection.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleCon0100664 0001751 0001751 00000011504 12612226251 035315 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.simple; import java.util.Collections; import java.util.List; import java.util.Map; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.environment.LocalEnvironment; import org.glyptodon.guacamole.net.GuacamoleSocket; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.InetGuacamoleSocket; import org.glyptodon.guacamole.net.SSLGuacamoleSocket; import org.glyptodon.guacamole.net.SimpleGuacamoleTunnel; import org.glyptodon.guacamole.net.auth.AbstractConnection; import org.glyptodon.guacamole.net.auth.ConnectionRecord; import org.glyptodon.guacamole.protocol.ConfiguredGuacamoleSocket; import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; /** * An extremely basic Connection implementation. * * @author Michael Jumper */ public class SimpleConnection extends AbstractConnection { /** * The hostname to use when connecting to guacd if no hostname is provided * within guacamole.properties. */ private static final String DEFAULT_GUACD_HOSTNAME = "localhost"; /** * The port to use when connecting to guacd if no port is provided within * guacamole.properties. */ private static final int DEFAULT_GUACD_PORT = 4822; /** * Backing configuration, containing all sensitive information. */ private GuacamoleConfiguration config; /** * Creates a completely uninitialized SimpleConnection. */ public SimpleConnection() { } /** * Creates a new SimpleConnection having the given identifier and * GuacamoleConfiguration. * * @param name The name to associate with this connection. * @param identifier The identifier to associate with this connection. * @param config The configuration describing how to connect to this * connection. */ public SimpleConnection(String name, String identifier, GuacamoleConfiguration config) { // Set name setName(name); // Set identifier setIdentifier(identifier); // Set config setConfiguration(config); this.config = config; } @Override public int getActiveConnections() { return 0; } @Override public Map getAttributes() { return Collections.emptyMap(); } @Override public void setAttributes(Map attributes) { // Do nothing - there are no attributes } @Override public GuacamoleTunnel connect(GuacamoleClientInformation info) throws GuacamoleException { Environment env = new LocalEnvironment(); // Get guacd connection parameters String hostname = env.getProperty(Environment.GUACD_HOSTNAME, DEFAULT_GUACD_HOSTNAME); int port = env.getProperty(Environment.GUACD_PORT, DEFAULT_GUACD_PORT); GuacamoleSocket socket; // If guacd requires SSL, use it if (env.getProperty(Environment.GUACD_SSL, false)) socket = new ConfiguredGuacamoleSocket( new SSLGuacamoleSocket(hostname, port), config, info ); // Otherwise, just connect directly via TCP else socket = new ConfiguredGuacamoleSocket( new InetGuacamoleSocket(hostname, port), config, info ); return new SimpleGuacamoleTunnel(socket); } @Override public List getHistory() throws GuacamoleException { return Collections.emptyList(); } } ././@LongLink0100644 0000000 0000000 00000000162 12645510255 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleUserContext.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleUse0100664 0001751 0001751 00000017254 12645510255 035350 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.simple; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.UUID; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.form.Form; import org.glyptodon.guacamole.net.auth.ActiveConnection; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.ConnectionRecordSet; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.User; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; /** * An extremely simple UserContext implementation which provides access to * a defined and restricted set of GuacamoleConfigurations. Access to * querying or modifying either users or permissions is denied. * * @author Michael Jumper */ public class SimpleUserContext implements UserContext { /** * The unique identifier of the root connection group. */ private static final String ROOT_IDENTIFIER = "ROOT"; /** * The AuthenticationProvider that created this UserContext. */ private final AuthenticationProvider authProvider; /** * Reference to the user whose permissions dictate the configurations * accessible within this UserContext. */ private final User self; /** * The Directory with access only to the User associated with this * UserContext. */ private final Directory userDirectory; /** * The Directory with access only to the root group associated with this * UserContext. */ private final Directory connectionGroupDirectory; /** * The Directory with access to all connections within the root group * associated with this UserContext. */ private final Directory connectionDirectory; /** * The root connection group. */ private final ConnectionGroup rootGroup; /** * Creates a new SimpleUserContext which provides access to only those * configurations within the given Map. The username is assigned * arbitrarily. * * @param authProvider * The AuthenticationProvider creating this UserContext. * * @param configs * A Map of all configurations for which the user associated with this * UserContext has read access. */ public SimpleUserContext(AuthenticationProvider authProvider, Map configs) { this(authProvider, UUID.randomUUID().toString(), configs); } /** * Creates a new SimpleUserContext for the user with the given username * which provides access to only those configurations within the given Map. * * @param authProvider * The AuthenticationProvider creating this UserContext. * * @param username * The username of the user associated with this UserContext. * * @param configs * A Map of all configurations for which the user associated with * this UserContext has read access. */ public SimpleUserContext(AuthenticationProvider authProvider, String username, Map configs) { Collection connectionIdentifiers = new ArrayList(configs.size()); Collection connectionGroupIdentifiers = Collections.singleton(ROOT_IDENTIFIER); // Produce collection of connections from given configs Collection connections = new ArrayList(configs.size()); for (Map.Entry configEntry : configs.entrySet()) { // Get connection identifier and configuration String identifier = configEntry.getKey(); GuacamoleConfiguration config = configEntry.getValue(); // Add as simple connection Connection connection = new SimpleConnection(identifier, identifier, config); connection.setParentIdentifier(ROOT_IDENTIFIER); connections.add(connection); // Add identifier to overall set of identifiers connectionIdentifiers.add(identifier); } // Add root group that contains only the given configurations this.rootGroup = new SimpleConnectionGroup( ROOT_IDENTIFIER, ROOT_IDENTIFIER, connectionIdentifiers, Collections.emptyList() ); // Build new user from credentials this.self = new SimpleUser(username, connectionIdentifiers, connectionGroupIdentifiers); // Create directories for new user this.userDirectory = new SimpleUserDirectory(self); this.connectionDirectory = new SimpleConnectionDirectory(connections); this.connectionGroupDirectory = new SimpleConnectionGroupDirectory(Collections.singleton(this.rootGroup)); // Associate provided AuthenticationProvider this.authProvider = authProvider; } @Override public User self() { return self; } @Override public AuthenticationProvider getAuthenticationProvider() { return authProvider; } @Override public Directory getUserDirectory() throws GuacamoleException { return userDirectory; } @Override public Directory getConnectionDirectory() throws GuacamoleException { return connectionDirectory; } @Override public Directory getConnectionGroupDirectory() throws GuacamoleException { return connectionGroupDirectory; } @Override public ConnectionGroup getRootConnectionGroup() throws GuacamoleException { return rootGroup; } @Override public Directory getActiveConnectionDirectory() throws GuacamoleException { return new SimpleDirectory(); } @Override public ConnectionRecordSet getConnectionHistory() throws GuacamoleException { return new SimpleConnectionRecordSet(); } @Override public Collection getUserAttributes() { return Collections.emptyList(); } @Override public Collection getConnectionAttributes() { return Collections.emptyList(); } @Override public Collection getConnectionGroupAttributes() { return Collections.emptyList(); } } ././@LongLink0100644 0000000 0000000 00000000164 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleUserDirectory.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleUse0100664 0001751 0001751 00000003355 12612226251 035337 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.simple; import java.util.Collections; import org.glyptodon.guacamole.net.auth.User; /** * An extremely simple read-only implementation of a Directory of Users which * provides access to a single pre-defined User. * * @author Michael Jumper */ public class SimpleUserDirectory extends SimpleDirectory { /** * Creates a new SimpleUserDirectory which provides access to the single * user provided. * * @param user The user to provide access to. */ public SimpleUserDirectory(User user) { super(Collections.singletonMap(user.getIdentifier(), user)); } } ././@LongLink0100644 0000000 0000000 00000000172 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleObjectPermissionSet.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleObj0100664 0001751 0001751 00000011545 12612226251 035315 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.simple; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Set; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; /** * A read-only implementation of ObjectPermissionSet which uses a backing Set * of Permissions to determine which permissions are present. * * @author Michael Jumper */ public class SimpleObjectPermissionSet implements ObjectPermissionSet { /** * The set of all permissions currently granted. */ private Set permissions = Collections.emptySet(); /** * Creates a new empty SimpleObjectPermissionSet. */ public SimpleObjectPermissionSet() { } /** * Creates a new SimpleObjectPermissionSet which contains the permissions * within the given Set. * * @param permissions * The Set of permissions this SimpleObjectPermissionSet should * contain. */ public SimpleObjectPermissionSet(Set permissions) { this.permissions = permissions; } /** * Sets the Set which backs this SimpleObjectPermissionSet. Future function * calls on this SimpleObjectPermissionSet will use the provided Set. * * @param permissions * The Set of permissions this SimpleObjectPermissionSet should * contain. */ protected void setPermissions(Set permissions) { this.permissions = permissions; } @Override public Set getPermissions() { return permissions; } @Override public boolean hasPermission(ObjectPermission.Type permission, String identifier) throws GuacamoleException { ObjectPermission objectPermission = new ObjectPermission(permission, identifier); return permissions.contains(objectPermission); } @Override public void addPermission(ObjectPermission.Type permission, String identifier) throws GuacamoleException { throw new GuacamoleSecurityException("Permission denied."); } @Override public void removePermission(ObjectPermission.Type permission, String identifier) throws GuacamoleException { throw new GuacamoleSecurityException("Permission denied."); } @Override public Collection getAccessibleObjects( Collection permissionTypes, Collection identifiers) throws GuacamoleException { Collection accessibleObjects = new ArrayList(permissions.size()); // For each identifier/permission combination for (String identifier : identifiers) { for (ObjectPermission.Type permissionType : permissionTypes) { // Add identifier if at least one requested permission is granted ObjectPermission permission = new ObjectPermission(permissionType, identifier); if (permissions.contains(permission)) { accessibleObjects.add(identifier); break; } } } return accessibleObjects; } @Override public void addPermissions(Set permissions) throws GuacamoleException { throw new GuacamoleSecurityException("Permission denied."); } @Override public void removePermissions(Set permissions) throws GuacamoleException { throw new GuacamoleSecurityException("Permission denied."); } } ././@LongLink0100644 0000000 0000000 00000000175 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleAuthenticationProvider.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleAut0100664 0001751 0001751 00000023522 12612226251 035332 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.simple; import java.util.Map; import java.util.UUID; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.AbstractAuthenticatedUser; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.AuthenticatedUser; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; import org.glyptodon.guacamole.token.StandardTokens; import org.glyptodon.guacamole.token.TokenFilter; /** * Provides means of retrieving a set of named GuacamoleConfigurations for a * given Credentials object. This is a simple AuthenticationProvider * implementation intended to be easily extended. It is useful for simple * authentication situations where access to web-based administration and * complex users and permissions are not required. * * The interface provided by SimpleAuthenticationProvider is similar to that of * the AuthenticationProvider interface of older Guacamole releases. * * @author Michael Jumper */ public abstract class SimpleAuthenticationProvider implements AuthenticationProvider { /** * Given an arbitrary credentials object, returns a Map containing all * configurations authorized by those credentials. The keys of this Map * are Strings which uniquely identify each configuration. * * @param credentials The credentials to use to retrieve authorized * configurations. * @return A Map of all configurations authorized by the given credentials, * or null if the credentials given are not authorized. * @throws GuacamoleException If an error occurs while retrieving * configurations. */ public abstract Map getAuthorizedConfigurations(Credentials credentials) throws GuacamoleException; /** * AuthenticatedUser which contains its own predefined set of authorized * configurations. * * @author Michael Jumper */ private class SimpleAuthenticatedUser extends AbstractAuthenticatedUser { /** * The credentials provided when this AuthenticatedUser was * authenticated. */ private final Credentials credentials; /** * The GuacamoleConfigurations that this AuthenticatedUser is * authorized to use. */ private final Map configs; /** * Creates a new SimpleAuthenticatedUser associated with the given * credentials and having access to the given Map of * GuacamoleConfigurations. * * @param credentials * The credentials provided by the user when they authenticated. * * @param configs * A Map of all GuacamoleConfigurations for which this user has * access. The keys of this Map are Strings which uniquely identify * each configuration. */ public SimpleAuthenticatedUser(Credentials credentials, Map configs) { // Store credentials and configurations this.credentials = credentials; this.configs = configs; // Pull username from credentials if it exists String username = credentials.getUsername(); if (username != null && !username.isEmpty()) setIdentifier(username); // Otherwise generate a random username else setIdentifier(UUID.randomUUID().toString()); } /** * Returns a Map containing all GuacamoleConfigurations that this user * is authorized to use. The keys of this Map are Strings which * uniquely identify each configuration. * * @return * A Map of all configurations for which this user is authorized. */ public Map getAuthorizedConfigurations() { return configs; } @Override public AuthenticationProvider getAuthenticationProvider() { return SimpleAuthenticationProvider.this; } @Override public Credentials getCredentials() { return credentials; } } /** * Given an arbitrary credentials object, returns a Map containing all * configurations authorized by those credentials, filtering those * configurations using a TokenFilter and the standard credential tokens * (like ${GUAC_USERNAME} and ${GUAC_PASSWORD}). The keys of this Map * are Strings which uniquely identify each configuration. * * @param credentials * The credentials to use to retrieve authorized configurations. * * @return * A Map of all configurations authorized by the given credentials, or * null if the credentials given are not authorized. * * @throws GuacamoleException * If an error occurs while retrieving configurations. */ private Map getFilteredAuthorizedConfigurations(Credentials credentials) throws GuacamoleException { // Get configurations Map configs = getAuthorizedConfigurations(credentials); // Return as unauthorized if not authorized to retrieve configs if (configs == null) return null; // Build credential TokenFilter TokenFilter tokenFilter = new TokenFilter(); StandardTokens.addStandardTokens(tokenFilter, credentials); // Filter each configuration for (GuacamoleConfiguration config : configs.values()) tokenFilter.filterValues(config.getParameters()); return configs; } /** * Given a user who has already been authenticated, returns a Map * containing all configurations for which that user is authorized, * filtering those configurations using a TokenFilter and the standard * credential tokens (like ${GUAC_USERNAME} and ${GUAC_PASSWORD}). The keys * of this Map are Strings which uniquely identify each configuration. * * @param authenticatedUser * The user whose authorized configurations are to be retrieved. * * @return * A Map of all configurations authorized for use by the given user, or * null if the user is not authorized to use any configurations. * * @throws GuacamoleException * If an error occurs while retrieving configurations. */ private Map getFilteredAuthorizedConfigurations(AuthenticatedUser authenticatedUser) throws GuacamoleException { // Pull cached configurations, if any if (authenticatedUser instanceof SimpleAuthenticatedUser && authenticatedUser.getAuthenticationProvider() == this) return ((SimpleAuthenticatedUser) authenticatedUser).getAuthorizedConfigurations(); // Otherwise, pull using credentials return getFilteredAuthorizedConfigurations(authenticatedUser.getCredentials()); } @Override public AuthenticatedUser authenticateUser(final Credentials credentials) throws GuacamoleException { // Get configurations Map configs = getFilteredAuthorizedConfigurations(credentials); // Return as unauthorized if not authorized to retrieve configs if (configs == null) return null; return new SimpleAuthenticatedUser(credentials, configs); } @Override public UserContext getUserContext(AuthenticatedUser authenticatedUser) throws GuacamoleException { // Get configurations Map configs = getFilteredAuthorizedConfigurations(authenticatedUser); // Return as unauthorized if not authorized to retrieve configs if (configs == null) return null; // Return user context restricted to authorized configs return new SimpleUserContext(this, authenticatedUser.getIdentifier(), configs); } @Override public AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser, Credentials credentials) throws GuacamoleException { // Simply return the given user, updating nothing return authenticatedUser; } @Override public UserContext updateUserContext(UserContext context, AuthenticatedUser authorizedUser) throws GuacamoleException { // Simply return the given context, updating nothing return context; } } ././@LongLink0100644 0000000 0000000 00000000166 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleConnectionGroup.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleCon0100664 0001751 0001751 00000010101 12612226251 035305 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.simple; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.AbstractConnectionGroup; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; /** * An extremely simple read-only implementation of a ConnectionGroup which * returns the connection and connection group identifiers it was constructed * with. Load balancing across this connection group is not allowed. * * @author James Muehlner */ public class SimpleConnectionGroup extends AbstractConnectionGroup { /** * The identifiers of all connections in this group. */ private final Set connectionIdentifiers; /** * The identifiers of all connection groups in this group. */ private final Set connectionGroupIdentifiers; /** * Creates a new SimpleConnectionGroup having the given name and identifier * which will expose the given contents. * * @param name * The name to associate with this connection group. * * @param identifier * The identifier to associate with this connection group. * * @param connectionIdentifiers * The connection identifiers to expose when requested. * * @param connectionGroupIdentifiers * The connection group identifiers to expose when requested. */ public SimpleConnectionGroup(String name, String identifier, Collection connectionIdentifiers, Collection connectionGroupIdentifiers) { // Set name setName(name); // Set identifier setIdentifier(identifier); // Set group type setType(ConnectionGroup.Type.ORGANIZATIONAL); // Populate contents this.connectionIdentifiers = new HashSet(connectionIdentifiers); this.connectionGroupIdentifiers = new HashSet(connectionGroupIdentifiers); } @Override public int getActiveConnections() { return 0; } @Override public Set getConnectionIdentifiers() { return connectionIdentifiers; } @Override public Set getConnectionGroupIdentifiers() { return connectionGroupIdentifiers; } @Override public Map getAttributes() { return Collections.emptyMap(); } @Override public void setAttributes(Map attributes) { // Do nothing - there are no attributes } @Override public GuacamoleTunnel connect(GuacamoleClientInformation info) throws GuacamoleException { throw new GuacamoleSecurityException("Permission denied."); } } ././@LongLink0100644 0000000 0000000 00000000153 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleUser.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleUse0100664 0001751 0001751 00000014720 12612226251 035335 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.simple; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.AbstractUser; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; /** * An extremely basic User implementation. * * @author Michael Jumper */ public class SimpleUser extends AbstractUser { /** * All connection permissions granted to this user. */ private final Set userPermissions = new HashSet(); /** * All connection permissions granted to this user. */ private final Set connectionPermissions = new HashSet(); /** * All connection group permissions granted to this user. */ private final Set connectionGroupPermissions = new HashSet(); /** * Creates a completely uninitialized SimpleUser. */ public SimpleUser() { } /** * Creates a new SimpleUser having the given username and no permissions. * * @param username * The username to assign to this SimpleUser. */ public SimpleUser(String username) { // Set username setIdentifier(username); } /** * Adds a new READ permission to the given set of permissions for each of * the given identifiers. * * @param permissions * The set of permissions to add READ permissions to. * * @param identifiers * The identifiers which should each have a corresponding READ * permission added to the given set. */ private void addReadPermissions(Set permissions, Collection identifiers) { // Add a READ permission to the set for each identifier given for (String identifier : identifiers) { permissions.add(new ObjectPermission ( ObjectPermission.Type.READ, identifier )); } } /** * Creates a new SimpleUser having the given username and READ access to * the connections and groups having the given identifiers. * * @param username * The username to assign to this SimpleUser. * * @param connectionIdentifiers * The identifiers of all connections this user has READ access to. * * @param connectionGroupIdentifiers * The identifiers of all connection groups this user has READ access * to. */ public SimpleUser(String username, Collection connectionIdentifiers, Collection connectionGroupIdentifiers) { this(username); // Add permissions addReadPermissions(connectionPermissions, connectionIdentifiers); addReadPermissions(connectionGroupPermissions, connectionGroupIdentifiers); } /** * Creates a new SimpleUser having the given username and READ access to * the users, connections, and groups having the given identifiers. * * @param username * The username to assign to this SimpleUser. * * @param userIdentifiers * The identifiers of all users this user has READ access to. * * @param connectionIdentifiers * The identifiers of all connections this user has READ access to. * * @param connectionGroupIdentifiers * The identifiers of all connection groups this user has READ access * to. */ public SimpleUser(String username, Collection userIdentifiers, Collection connectionIdentifiers, Collection connectionGroupIdentifiers) { this(username); // Add permissions addReadPermissions(userPermissions, userIdentifiers); addReadPermissions(connectionPermissions, connectionIdentifiers); addReadPermissions(connectionGroupPermissions, connectionGroupIdentifiers); } @Override public Map getAttributes() { return Collections.emptyMap(); } @Override public void setAttributes(Map attributes) { // Do nothing - there are no attributes } @Override public SystemPermissionSet getSystemPermissions() throws GuacamoleException { return new SimpleSystemPermissionSet(); } @Override public ObjectPermissionSet getConnectionPermissions() throws GuacamoleException { return new SimpleObjectPermissionSet(connectionPermissions); } @Override public ObjectPermissionSet getConnectionGroupPermissions() throws GuacamoleException { return new SimpleObjectPermissionSet(connectionGroupPermissions); } @Override public ObjectPermissionSet getUserPermissions() throws GuacamoleException { return new SimpleObjectPermissionSet(userPermissions); } @Override public ObjectPermissionSet getActiveConnectionPermissions() throws GuacamoleException { return new SimpleObjectPermissionSet(); } } ././@LongLink0100644 0000000 0000000 00000000177 12612226251 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleConnectionGroupDirectory.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleCon0100664 0001751 0001751 00000006454 12612226251 035325 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.simple; import java.util.Collection; import java.util.HashMap; import java.util.Map; import org.glyptodon.guacamole.net.auth.ConnectionGroup; /** * An extremely simple read-only implementation of a Directory of * ConnectionGroup which provides which provides access to a pre-defined * Collection of ConnectionGroups. * * @author James Muehlner */ public class SimpleConnectionGroupDirectory extends SimpleDirectory { /** * The Map of ConnectionGroups to provide access to. */ private final Map connectionGroups = new HashMap(); /** * Creates a new SimpleConnectionGroupDirectory which contains the given * groups. * * @param groups A Collection of all groups that should be present in this * connection group directory. */ public SimpleConnectionGroupDirectory(Collection groups) { // Add all given groups for (ConnectionGroup group : groups) connectionGroups.put(group.getIdentifier(), group); // Use the connection group map to back the underlying AbstractDirectory super.setObjects(connectionGroups); } /** * An internal method for modifying the ConnectionGroups in this Directory. * Returns the previous connection group for the given identifier, if found. * * @param connectionGroup The connection group to add or update the * Directory with. * @return The previous connection group for the connection group * identifier, if found. */ public ConnectionGroup putConnectionGroup(ConnectionGroup connectionGroup) { return connectionGroups.put(connectionGroup.getIdentifier(), connectionGroup); } /** * An internal method for removing a ConnectionGroup from this Directory. * * @param identifier The identifier of the ConnectionGroup to remove. * @return The previous connection group for the given identifier, if found. */ public ConnectionGroup removeConnectionGroup(String identifier) { return connectionGroups.remove(identifier); } } ././@LongLink0100644 0000000 0000000 00000000155 12471273273 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/package-info.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/package-i0100664 0001751 0001751 00000002511 12471273273 035254 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides a basic AuthenticationProvider base class that can be used to create * simple AuthenticationProviders in the same way allowed by the old * authentication API. */ package org.glyptodon.guacamole.net.auth.simple; ././@LongLink0100644 0000000 0000000 00000000160 12645510255 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleDirectory.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleDir0100664 0001751 0001751 00000011207 12645510255 035322 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.simple; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.Set; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.Identifiable; /** * An extremely simple read-only implementation of a Directory which provides * access to a pre-defined Map of arbitrary objects. Any changes to the Map * will affect the available contents of this SimpleDirectory. * * @author Michael Jumper * @param * The type of objects stored within this SimpleDirectory. */ public class SimpleDirectory implements Directory { /** * The Map of objects to provide access to. */ private Map objects = Collections.emptyMap(); /** * Creates a new empty SimpleDirectory which does not provide access to * any objects. */ public SimpleDirectory() { } /** * Creates a new SimpleDirectory which provides access to the objects * contained within the given Map. * * @param objects * The Map of objects to provide access to. */ public SimpleDirectory(Map objects) { this.objects = objects; } /** * Sets the Map which backs this SimpleDirectory. Future function calls * which retrieve objects from this SimpleDirectory will use the provided * Map. * * @param objects * The Map of objects to provide access to. */ protected void setObjects(Map objects) { this.objects = objects; } /** * Returns the Map which currently backs this SimpleDirectory. Changes to * this Map will affect future function calls that retrieve objects from * this SimpleDirectory. * * @return * The Map of objects which currently backs this SimpleDirectory. */ protected Map getObjects() { return objects; } @Override public ObjectType get(String identifier) throws GuacamoleException { return objects.get(identifier); } @Override public Collection getAll(Collection identifiers) throws GuacamoleException { // Create collection which has an appropriate initial size Collection foundObjects = new ArrayList(identifiers.size()); // Populate collection with matching objects for (String identifier : identifiers) { // Add the object which has the current identifier, if any ObjectType object = objects.get(identifier); if (object != null) foundObjects.add(object); } return foundObjects; } @Override public Set getIdentifiers() throws GuacamoleException { return objects.keySet(); } @Override public void add(ObjectType connection) throws GuacamoleException { throw new GuacamoleSecurityException("Permission denied."); } @Override public void update(ObjectType connection) throws GuacamoleException { throw new GuacamoleSecurityException("Permission denied."); } @Override public void remove(String identifier) throws GuacamoleException { throw new GuacamoleSecurityException("Permission denied."); } } ././@LongLink0100644 0000000 0000000 00000000172 12645510255 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleConnectionRecordSet.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleCon0100664 0001751 0001751 00000004155 12645510255 035327 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.simple; import java.util.Collection; import java.util.Collections; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.ConnectionRecord; import org.glyptodon.guacamole.net.auth.ConnectionRecordSet; /** * An immutable and empty ConnectionRecordSet. * * @author Michael Jumper */ public class SimpleConnectionRecordSet implements ConnectionRecordSet { @Override public Collection asCollection() throws GuacamoleException { return Collections.emptyList(); } @Override public ConnectionRecordSet contains(String value) throws GuacamoleException { return this; } @Override public ConnectionRecordSet limit(int limit) throws GuacamoleException { return this; } @Override public ConnectionRecordSet sort(SortableProperty property, boolean desc) throws GuacamoleException { return this; } } ././@LongLink0100644 0000000 0000000 00000000172 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleSystemPermissionSet.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleSys0100664 0001751 0001751 00000007535 12612226251 035365 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.simple; import java.util.Collections; import java.util.Set; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; /** * A read-only implementation of SystemPermissionSet which uses a backing Set * of Permissions to determine which permissions are present. * * @author Michael Jumper */ public class SimpleSystemPermissionSet implements SystemPermissionSet { /** * The set of all permissions currently granted. */ private Set permissions = Collections.emptySet(); /** * Creates a new empty SimpleSystemPermissionSet. */ public SimpleSystemPermissionSet() { } /** * Creates a new SimpleSystemPermissionSet which contains the permissions * within the given Set. * * @param permissions * The Set of permissions this SimpleSystemPermissionSet should * contain. */ public SimpleSystemPermissionSet(Set permissions) { this.permissions = permissions; } /** * Sets the Set which backs this SimpleSystemPermissionSet. Future function * calls on this SimpleSystemPermissionSet will use the provided Set. * * @param permissions * The Set of permissions this SimpleSystemPermissionSet should * contain. */ protected void setPermissions(Set permissions) { this.permissions = permissions; } @Override public Set getPermissions() { return permissions; } @Override public boolean hasPermission(SystemPermission.Type permission) throws GuacamoleException { SystemPermission systemPermission = new SystemPermission(permission); return permissions.contains(systemPermission); } @Override public void addPermission(SystemPermission.Type permission) throws GuacamoleException { throw new GuacamoleSecurityException("Permission denied."); } @Override public void removePermission(SystemPermission.Type permission) throws GuacamoleException { throw new GuacamoleSecurityException("Permission denied."); } @Override public void addPermissions(Set permissions) throws GuacamoleException { throw new GuacamoleSecurityException("Permission denied."); } @Override public void removePermissions(Set permissions) throws GuacamoleException { throw new GuacamoleSecurityException("Permission denied."); } } ././@LongLink0100644 0000000 0000000 00000000163 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/AbstractAuthenticatedUser.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/AbstractAuthenti0100664 0001751 0001751 00000004443 12612226251 035404 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth; /** * Basic implementation of an AuthenticatedUser which uses the username to * determine equality. Username comparison is case-sensitive. * * @author Michael Jumper */ public abstract class AbstractAuthenticatedUser implements AuthenticatedUser { /** * The name of this user. */ private String username; @Override public String getIdentifier() { return username; } @Override public void setIdentifier(String username) { this.username = username; } @Override public int hashCode() { if (username == null) return 0; return username.hashCode(); } @Override public boolean equals(Object obj) { // Not equal if null or not a User if (obj == null) return false; if (!(obj instanceof AbstractAuthenticatedUser)) return false; // Get username String objUsername = ((AbstractAuthenticatedUser) obj).username; // If null, equal only if this username is null if (objUsername == null) return username == null; // Otherwise, equal only if strings are identical return objUsername.equals(username); } } ././@LongLink0100644 0000000 0000000 00000000145 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/Connectable.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/Connectable.java0100664 0001751 0001751 00000004464 12612226251 035277 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; /** * An object which Guacamole can connect to. * * @author Michael Jumper */ public interface Connectable { /** * Establishes a connection to guacd using the information associated with * this object. The connection will be provided the given client * information. * * @param info * Information associated with the connecting client. * * @return * A fully-established GuacamoleTunnel. * * @throws GuacamoleException * If an error occurs while connecting to guacd, or if permission to * connect is denied. */ public GuacamoleTunnel connect(GuacamoleClientInformation info) throws GuacamoleException; /** * Returns the number of active connections associated with this object. * Implementations may simply return 0 if this value is not tracked. * * @return * The number of active connections associated with this object. */ public int getActiveConnections(); } ././@LongLink0100644 0000000 0000000 00000000161 12471273273 011641 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/AbstractConnectionGroup.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/AbstractConnecti0100664 0001751 0001751 00000006301 12471273273 035371 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth; /** * Basic implementation of a Guacamole connection group. * * @author James Muehlner */ public abstract class AbstractConnectionGroup implements ConnectionGroup { /** * The name associated with this connection group. */ private String name; /** * The unique identifier associated with this connection group. */ private String identifier; /** * The unique identifier of the parent connection group for * this connection group. */ private String parentIdentifier; /** * The type of this connection group. */ private ConnectionGroup.Type type; @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } @Override public String getIdentifier() { return identifier; } @Override public void setIdentifier(String identifier) { this.identifier = identifier; } @Override public String getParentIdentifier() { return parentIdentifier; } @Override public void setParentIdentifier(String parentIdentifier) { this.parentIdentifier = parentIdentifier; } @Override public ConnectionGroup.Type getType() { return type; } @Override public void setType(ConnectionGroup.Type type) { this.type = type; } @Override public int hashCode() { if (identifier == null) return 0; return identifier.hashCode(); } @Override public boolean equals(Object obj) { // Not equal if null or not a ConnectionGroup if (obj == null) return false; if (!(obj instanceof AbstractConnectionGroup)) return false; // Get identifier String objIdentifier = ((AbstractConnectionGroup) obj).identifier; // If null, equal only if this identifier is null if (objIdentifier == null) return identifier == null; // Otherwise, equal only if strings are identical return objIdentifier.equals(identifier); } } ././@LongLink0100644 0000000 0000000 00000000160 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/AuthenticationProvider.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/AuthenticationPr0100664 0001751 0001751 00000014303 12612226251 035414 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth; import org.glyptodon.guacamole.GuacamoleException; /** * Provides means of authorizing users and for accessing and managing data * associated with those users. Access to such data is limited according to the * AuthenticationProvider implementation. * * @author Michael Jumper */ public interface AuthenticationProvider { /** * Returns the identifier which uniquely and consistently identifies this * AuthenticationProvider implementation. This identifier may not be null * and must be unique across all AuthenticationProviders loaded by the * Guacamole web application. * * @return * The unique identifier assigned to this AuthenticationProvider, which * may not be null. */ String getIdentifier(); /** * Returns an AuthenticatedUser representing the user authenticated by the * given credentials, if any. * * @param credentials * The credentials to use for authentication. * * @return * An AuthenticatedUser representing the user authenticated by the * given credentials, if any, or null if the credentials are invalid. * * @throws GuacamoleException * If an error occurs while authenticating the user, or if access is * temporarily, permanently, or conditionally denied, such as if the * supplied credentials are insufficient or invalid. */ AuthenticatedUser authenticateUser(Credentials credentials) throws GuacamoleException; /** * Returns a new or updated AuthenticatedUser for the given credentials * already having produced the given AuthenticatedUser. Note that because * this function will be called for all future requests after initial * authentication, including tunnel requests, care must be taken to avoid * using functions of HttpServletRequest which invalidate the entire request * body, such as getParameter(). Doing otherwise may cause the * GuacamoleHTTPTunnelServlet to fail. * * @param credentials * The credentials to use for authentication. * * @param authenticatedUser * An AuthenticatedUser object representing the user authenticated by * an arbitrary set of credentials. The AuthenticatedUser may come from * this AuthenticationProvider or any other installed * AuthenticationProvider. * * @return * An updated AuthenticatedUser representing the user authenticated by * the given credentials, if any, or null if the credentials are * invalid. * * @throws GuacamoleException * If an error occurs while updating the AuthenticatedUser. */ AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser, Credentials credentials) throws GuacamoleException; /** * Returns the UserContext of the user authenticated by the given * credentials. * * @param authenticatedUser * An AuthenticatedUser object representing the user authenticated by * an arbitrary set of credentials. The AuthenticatedUser may come from * this AuthenticationProvider or any other installed * AuthenticationProvider. * * @return * A UserContext describing the permissions, connection, connection * groups, etc. accessible or associated with the given authenticated * user, or null if this AuthenticationProvider refuses to provide any * such data. * * @throws GuacamoleException * If an error occurs while creating the UserContext. */ UserContext getUserContext(AuthenticatedUser authenticatedUser) throws GuacamoleException; /** * Returns a new or updated UserContext for the given AuthenticatedUser * already having the given UserContext. Note that because this function * will be called for all future requests after initial authentication, * including tunnel requests, care must be taken to avoid using functions * of HttpServletRequest which invalidate the entire request body, such as * getParameter(). Doing otherwise may cause the GuacamoleHTTPTunnelServlet * to fail. * * @param context * The existing UserContext belonging to the user in question. * * @param authenticatedUser * An AuthenticatedUser object representing the user authenticated by * an arbitrary set of credentials. The AuthenticatedUser may come from * this AuthenticationProvider or any other installed * AuthenticationProvider. * * @return * An updated UserContext describing the permissions, connection, * connection groups, etc. accessible or associated with the given * authenticated user, or null if this AuthenticationProvider refuses * to provide any such data. * * @throws GuacamoleException * If an error occurs while updating the UserContext. */ UserContext updateUserContext(UserContext context, AuthenticatedUser authenticatedUser) throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000145 12566525611 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/Credentials.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/Credentials.java0100664 0001751 0001751 00000010663 12566525611 035327 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth; import java.io.Serializable; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; /** * Simple arbitrary set of credentials, including a username/password pair, * the HttpServletRequest associated with the request for authorization * (if any) and the HttpSession associated with that request. * * This class is used along with AuthenticationProvider to provide arbitrary * HTTP-based authentication for Guacamole. * * @author Michael Jumper */ public class Credentials implements Serializable { /** * Unique identifier associated with this specific version of Credentials. */ private static final long serialVersionUID = 1L; /** * An arbitrary username. */ private String username; /** * An arbitrary password. */ private String password; /** * The HttpServletRequest carrying additional credentials, if any. */ private transient HttpServletRequest request; /** * The HttpSession carrying additional credentials, if any. */ private transient HttpSession session; /** * Returns the password associated with this set of credentials. * @return The password associated with this username/password pair, or * null if no password has been set. */ public String getPassword() { return password; } /** * Sets the password associated with this set of credentials. * @param password The password to associate with this username/password * pair. */ public void setPassword(String password) { this.password = password; } /** * Returns the username associated with this set of credentials. * @return The username associated with this username/password pair, or * null if no username has been set. */ public String getUsername() { return username; } /** * Sets the username associated with this set of credentials. * @param username The username to associate with this username/password * pair. */ public void setUsername(String username) { this.username = username; } /** * Returns the HttpServletRequest associated with this set of credentials. * @return The HttpServletRequest associated with this set of credentials, * or null if no such request exists. */ public HttpServletRequest getRequest() { return request; } /** * Sets the HttpServletRequest associated with this set of credentials. * @param request The HttpServletRequest to associated with this set of * credentials. */ public void setRequest(HttpServletRequest request) { this.request = request; } /** * Returns the HttpSession associated with this set of credentials. * @return The HttpSession associated with this set of credentials, or null * if no such request exists. */ public HttpSession getSession() { return session; } /** * Sets the HttpSession associated with this set of credentials. * @param session The HttpSession to associated with this set of * credentials. */ public void setSession(HttpSession session) { this.session = session; } } ././@LongLink0100644 0000000 0000000 00000000151 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/ConnectionGroup.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/ConnectionGroup.0100664 0001751 0001751 00000011631 12612226251 035326 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth; import java.util.Map; import java.util.Set; import org.glyptodon.guacamole.GuacamoleException; /** * Represents a connection group, which can contain both other connection groups * as well as connections. * * @author James Muehlner */ public interface ConnectionGroup extends Identifiable, Connectable { /** * All legal types of connection group. */ public enum Type { /** * A connection group that purely organizes other connections or * connection groups, serving only as a container. An organizational * connection group is analogous to a directory or folder in a * filesystem. */ ORGANIZATIONAL, /** * A connection group that acts as a load balancer. A balancing * connection group can be connected to in the same manner as a * connection, and will transparently route to the least-used * underlying connection. */ BALANCING }; /** * Returns the name assigned to this ConnectionGroup. * @return The name assigned to this ConnectionGroup. */ public String getName(); /** * Sets the name assigned to this ConnectionGroup. * * @param name The name to assign. */ public void setName(String name); /** * Returns the unique identifier of the parent ConnectionGroup for * this ConnectionGroup. * * @return The unique identifier of the parent ConnectionGroup for * this ConnectionGroup. */ public String getParentIdentifier(); /** * Sets the unique identifier of the parent ConnectionGroup for * this ConnectionGroup. * * @param parentIdentifier The unique identifier of the parent * ConnectionGroup for this ConnectionGroup. */ public void setParentIdentifier(String parentIdentifier); /** * Set the type of this ConnectionGroup. * * @param type The type of this ConnectionGroup. */ public void setType(Type type); /** * Returns the type of this connection. * @return the type of this connection. */ public Type getType(); /** * Returns the identifiers of all readable connections that are children * of this connection group. * * @return * The set of identifiers of all readable connections that are children * of this connection group. * * @throws GuacamoleException * If an error occurs while retrieving the identifiers. */ public Set getConnectionIdentifiers() throws GuacamoleException; /** * Returns the identifiers of all readable connection groups that are * children of this connection group. * * @return * The set of identifiers of all readable connection groups that are * children of this connection group. * * @throws GuacamoleException * If an error occurs while retrieving the identifiers. */ public Set getConnectionGroupIdentifiers() throws GuacamoleException; /** * Returns all attributes associated with this connection group. The * returned map may not be modifiable. * * @return * A map of all attribute identifiers to their corresponding values, * for all attributes associated with this connection group, which may * not be modifiable. */ Map getAttributes(); /** * Sets the given attributes. If an attribute within the map is not * supported, it will simply be dropped. Any attributes not within the * given map will be left untouched. * * @param attributes * A map of all attribute identifiers to their corresponding values. */ void setAttributes(Map attributes); } ././@LongLink0100644 0000000 0000000 00000000153 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/AuthenticatedUser.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/AuthenticatedUse0100664 0001751 0001751 00000003467 12612226251 035403 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth; /** * A user of the Guacamole web application who has been authenticated by an * AuthenticationProvider. * * @author Michael Jumper */ public interface AuthenticatedUser extends Identifiable { /** * Returns the AuthenticationProvider that authenticated this user. * * @return * The AuthenticationProvider that authenticated this user. */ AuthenticationProvider getAuthenticationProvider(); /** * Returns the credentials that the user provided when they successfully * authenticated. * * @return * The credentials provided by the user when they authenticated. */ Credentials getCredentials(); } ././@LongLink0100644 0000000 0000000 00000000154 12471273273 011643 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/AbstractConnection.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/AbstractConnecti0100664 0001751 0001751 00000006436 12471273273 035402 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; /** * Basic implementation of a Guacamole connection. * * @author Michael Jumper */ public abstract class AbstractConnection implements Connection { /** * The name associated with this connection. */ private String name; /** * The unique identifier associated with this connection. */ private String identifier; /** * The unique identifier of the parent ConnectionGroup for * this Connection. */ private String parentIdentifier; /** * The GuacamoleConfiguration associated with this connection. */ private GuacamoleConfiguration configuration; @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } @Override public String getIdentifier() { return identifier; } @Override public void setIdentifier(String identifier) { this.identifier = identifier; } @Override public String getParentIdentifier() { return parentIdentifier; } @Override public void setParentIdentifier(String parentIdentifier) { this.parentIdentifier = parentIdentifier; } @Override public GuacamoleConfiguration getConfiguration() { return configuration; } @Override public void setConfiguration(GuacamoleConfiguration configuration) { this.configuration = configuration; } @Override public int hashCode() { if (identifier == null) return 0; return identifier.hashCode(); } @Override public boolean equals(Object obj) { // Not equal if null or not a Connection if (obj == null) return false; if (!(obj instanceof AbstractConnection)) return false; // Get identifier String objIdentifier = ((AbstractConnection) obj).identifier; // If null, equal only if this identifier is null if (objIdentifier == null) return identifier == null; // Otherwise, equal only if strings are identical return objIdentifier.equals(identifier); } } ././@LongLink0100644 0000000 0000000 00000000146 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/Identifiable.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/Identifiable.jav0100664 0001751 0001751 00000003465 12612226251 035300 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth; /** * An object which has a deterministic, unique identifier, which may not be * null. * * @author Michael Jumper */ public interface Identifiable { /** * Returns the unique identifier assigned to this object. All identifiable * objects must have a deterministic, unique identifier which may not be * null. * * @return * The unique identifier assigned to this object, which may not be * null. */ public String getIdentifier(); /** * Sets the identifier assigned to this object. * * @param identifier * The identifier to assign. */ public void setIdentifier(String identifier); } guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/Connection.java0100664 0001751 0001751 00000011106 12612226251 035150 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth; import java.util.List; import java.util.Map; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; /** * Represents a pairing of a GuacamoleConfiguration with a unique, * human-readable identifier, and abstracts the connection process. The * backing GuacamoleConfiguration may be intentionally obfuscated or tokenized * to protect sensitive configuration information. * * @author Michael Jumper */ public interface Connection extends Identifiable, Connectable { /** * Returns the name assigned to this Connection. * @return The name assigned to this Connection. */ public String getName(); /** * Sets the name assigned to this Connection. * * @param name The name to assign. */ public void setName(String name); /** * Returns the unique identifier of the parent ConnectionGroup for * this Connection. * * @return The unique identifier of the parent ConnectionGroup for * this Connection. */ public String getParentIdentifier(); /** * Sets the unique identifier of the parent ConnectionGroup for * this Connection. * * @param parentIdentifier The unique identifier of the parent * ConnectionGroup for this Connection. */ public void setParentIdentifier(String parentIdentifier); /** * Returns the GuacamoleConfiguration associated with this Connection. Note * that because configurations may contain sensitive information, some data * in this configuration may be omitted or tokenized. * * @return The GuacamoleConfiguration associated with this Connection. */ public GuacamoleConfiguration getConfiguration(); /** * Sets the GuacamoleConfiguration associated with this Connection. * * @param config The GuacamoleConfiguration to associate with this * Connection. */ public void setConfiguration(GuacamoleConfiguration config); /** * Returns all attributes associated with this connection. The returned map * may not be modifiable. * * @return * A map of all attribute identifiers to their corresponding values, * for all attributes associated with this connection, which may not be * modifiable. */ Map getAttributes(); /** * Sets the given attributes. If an attribute within the map is not * supported, it will simply be dropped. Any attributes not within the * given map will be left untouched. * * @param attributes * A map of all attribute identifiers to their corresponding values. */ void setAttributes(Map attributes); /** * Returns a list of ConnectionRecords representing the usage history * of this Connection, including any active users. ConnectionRecords * in this list will be sorted in descending order of end time (active * connections are first), and then in descending order of start time * (newer connections are first). * * @return A list of ConnectionRecrods representing the usage history * of this Connection. * * @throws GuacamoleException If an error occurs while reading the history * of this connection, or if permission is * denied. */ public List getHistory() throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000162 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/permission/PermissionSet.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/permission/Permi0100664 0001751 0001751 00000005660 12612226251 035405 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.permission; import java.util.Set; import org.glyptodon.guacamole.GuacamoleException; /** * An arbitrary set of permissions. * * @author Michael Jumper * @param * The type of permission stored within this PermissionSet. */ public interface PermissionSet { /** * Returns a Set which contains all permissions granted within this * permission set. * * @return * A Set containing all permissions granted within this permission set. * * @throws GuacamoleException * If an error occurs while retrieving permissions, or if permissions * cannot be retrieved due to lack of permissions to do so. */ Set getPermissions() throws GuacamoleException; /** * Adds the specified permissions, if not already granted. If a specified * permission is already granted, no operation is performed regarding that * permission. * * @param permissions * The permissions to add. * * @throws GuacamoleException * If an error occurs while adding the permissions, or if permission to * add permissions is denied. */ void addPermissions(Set permissions) throws GuacamoleException; /** * Removes each of the specified permissions, if granted. If a specified * permission is not granted, no operation is performed regarding that * permission. * * @param permissions * The permissions to remove. * * @throws GuacamoleException * If an error occurs while removing the permissions, or if permission * to remove permissions is denied. */ void removePermissions(Set permissions) throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000170 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/permission/ObjectPermissionSet.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/permission/Objec0100664 0001751 0001751 00000011354 12612226251 035350 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.permission; import java.util.Collection; import java.util.Set; import org.glyptodon.guacamole.GuacamoleException; /** * A set of permissions which affect arbitrary objects, where each object has * an associated unique identifier. * * @author Michael Jumper */ public interface ObjectPermissionSet extends PermissionSet { /** * Tests whether the permission of the given type is granted for the * object having the given identifier. * * @param permission * The permission to check. * * @param identifier * The identifier of the object affected by the permission being * checked. * * @return * true if the permission is granted, false otherwise. * * @throws GuacamoleException * If an error occurs while checking permissions, or if permissions * cannot be checked due to lack of permissions to do so. */ boolean hasPermission(ObjectPermission.Type permission, String identifier) throws GuacamoleException; /** * Adds the specified permission for the object having the given * identifier. * * @param permission * The permission to add. * * @param identifier * The identifier of the object affected by the permission being * added. * * @throws GuacamoleException * If an error occurs while adding the permission, or if permission to * add permissions is denied. */ void addPermission(ObjectPermission.Type permission, String identifier) throws GuacamoleException; /** * Removes the specified permission for the object having the given * identifier. * * @param permission * The permission to remove. * * @param identifier * The identifier of the object affected by the permission being * added. * * @throws GuacamoleException * If an error occurs while removing the permission, or if permission * to remove permissions is denied. */ void removePermission(ObjectPermission.Type permission, String identifier) throws GuacamoleException; /** * Tests whether this user has the specified permissions for the objects * having the given identifiers. The identifier of an object is returned * in a new collection if at least one of the specified permissions is * granted for that object. * * @param permissions * The permissions to check. An identifier will be included in the * resulting collection if at least one of these permissions is granted * for the associated object * * @param identifiers * The identifiers of the objects affected by the permissions being * checked. * * @return * A collection containing the subset of identifiers for which at least * one of the specified permissions is granted. * * @throws GuacamoleException * If an error occurs while checking permissions, or if permissions * cannot be checked due to lack of permissions to do so. */ Collection getAccessibleObjects( Collection permissions, Collection identifiers) throws GuacamoleException; @Override Set getPermissions() throws GuacamoleException; @Override void addPermissions(Set permissions) throws GuacamoleException; @Override void removePermissions(Set permissions) throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000170 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/permission/SystemPermissionSet.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/permission/Syste0100664 0001751 0001751 00000005720 12612226251 035435 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.permission; import java.util.Set; import org.glyptodon.guacamole.GuacamoleException; /** * A set of permissions which affects the system as a whole. * * @author Michael Jumper */ public interface SystemPermissionSet extends PermissionSet { /** * Tests whether the permission of the given type is granted. * * @param permission * The permission to check. * * @return * true if the permission is granted, false otherwise. * * @throws GuacamoleException * If an error occurs while checking permissions, or if permissions * cannot be checked due to lack of permissions to do so. */ boolean hasPermission(SystemPermission.Type permission) throws GuacamoleException; /** * Adds the specified permission. * * @param permission * The permission to add. * * @throws GuacamoleException * If an error occurs while adding the permission, or if permission to * add permissions is denied. */ void addPermission(SystemPermission.Type permission) throws GuacamoleException; /** * Removes the specified permission. * * @param permission * The permission to remove. * * @throws GuacamoleException * If an error occurs while removing the permission, or if permission * to remove permissions is denied. */ void removePermission(SystemPermission.Type permission) throws GuacamoleException; @Override Set getPermissions() throws GuacamoleException; @Override void addPermissions(Set permissions) throws GuacamoleException; @Override void removePermissions(Set permissions) throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000161 12471273273 011641 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/permission/package-info.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/permission/packa0100664 0001751 0001751 00000002377 12471273273 035423 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides classes which describe the various permissions a Guacamole user * can be granted. */ package org.glyptodon.guacamole.net.auth.permission; ././@LongLink0100644 0000000 0000000 00000000165 12471273273 011645 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/permission/SystemPermission.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/permission/Syste0100664 0001751 0001751 00000005531 12471273273 035446 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.permission; /** * A permission which affects the system as a whole, rather than an individual * object. * * @author Michael Jumper */ public class SystemPermission implements Permission { /** * Specific types of system-level permissions. Each permission type is * related to a specific class of system-level operation. */ public enum Type { /** * Create users. */ CREATE_USER, /** * Create connections. */ CREATE_CONNECTION, /** * Create connection groups. */ CREATE_CONNECTION_GROUP, /** * Administer the system in general, including adding permissions * which affect the system (like user creation, connection creation, * and system administration). */ ADMINISTER } /** * The type of operation affected by this permission. */ private Type type; /** * Creates a new SystemPermission with the given * type. * * @param type The type of operation controlled by this permission. */ public SystemPermission(Type type) { this.type = type; } @Override public Type getType() { return type; } @Override public int hashCode() { return type.hashCode(); } @Override public boolean equals(Object obj) { // Not equal if null or wrong type if (obj == null) return false; if (getClass() != obj.getClass()) return false; final SystemPermission other = (SystemPermission) obj; // Compare types if (type != other.type) return false; return true; } } ././@LongLink0100644 0000000 0000000 00000000157 12471273273 011646 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/permission/Permission.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/permission/Permi0100664 0001751 0001751 00000003230 12471273273 035405 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.permission; /** * A permission which affects a specific type of operation, where all available * operation types are defined by an enumeration. * * @author Michael Jumper * @param The enumeration of all available operation types that this * permission can affect. */ public interface Permission { /** * Returns the type of operation affected by this permission. * @return The type of operation affected by this permission. */ public Type getType(); } ././@LongLink0100644 0000000 0000000 00000000165 12612226251 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/permission/ObjectPermission.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/permission/Objec0100664 0001751 0001751 00000007655 12612226251 035361 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.permission; /** * A permission which affects a specific object, rather than the system as a * whole. * * @author Michael Jumper */ public class ObjectPermission implements Permission { /** * Specific types of object-level permissions. Each permission type is * related to a specific class of object-level operation. */ public enum Type { /** * Read data within an object. */ READ, /** * Update data within an object. */ UPDATE, /** * Delete an object. */ DELETE, /** * Change who has access to an object. */ ADMINISTER } /** * The identifier of the GuacamoleConfiguration associated with the * operation affected by this permission. */ private final String identifier; /** * The type of operation affected by this permission. */ private final Type type; /** * Creates a new ObjectPermission having the given type and identifier. * The identifier must be the unique identifier assigned to the object * associated with this permission by the AuthenticationProvider in use. * * @param type * The type of operation affected by this permission. * * @param identifier * The identifier of the object associated with the operation affected * by this permission. */ public ObjectPermission(Type type, String identifier) { this.identifier = identifier; this.type = type; } /** * Returns the identifier of the specific object affected by this * permission. * * @return The identifier of the specific object affected by this * permission. */ public String getObjectIdentifier() { return identifier; } @Override public Type getType() { return type; } @Override public int hashCode() { int hash = 5; if (identifier != null) hash = 47 * hash + identifier.hashCode(); if (type != null) hash = 47 * hash + type.hashCode(); return hash; } @Override public boolean equals(Object obj) { // Not equal if null or wrong type if (obj == null) return false; if (getClass() != obj.getClass()) return false; final ObjectPermission other = (ObjectPermission) obj; // Not equal if different type if (this.type != other.type) return false; // If null identifier, equality depends on whether other identifier // is null if (identifier == null) return other.identifier == null; // Otherwise, equality depends entirely on identifier return identifier.equals(other.identifier); } } guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/Directory.java0100664 0001751 0001751 00000012241 12645510255 035024 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth; import java.util.Collection; import java.util.Set; import org.glyptodon.guacamole.GuacamoleException; /** * Provides access to a collection of all objects with associated identifiers, * and allows user manipulation and removal. Objects returned by a Directory * are not necessarily backed by the stored objects, thus updating an object * always requires calling the update() function. * * @author Michael Jumper * @param * The type of objects stored within this Directory. */ public interface Directory { /** * Returns the object having the given identifier. Note that changes to * the object returned will not necessarily affect the object stored within * the Directory. To update an object stored within an * Directory such that future calls to get() will return the updated * object, you must call update() on the object after modification. * * @param identifier The identifier to use when locating the object to * return. * @return The object having the given identifier, or null if no such object * exists. * * @throws GuacamoleException If an error occurs while retrieving the * object, or if permission for retrieving the * object is denied. */ ObjectType get(String identifier) throws GuacamoleException; /** * Returns the objects having the given identifiers. Note that changes to * any object returned will not necessarily affect the object stored within * the Directory. To update an object stored within a * Directory such that future calls to get() will return the updated * object, you must call update() on the object after modification. * * @param identifiers * The identifiers to use when locating the objects to return. * * @return * The objects having the given identifiers. If any identifiers do not * correspond to accessible objects, those identifiers will be ignored. * If no objects correspond to any of the given identifiers, the * returned collection will be empty. * * @throws GuacamoleException * If an error occurs while retrieving the objects, or if permission * to retrieve the requested objects is denied. */ Collection getAll(Collection identifiers) throws GuacamoleException; /** * Returns a Set containing all identifiers for all objects within this * Directory. * * @return A Set of all identifiers. * @throws GuacamoleException If an error occurs while retrieving * the identifiers. */ Set getIdentifiers() throws GuacamoleException; /** * Adds the given object to the overall set. If a new identifier is * created for the added object, that identifier will be automatically * assigned via setIdentifier(). * * @param object * The object to add. * * @throws GuacamoleException * If an error occurs while adding the object, or if adding the object * is not allowed. */ void add(ObjectType object) throws GuacamoleException; /** * Updates the stored object with the data contained in the given object. * * @param object The object which will supply the data for the update. * * @throws GuacamoleException If an error occurs while updating the object, * or if updating the object is not allowed. */ void update(ObjectType object) throws GuacamoleException; /** * Removes the object with the given identifier from the overall set. * * @param identifier The identifier of the object to remove. * * @throws GuacamoleException If an error occurs while removing the object, * or if removing object is not allowed. */ void remove(String identifier) throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000152 12645510255 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/ConnectionRecord.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/ConnectionRecord0100664 0001751 0001751 00000006263 12645510255 035405 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth; import java.util.Date; /** * A logging record describing when a user started and ended usage of a * particular connection. * * @author Michael Jumper */ public interface ConnectionRecord { /** * Returns the identifier of the connection associated with this * connection record. * * @return * The identifier of the connection associated with this connection * record. */ public String getConnectionIdentifier(); /** * Returns the name of the connection associated with this connection * record. * * @return * The name of the connection associated with this connection record. */ public String getConnectionName(); /** * Returns the date and time the connection began. * * @return The date and time the connection began. */ public Date getStartDate(); /** * Returns the date and time the connection ended, if applicable. * * @return The date and time the connection ended, or null if the * connection is still running or if the end time is unknown. */ public Date getEndDate(); /** * Returns the hostname or IP address of the remote host that used the * connection associated with this record, if known. If the hostname or IP * address is not known, null is returned. * * @return * The hostname or IP address of the remote host, or null if this * information is not available. */ public String getRemoteHost(); /** * Returns the name of the user who used or is using the connection at the * times given by this connection record. * * @return The name of the user who used or is using the associated * connection. */ public String getUsername(); /** * Returns whether the connection associated with this record is still * active. * * @return true if the connection associated with this record is still * active, false otherwise. */ public boolean isActive(); } ././@LongLink0100644 0000000 0000000 00000000146 12471273273 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/package-info.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/package-info.jav0100664 0001751 0001751 00000002426 12471273273 035252 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides classes which can be used to extend or replace the authentication * functionality of the Guacamole web application. */ package org.glyptodon.guacamole.net.auth; guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/User.java0100664 0001751 0001751 00000011711 12612226251 033771 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth; import java.util.Map; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; /** * A user of the Guacamole web application. * * @author Michael Jumper */ public interface User extends Identifiable { /** * Returns this user's password. Note that the password returned may be * hashed or completely arbitrary. * * @return A String which may (or may not) be the user's password. */ public String getPassword(); /** * Sets this user's password. Note that while this function is guaranteed * to change the password of this User object, there is no guarantee that * getPassword() will return the value given to setPassword(). * * @param password The password to set. */ public void setPassword(String password); /** * Returns all attributes associated with this user. The returned map may * not be modifiable. * * @return * A map of all attribute identifiers to their corresponding values, * for all attributes associated with this user, which may not be * modifiable. */ Map getAttributes(); /** * Sets the given attributes. If an attribute within the map is not * supported, it will simply be dropped. Any attributes not within the * given map will be left untouched. * * @param attributes * A map of all attribute identifiers to their corresponding values. */ void setAttributes(Map attributes); /** * Returns all system-level permissions given to this user. * * @return * A SystemPermissionSet of all system-level permissions granted to * this user. * * @throws GuacamoleException * If an error occurs while retrieving permissions, or if reading all * permissions is not allowed. */ SystemPermissionSet getSystemPermissions() throws GuacamoleException; /** * Returns all connection permissions given to this user. * * @return * An ObjectPermissionSet of all connection permissions granted to this * user. * * @throws GuacamoleException * If an error occurs while retrieving permissions, or if reading all * permissions is not allowed. */ ObjectPermissionSet getConnectionPermissions() throws GuacamoleException; /** * Returns all connection group permissions given to this user. * * @return * An ObjectPermissionSet of all connection group permissions granted * to this user. * * @throws GuacamoleException * If an error occurs while retrieving permissions, or if reading all * permissions is not allowed. */ ObjectPermissionSet getConnectionGroupPermissions() throws GuacamoleException; /** * Returns all permissions given to this user regarding currently-active * connections. * * @return * An ObjectPermissionSet of all active connection permissions granted * to this user. * * @throws GuacamoleException * If an error occurs while retrieving permissions, or if reading all * permissions is not allowed. */ ObjectPermissionSet getActiveConnectionPermissions() throws GuacamoleException; /** * Returns all user permissions given to this user. * * @return * An ObjectPermissionSet of all user permissions granted to this user. * * @throws GuacamoleException * If an error occurs while retrieving permissions, or if reading all * permissions is not allowed. */ ObjectPermissionSet getUserPermissions() throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000145 12645510255 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/UserContext.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/UserContext.java0100664 0001751 0001751 00000014502 12645510255 035345 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth; import java.util.Collection; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.form.Form; /** * The context of an active user. The functions of this class enforce all * permissions and act only within the rights of the associated user. * * @author Michael Jumper */ public interface UserContext { /** * Returns the User whose access rights control the operations of this * UserContext. * * @return The User whose access rights control the operations of this * UserContext. */ User self(); /** * Returns the AuthenticationProvider which created this UserContext, which * may not be the same AuthenticationProvider that authenticated the user * associated with this UserContext. * * @return * The AuthenticationProvider that created this UserContext. */ AuthenticationProvider getAuthenticationProvider(); /** * Retrieves a Directory which can be used to view and manipulate other * users, but only as allowed by the permissions given to the user of this * UserContext. * * @return A Directory whose operations are bound by the restrictions * of this UserContext. * * @throws GuacamoleException If an error occurs while creating the * Directory. */ Directory getUserDirectory() throws GuacamoleException; /** * Retrieves a Directory which can be used to view and manipulate * connections and their configurations, but only as allowed by the * permissions given to the user. * * @return A Directory whose operations are bound by the permissions of * the user. * * @throws GuacamoleException If an error occurs while creating the * Directory. */ Directory getConnectionDirectory() throws GuacamoleException; /** * Retrieves a Directory which can be used to view and manipulate * connection groups and their members, but only as allowed by the * permissions given to the user. * * @return A Directory whose operations are bound by the permissions of * the user. * * @throws GuacamoleException If an error occurs while creating the * Directory. */ Directory getConnectionGroupDirectory() throws GuacamoleException; /** * Retrieves a Directory which can be used to view and manipulate * active connections, but only as allowed by the permissions given to the * user. * * @return * A Directory whose operations are bound by the permissions of the * user. * * @throws GuacamoleException * If an error occurs while creating the Directory. */ Directory getActiveConnectionDirectory() throws GuacamoleException; /** * Retrieves all connection records visible to current user. The resulting * set of connection records can be further filtered and ordered using the * methods defined on ConnectionRecordSet. * * @return * A set of all connection records visible to the current user. * * @throws GuacamoleException * If an error occurs while retrieving the connection records. */ ConnectionRecordSet getConnectionHistory() throws GuacamoleException; /** * Retrieves a connection group which can be used to view and manipulate * connections, but only as allowed by the permissions given to the user of * this UserContext. * * @return A connection group whose operations are bound by the restrictions * of this UserContext. * * @throws GuacamoleException If an error occurs while creating the * Directory. */ ConnectionGroup getRootConnectionGroup() throws GuacamoleException; /** * Retrieves a collection of all attributes applicable to users. This * collection will contain only those attributes which the current user has * general permission to view or modify. If there are no such attributes, * this collection will be empty. * * @return * A collection of all attributes applicable to users. */ Collection getUserAttributes(); /** * Retrieves a collection of all attributes applicable to connections. This * collection will contain only those attributes which the current user has * general permission to view or modify. If there are no such attributes, * this collection will be empty. * * @return * A collection of all attributes applicable to connections. */ Collection getConnectionAttributes(); /** * Retrieves a collection of all attributes applicable to connection * groups. This collection will contain only those attributes which the * current user has general permission to view or modify. If there are no * such attributes, this collection will be empty. * * @return * A collection of all attributes applicable to connection groups. */ Collection getConnectionGroupAttributes(); } ././@LongLink0100644 0000000 0000000 00000000155 12645510255 011641 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/ConnectionRecordSet.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/ConnectionRecord0100664 0001751 0001751 00000012004 12645510255 035373 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth; import java.util.Collection; import org.glyptodon.guacamole.GuacamoleException; /** * The set of all available connection records, or a subset of those records. * * @author James Muehlner * @author Michael Jumper */ public interface ConnectionRecordSet { /** * All properties of connection records which can be used as sorting * criteria. */ enum SortableProperty { /** * The date and time when the connection associated with the * connection record began. */ START_DATE }; /** * Returns all connection records within this set as a standard Collection. * * @return * A collection containing all connection records within this set. * * @throws GuacamoleException * If an error occurs while retrieving the connection records within * this set. */ Collection asCollection() throws GuacamoleException; /** * Returns the subset of connection records to only those where the * connection name, user identifier, or any associated date field contain * the given value. This function may also affect the contents of the * current ConnectionRecordSet. The contents of the current * ConnectionRecordSet should NOT be relied upon after this function is * called. * * @param value * The value which all connection records within the resulting subset * should contain within their associated connection name or user * identifier. * * @return * The subset of connection history records which contain the specified * value within their associated connection name or user identifier. * * @throws GuacamoleException * If an error occurs while restricting the current subset. */ ConnectionRecordSet contains(String value) throws GuacamoleException; /** * Returns the subset of connection history records containing only the * first limit records. If the subset has fewer than * limit records, then this function has no effect. This * function may also affect the contents of the current * ConnectionRecordSet. The contents of the current ConnectionRecordSet * should NOT be relied upon after this function is called. * * @param limit * The maximum number of records that the new subset should contain. * * @return * The subset of connection history records that containing only the * first limit records. * * @throws GuacamoleException * If an error occurs while limiting the current subset. */ ConnectionRecordSet limit(int limit) throws GuacamoleException; /** * Returns a ConnectionRecordSet containing identically the records within * this set, sorted according to the specified criteria. The sort operation * performed is guaranteed to be stable with respect to any past call to * sort(). This function may also affect the contents of the current * ConnectionRecordSet. The contents of the current ConnectionRecordSet * should NOT be relied upon after this function is called. * * @param property * The property by which the connection records within the resulting * set should be sorted. * * @param desc * Whether the records should be sorted according to the specified * property in descending order. If false, records will be sorted * according to the specified property in ascending order. * * @return * The ConnnectionRecordSet, sorted according to the specified * criteria. * * @throws GuacamoleException * If an error occurs while sorting the current subset. */ ConnectionRecordSet sort(SortableProperty property, boolean desc) throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000217 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/credentials/GuacamoleInsufficientCredentialsException.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/credentials/Guac0100664 0001751 0001751 00000006303 12612226251 035310 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.credentials; /** * A security-related exception thrown when access is denied to a user because * the provided credentials are not sufficient for authentication to succeed. * The validity or invalidity of the given credentials is not specified, and * more information is needed before a decision can be made. Additional * information describing the form of valid credentials is provided. * * @author Michael Jumper */ public class GuacamoleInsufficientCredentialsException extends GuacamoleCredentialsException { /** * Creates a new GuacamoleInsufficientCredentialsException with the given * message, cause, and associated credential information. * * @param message * A human readable description of the exception that occurred. * * @param cause * The cause of this exception. * * @param credentialsInfo * Information describing the form of valid credentials. */ public GuacamoleInsufficientCredentialsException(String message, Throwable cause, CredentialsInfo credentialsInfo) { super(message, cause, credentialsInfo); } /** * Creates a new GuacamoleInsufficientCredentialsException with the given * message and associated credential information. * * @param message * A human readable description of the exception that occurred. * * @param credentialsInfo * Information describing the form of valid credentials. */ public GuacamoleInsufficientCredentialsException(String message, CredentialsInfo credentialsInfo) { super(message, credentialsInfo); } /** * Creates a new GuacamoleInsufficientCredentialsException with the given * cause and associated credential information. * * @param cause * The cause of this exception. * * @param credentialsInfo * Information describing the form of valid credentials. */ public GuacamoleInsufficientCredentialsException(Throwable cause, CredentialsInfo credentialsInfo) { super(cause, credentialsInfo); } } ././@LongLink0100644 0000000 0000000 00000000165 12612226251 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/credentials/CredentialsInfo.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/credentials/Cred0100664 0001751 0001751 00000006067 12612226251 035315 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.credentials; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import org.glyptodon.guacamole.form.Field; import org.glyptodon.guacamole.form.PasswordField; import org.glyptodon.guacamole.form.UsernameField; /** * Information which describes a set of valid credentials. * * @author Michael Jumper */ public class CredentialsInfo { /** * All fields required for valid credentials. */ private final Collection fields; /** * Creates a new CredentialsInfo object which requires the given fields for * any conforming credentials. * * @param fields * The fields to require. */ public CredentialsInfo(Collection fields) { this.fields = fields; } /** * Returns all fields required for valid credentials as described by this * object. * * @return * All fields required for valid credentials. */ public Collection getFields() { return Collections.unmodifiableCollection(fields); } /** * CredentialsInfo object which describes empty credentials. No fields are * required. */ public static final CredentialsInfo EMPTY = new CredentialsInfo(Collections.emptyList()); /** * A field describing the username HTTP parameter expected by Guacamole * during login, if usernames are being used. */ public static final Field USERNAME = new UsernameField("username"); /** * A field describing the password HTTP parameter expected by Guacamole * during login, if passwords are being used. */ public static final Field PASSWORD = new PasswordField("password"); /** * CredentialsInfo object which describes standard username/password * credentials. */ public static final CredentialsInfo USERNAME_PASSWORD = new CredentialsInfo(Arrays.asList( USERNAME, PASSWORD )); } ././@LongLink0100644 0000000 0000000 00000000203 12645510255 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/credentials/GuacamoleCredentialsException.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/credentials/Guac0100664 0001751 0001751 00000007011 12645510255 035313 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.credentials; import org.glyptodon.guacamole.GuacamoleUnauthorizedException; /** * A security-related exception thrown when access is denied to a user because * of a problem related to the provided credentials. Additional information * describing the form of valid credentials is provided. * * @author Michael Jumper */ public class GuacamoleCredentialsException extends GuacamoleUnauthorizedException { /** * Information describing the form of valid credentials. */ private final CredentialsInfo credentialsInfo; /** * Creates a new GuacamoleInvalidCredentialsException with the given * message, cause, and associated credential information. * * @param message * A human readable description of the exception that occurred. * * @param cause * The cause of this exception. * * @param credentialsInfo * Information describing the form of valid credentials. */ public GuacamoleCredentialsException(String message, Throwable cause, CredentialsInfo credentialsInfo) { super(message, cause); this.credentialsInfo = credentialsInfo; } /** * Creates a new GuacamoleInvalidCredentialsException with the given * message and associated credential information. * * @param message * A human readable description of the exception that occurred. * * @param credentialsInfo * Information describing the form of valid credentials. */ public GuacamoleCredentialsException(String message, CredentialsInfo credentialsInfo) { super(message); this.credentialsInfo = credentialsInfo; } /** * Creates a new GuacamoleInvalidCredentialsException with the given cause * and associated credential information. * * @param cause * The cause of this exception. * * @param credentialsInfo * Information describing the form of valid credentials. */ public GuacamoleCredentialsException(Throwable cause, CredentialsInfo credentialsInfo) { super(cause); this.credentialsInfo = credentialsInfo; } /** * Returns information describing the form of valid credentials. * * @return * Information describing the form of valid credentials. */ public CredentialsInfo getCredentialsInfo() { return credentialsInfo; } } ././@LongLink0100644 0000000 0000000 00000000212 12612226251 011625 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/credentials/GuacamoleInvalidCredentialsException.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/credentials/Guac0100664 0001751 0001751 00000005761 12612226251 035317 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth.credentials; /** * A security-related exception thrown when access is denied to a user because * the provided credentials are invalid. Additional information describing * the form of valid credentials is provided. * * @author Michael Jumper */ public class GuacamoleInvalidCredentialsException extends GuacamoleCredentialsException { /** * Creates a new GuacamoleInvalidCredentialsException with the given * message, cause, and associated credential information. * * @param message * A human readable description of the exception that occurred. * * @param cause * The cause of this exception. * * @param credentialsInfo * Information describing the form of valid credentials. */ public GuacamoleInvalidCredentialsException(String message, Throwable cause, CredentialsInfo credentialsInfo) { super(message, cause, credentialsInfo); } /** * Creates a new GuacamoleInvalidCredentialsException with the given * message and associated credential information. * * @param message * A human readable description of the exception that occurred. * * @param credentialsInfo * Information describing the form of valid credentials. */ public GuacamoleInvalidCredentialsException(String message, CredentialsInfo credentialsInfo) { super(message, credentialsInfo); } /** * Creates a new GuacamoleInvalidCredentialsException with the given cause * and associated credential information. * * @param cause * The cause of this exception. * * @param credentialsInfo * Information describing the form of valid credentials. */ public GuacamoleInvalidCredentialsException(Throwable cause, CredentialsInfo credentialsInfo) { super(cause, credentialsInfo); } } ././@LongLink0100644 0000000 0000000 00000000152 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/ActiveConnection.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/ActiveConnection0100664 0001751 0001751 00000010145 12612226251 035366 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth; import java.util.Date; import org.glyptodon.guacamole.net.GuacamoleTunnel; /** * A pairing of username and GuacamoleTunnel representing an active usage of a * particular connection. * * @author Michael Jumper */ public interface ActiveConnection extends Identifiable { /** * Returns the identifier of the connection being actively used. Unlike the * other information stored in this object, the connection identifier must * be present and MAY NOT be null. * * @return * The identifier of the connection being actively used. */ String getConnectionIdentifier(); /** * Sets the identifier of the connection being actively used. * * @param connnectionIdentifier * The identifier of the connection being actively used. */ void setConnectionIdentifier(String connnectionIdentifier); /** * Returns the date and time the connection began. * * @return * The date and time the connection began, or null if this * information is not available. */ Date getStartDate(); /** * Sets the date and time the connection began. * * @param startDate * The date and time the connection began, or null if this * information is not available. */ void setStartDate(Date startDate); /** * Returns the hostname or IP address of the remote host that initiated the * connection, if known. If the hostname or IP address is not known, null * is returned. * * @return * The hostname or IP address of the remote host, or null if this * information is not available. */ String getRemoteHost(); /** * Sets the hostname or IP address of the remote host that initiated the * connection. * * @param remoteHost * The hostname or IP address of the remote host, or null if this * information is not available. */ void setRemoteHost(String remoteHost); /** * Returns the name of the user who is using this connection. * * @return * The name of the user who is using this connection, or null if this * information is not available. */ String getUsername(); /** * Sets the name of the user who is using this connection. * * @param username * The name of the user who is using this connection, or null if this * information is not available. */ void setUsername(String username); /** * Returns the connected GuacamoleTunnel being used. This may be null if * access to the underlying tunnel is denied. * * @return * The connected GuacamoleTunnel, or null if permission is denied. */ GuacamoleTunnel getTunnel(); /** * Sets the connected GuacamoleTunnel being used. * * @param tunnel * The connected GuacamoleTunnel, or null if permission is denied. */ void setTunnel(GuacamoleTunnel tunnel); } ././@LongLink0100644 0000000 0000000 00000000162 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/AbstractActiveConnection.javaguacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/AbstractActiveCo0100664 0001751 0001751 00000006074 12612226251 035322 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net.auth; import java.util.Date; import org.glyptodon.guacamole.net.GuacamoleTunnel; public abstract class AbstractActiveConnection implements ActiveConnection { /** * The identifier of this active connection. */ private String identifier; /** * The identifier of the associated connection. */ private String connectionIdentifier; /** * The date and time this active connection began. */ private Date startDate; /** * The remote host that initiated this connection. */ private String remoteHost; /** * The username of the user that initiated this connection. */ private String username; /** * The underlying GuacamoleTunnel. */ private GuacamoleTunnel tunnel; @Override public String getIdentifier() { return identifier; } @Override public void setIdentifier(String identifier) { this.identifier = identifier; } @Override public String getConnectionIdentifier() { return connectionIdentifier; } @Override public void setConnectionIdentifier(String connnectionIdentifier) { this.connectionIdentifier = connnectionIdentifier; } @Override public Date getStartDate() { return startDate; } @Override public void setStartDate(Date startDate) { this.startDate = startDate; } @Override public String getRemoteHost() { return remoteHost; } @Override public void setRemoteHost(String remoteHost) { this.remoteHost = remoteHost; } @Override public String getUsername() { return username; } @Override public void setUsername(String username) { this.username = username; } @Override public GuacamoleTunnel getTunnel() { return tunnel; } @Override public void setTunnel(GuacamoleTunnel tunnel) { this.tunnel = tunnel; } } guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/form/NumericField.java0100664 0001751 0001751 00000005314 12612226251 034637 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.form; /** * Represents a field which may contain only integer values. * * @author Michael Jumper */ public class NumericField extends Field { /** * Creates a new NumericField with the given name. * * @param name * The unique name to associate with this field. */ public NumericField(String name) { super(name, Field.Type.NUMERIC); } /** * Formats the given integer in the format required by a numeric field. * * @param i * The integer to format, which may be null. * * @return * A string representation of the given integer, or null if the given * integer was null. */ public static String format(Integer i) { // Return null if no value provided if (i == null) return null; // Convert to string return i.toString(); } /** * Parses the given string as an integer, where the given string is in the * format required by a numeric field. * * @param str * The string to parse as an integer, which may be null. * * @return * The integer representation of the given string, or null if the given * string was null. * * @throws NumberFormatException * If the given string is not in a parseable format. */ public static Integer parse(String str) throws NumberFormatException { // Return null if no value provided if (str == null || str.isEmpty()) return null; // Parse as integer return new Integer(str); } } guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/form/MultilineField.java0100664 0001751 0001751 00000003021 12612226251 035170 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.form; /** * Represents a field which can contain multiple lines of text. * * @author Michael Jumper */ public class MultilineField extends Field { /** * Creates a new MultilineField with the given name. * * @param name * The unique name to associate with this field. */ public MultilineField(String name) { super(name, Field.Type.MULTILINE); } } guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/form/EnumField.java0100664 0001751 0001751 00000003275 12612226251 034145 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.form; import java.util.Collection; /** * Represents an arbitrary field with a finite, enumerated set of possible * values. * * @author Michael Jumper */ public class EnumField extends Field { /** * Creates a new EnumField with the given name and possible values. * * @param name * The unique name to associate with this field. * * @param options * All possible legal options for this field. */ public EnumField(String name, Collection options) { super(name, Field.Type.ENUM, options); } } guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/form/PasswordField.java0100664 0001751 0001751 00000003062 12612226251 035035 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.form; /** * Represents a field which contains sensitive text information related to * authenticating a user. * * @author Michael Jumper */ public class PasswordField extends Field { /** * Creates a new PasswordField with the given name. * * @param name * The unique name to associate with this field. */ public PasswordField(String name) { super(name, Field.Type.PASSWORD); } } guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/form/Form.java0100664 0001751 0001751 00000006547 12612226251 033205 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.form; import java.util.ArrayList; import java.util.Collection; import org.codehaus.jackson.map.annotate.JsonSerialize; /** * Information which describes logical set of fields. * * @author Michael Jumper */ @JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) public class Form { /** * The name of this form. The form name must identify the form uniquely * from other forms. */ private String name; /** * All fields associated with this form. */ private Collection fields; /** * Creates a new Form object with no associated fields. The name is left * unset as null. If no form name is provided, this form must not be used * in the same context as another unnamed form. */ public Form() { fields = new ArrayList(); } /** * Creates a new Form object having the given name and containing the given * fields. * * @param name * A name which uniquely identifies this form. * * @param fields * The fields to provided within the new Form. */ public Form(String name, Collection fields) { this.name = name; this.fields = fields; } /** * Returns a mutable collection of the fields associated with this form. * Changes to this collection affect the fields exposed to the user. * * @return * A mutable collection of fields. */ public Collection getFields() { return fields; } /** * Sets the collection of fields associated with this form. * * @param fields * The collection of fields to associate with this form. */ public void setFields(Collection fields) { this.fields = fields; } /** * Returns the name of this form. Form names must uniquely identify each * form. * * @return * The name of this form, or null if the form has no name. */ public String getName() { return name; } /** * Sets the name of this form. Form names must uniquely identify each form. * * @param name * The name to assign to this form. */ public void setName(String name) { this.name = name; } } guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/form/DateField.java0100664 0001751 0001751 00000006366 12612226251 034122 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.form; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; /** * Represents a date field. The field may contain only date values which * conform to a standard pattern, defined by DateField.FORMAT. * * @author Michael Jumper */ public class DateField extends Field { /** * The date format used by date fields, compatible with SimpleDateFormat. */ public static final String FORMAT = "yyyy-MM-dd"; /** * Creates a new DateField with the given name. * * @param name * The unique name to associate with this field. */ public DateField(String name) { super(name, Field.Type.DATE); } /** * Converts the given date into a string which follows the format used by * date fields. * * @param date * The date value to format, which may be null. * * @return * The formatted date, or null if the provided time was null. */ public static String format(Date date) { DateFormat dateFormat = new SimpleDateFormat(DateField.FORMAT); return date == null ? null : dateFormat.format(date); } /** * Parses the given string into a corresponding date. The string must * follow the standard format used by date fields, as defined by FORMAT * and as would be produced by format(). * * @param dateString * The date string to parse, which may be null. * * @return * The date corresponding to the given date string, or null if the * provided date string was null or blank. * * @throws ParseException * If the given date string does not conform to the standard format * used by date fields. */ public static Date parse(String dateString) throws ParseException { // Return null if no date provided if (dateString == null || dateString.isEmpty()) return null; // Parse date according to format DateFormat dateFormat = new SimpleDateFormat(DateField.FORMAT); return dateFormat.parse(dateString); } } guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/form/TimeField.java0100664 0001751 0001751 00000006364 12612226251 034141 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.form; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; /** * Represents a time field. The field may contain only time values which * conform to a standard pattern, defined by TimeField.FORMAT. * * @author Michael Jumper */ public class TimeField extends Field { /** * The time format used by time fields, compatible with SimpleDateFormat. */ public static final String FORMAT = "HH:mm:ss"; /** * Creates a new TimeField with the given name. * * @param name * The unique name to associate with this field. */ public TimeField(String name) { super(name, Field.Type.TIME); } /** * Parses the given string into a corresponding time. The string must * follow the standard format used by time fields, as defined by * FORMAT and as would be produced by format(). * * @param timeString * The time string to parse, which may be null. * * @return * The time corresponding to the given time string, or null if the * provided time string was null or blank. * * @throws ParseException * If the given time string does not conform to the standard format * used by time fields. */ public static Date parse(String timeString) throws ParseException { // Return null if no time provided if (timeString == null || timeString.isEmpty()) return null; // Parse time according to format DateFormat timeFormat = new SimpleDateFormat(TimeField.FORMAT); return timeFormat.parse(timeString); } /** * Converts the given time into a string which follows the format used by * time fields. * * @param time * The time value to format, which may be null. * * @return * The formatted time, or null if the provided time was null. */ public static String format(Date time) { DateFormat timeFormat = new SimpleDateFormat(TimeField.FORMAT); return time == null ? null : timeFormat.format(time); } } guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/form/Field.java0100664 0001751 0001751 00000014317 12612226251 033317 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.form; import java.util.Collection; import org.codehaus.jackson.map.annotate.JsonSerialize; /** * Represents an arbitrary field, such as an HTTP parameter, the parameter of a * remote desktop protocol, or an input field within a form. Fields are generic * and typed dynamically through a type string, with the semantics of the field * defined by the type string. The behavior of each field type is defined * either through the web application itself (see FormService.js) or through * extensions. * * @author Michael Jumper */ @JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) public class Field { /** * All types of fields which are available by default. Additional field * types may be defined by extensions by using a unique field type name and * registering that name with the form service within JavaScript. * * See FormService.js. */ public static class Type { /** * A text field, accepting arbitrary values. */ public static String TEXT = "TEXT"; /** * A username field. This field type generally behaves identically to * arbitrary text fields, but has semantic differences. */ public static String USERNAME = "USERNAME"; /** * A password field, whose value is sensitive and must be hidden. */ public static String PASSWORD = "PASSWORD"; /** * A numeric field, whose value must contain only digits. */ public static String NUMERIC = "NUMERIC"; /** * A boolean field, whose value is either blank or "true". */ public static String BOOLEAN = "BOOLEAN"; /** * An enumerated field, whose legal values are fully enumerated by a * provided, finite list. */ public static String ENUM = "ENUM"; /** * A text field that can span more than one line. */ public static String MULTILINE = "MULTILINE"; /** * A time zone field whose legal values are only valid time zone IDs, * as dictated by Java within TimeZone.getAvailableIDs(). */ public static String TIMEZONE = "TIMEZONE"; /** * A date field whose legal values conform to the pattern "YYYY-MM-DD", * zero-padded. */ public static String DATE = "DATE"; /** * A time field whose legal values conform to the pattern "HH:MM:SS", * zero-padded, 24-hour. */ public static String TIME = "TIME"; } /** * The unique name that identifies this field. */ private String name; /** * The type of this field. */ private String type; /** * A collection of all legal values of this field. */ private Collection options; /** * Creates a new Parameter with no associated name or type. */ public Field() { } /** * Creates a new Field with the given name and type. * * @param name * The unique name to associate with this field. * * @param type * The type of this field. */ public Field(String name, String type) { this.name = name; this.type = type; } /** * Creates a new Field with the given name, type, and possible values. * * @param name * The unique name to associate with this field. * * @param type * The type of this field. * * @param options * A collection of all possible valid options for this field. */ public Field(String name, String type, Collection options) { this.name = name; this.type = type; this.options = options; } /** * Returns the unique name associated with this field. * * @return * The unique name associated with this field. */ public String getName() { return name; } /** * Sets the unique name associated with this field. * * @param name * The unique name to assign to this field. */ public void setName(String name) { this.name = name; } /** * Returns the type of this field. * * @return * The type of this field. */ public String getType() { return type; } /** * Sets the type of this field. * * @param type * The type of this field. */ public void setType(String type) { this.type = type; } /** * Returns a mutable collection of field options. Changes to this * collection directly affect the available options. * * @return * A mutable collection of field options, or null if the field has no * options. */ public Collection getOptions() { return options; } /** * Sets the options available as possible values of this field. * * @param options * The options to associate with this field. */ public void setOptions(Collection options) { this.options = options; } } guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/form/TimeZoneField.java0100664 0001751 0001751 00000004500 12612226251 034763 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.form; /** * Represents a time zone field. The field may contain only valid time zone IDs, * as dictated by TimeZone.getAvailableIDs(). * * @author Michael Jumper */ public class TimeZoneField extends Field { /** * Creates a new TimeZoneField with the given name. * * @param name * The unique name to associate with this field. */ public TimeZoneField(String name) { super(name, Field.Type.TIMEZONE); } /** * Parses the given string into a time zone ID string. As these strings are * equivalent, the only transformation currently performed by this function * is to ensure that a blank time zone string is parsed into null. * * @param timeZone * The time zone string to parse, which may be null. * * @return * The ID of the time zone corresponding to the given string, or null * if the given time zone string was null or blank. */ public static String parse(String timeZone) { // Return null if no time zone provided if (timeZone == null || timeZone.isEmpty()) return null; // Otherwise, assume time zone is valid return timeZone; } } guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/form/UsernameField.java0100664 0001751 0001751 00000003047 12612226251 035015 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.form; /** * Represents a text field which will contain the uniquely-identifying name of * a user. * * @author Michael Jumper */ public class UsernameField extends Field { /** * Creates a new UsernameField with the given name. * * @param name * The unique name to associate with this field. */ public UsernameField(String name) { super(name, Field.Type.USERNAME); } } guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/form/package-info.java0100664 0001751 0001751 00000002373 12612226251 034617 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Provides classes which describe the contents and semantics of forms which * may be presented to the user. */ package org.glyptodon.guacamole.form; guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/form/FieldOption.java0100664 0001751 0001751 00000006054 12612226251 034507 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.form; import org.codehaus.jackson.map.annotate.JsonSerialize; /** * Describes an available legal value for an enumerated field. * * @author Michael Jumper */ @JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) public class FieldOption { /** * The value that will be assigned if this option is chosen. */ private String value; /** * A human-readable title describing the effect of the value. */ private String title; /** * Creates a new FieldOption with no associated value or title. */ public FieldOption() { } /** * Creates a new FieldOption having the given value and title. * * @param value * The value to assign if this option is chosen. * * @param title * The human-readable title to associate with this option. */ public FieldOption(String value, String title) { this.value = value; this.title = title; } /** * Returns the value that will be assigned if this option is chosen. * * @return * The value that will be assigned if this option is chosen. */ public String getValue() { return value; } /** * Sets the value that will be assigned if this option is chosen. * * @param value * The value to assign if this option is chosen. */ public void setValue(String value) { this.value = value; } /** * Returns the human-readable title describing the effect of this option. * * @return * The human-readable title describing the effect of this option. */ public String getTitle() { return title; } /** * Sets the human-readable title describing the effect of this option. * * @param title * A human-readable title describing the effect of this option. */ public void setTitle(String title) { this.title = title; } } guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/form/TextField.java0100664 0001751 0001751 00000003060 12612226251 034155 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.form; /** * Represents a basic text field. The field may generally contain any data, but * may not contain multiple lines. * * @author Michael Jumper */ public class TextField extends Field { /** * Creates a new TextField with the given name. * * @param name * The unique name to associate with this field. */ public TextField(String name) { super(name, Field.Type.TEXT); } } guacamole-client-0.9.9/guacamole-ext/src/main/java/org/glyptodon/guacamole/form/BooleanField.java0100664 0001751 0001751 00000004114 12612226251 034611 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.form; import java.util.Collections; /** * Represents a field with strictly one possible value. It is assumed that the * field may be blank, but that its sole non-blank value is the value provided. * The provided value represents "true" while all other values, including * having no associated value, represent "false". * * @author Michael Jumper */ public class BooleanField extends Field { /** * Creates a new BooleanField with the given name and truth value. The * truth value is the value that, when assigned to this field, means that * this field is "true". * * @param name * The unique name to associate with this field. * * @param truthValue * The value to consider "true" for this field. All other values will * be considered "false". */ public BooleanField(String name, String truthValue) { super(name, Field.Type.BOOLEAN, Collections.singletonList(truthValue)); } } ././@LongLink0100644 0000000 0000000 00000000146 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-ext/src/test/java/org/glyptodon/guacamole/token/TokenFilterTest.javaguacamole-client-0.9.9/guacamole-ext/src/test/java/org/glyptodon/guacamole/token/TokenFilterTest.jav0100664 0001751 0001751 00000006454 12612226251 035434 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.token; import java.util.HashMap; import java.util.Map; import org.junit.Test; import static org.junit.Assert.*; /** * Test which verifies the filtering functionality of TokenFilter. * * @author Michael Jumper */ public class TokenFilterTest { /** * Verifies that token replacement via filter() functions as specified. */ @Test public void testFilter() { // Create token filter TokenFilter tokenFilter = new TokenFilter(); tokenFilter.setToken("TOKEN_A", "value-of-a"); tokenFilter.setToken("TOKEN_B", "value-of-b"); // Test basic substitution and escaping assertEquals( "$${NOPE}hellovalue-of-aworldvalue-of-b${NOT_A_TOKEN}", tokenFilter.filter("$$${NOPE}hello${TOKEN_A}world${TOKEN_B}$${NOT_A_TOKEN}") ); // Unknown tokens must be interpreted as literals assertEquals( "${NOPE}hellovalue-of-aworld${TOKEN_C}", tokenFilter.filter("${NOPE}hello${TOKEN_A}world${TOKEN_C}") ); } /** * Verifies that token replacement via filterValues() functions as * specified. */ @Test public void testFilterValues() { // Create token filter TokenFilter tokenFilter = new TokenFilter(); tokenFilter.setToken("TOKEN_A", "value-of-a"); tokenFilter.setToken("TOKEN_B", "value-of-b"); // Create test map Map map = new HashMap(); map.put(1, "$$${NOPE}hello${TOKEN_A}world${TOKEN_B}$${NOT_A_TOKEN}"); map.put(2, "${NOPE}hello${TOKEN_A}world${TOKEN_C}"); map.put(3, null); // Filter map values tokenFilter.filterValues(map); // Filter should not affect size of map assertEquals(3, map.size()); // Filtered value 1 assertEquals( "$${NOPE}hellovalue-of-aworldvalue-of-b${NOT_A_TOKEN}", map.get(1) ); // Filtered value 2 assertEquals( "${NOPE}hellovalue-of-aworld${TOKEN_C}", map.get(2) ); // Null values are not filtered assertNull(map.get(3)); } } guacamole-client-0.9.9/guacamole-ext/pom.xml0100664 0001751 0001751 00000010573 12645510255 022612 0ustar00mjumpermjumper0000000 0000000 4.0.0 org.glyptodon.guacamole guacamole-ext jar 0.9.9 guacamole-ext http://guac-dev.org/ The Java API for extending the main Guacamole web application. This is not needed for authoring a new Guacamole-based web application. The MIT License http://www.opensource.org/licenses/mit-license.php repo https://github.com/glyptodon/guacamole-client scm:git:git://github.com/glyptodon/guacamole-client.git mike.jumper Michael Jumper mike.jumper@guac-dev.org james.muehlner James Muehlner james.muehlner@guac-dev.org UTF-8 org.apache.maven.plugins maven-compiler-plugin 3.3 1.6 1.6 -Xlint:all -Werror true org.apache.maven.plugins maven-source-plugin 2.4 attach-sources jar org.apache.maven.plugins maven-javadoc-plugin 2.10.3 false attach-javadocs jar javax.servlet servlet-api 2.5 provided org.glyptodon.guacamole guacamole-common 0.9.9 compile junit junit 4.10 test org.codehaus.jackson jackson-mapper-asl 1.9.2 guacamole-client-0.9.9/guacamole-ext/ChangeLog0100664 0001751 0001751 00000000675 12424302752 023045 0ustar00mjumpermjumper0000000 0000000 2012-10-15 Michael Jumper * Updated to latest guacamole-common 2012-08-11 Michael Jumper * Improved documentation 2012-05-04 Michael Jumper * Renamed to guacamole-ext * Generic HTTP-based Credentials object * Added support for listeners and hooks 2011-12-11 Michael Jumper * Initial release guacamole-client-0.9.9/guacamole-ext/LICENSE0100664 0001751 0001751 00000002041 12471273273 022274 0ustar00mjumpermjumper0000000 0000000 Copyright (C) 2013 Glyptodon LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. guacamole-client-0.9.9/project-assembly.xml0100664 0001751 0001751 00000001333 12612226251 022533 0ustar00mjumpermjumper0000000 0000000 project tar.gz ${project.basedir} true **/*.log **/${project.build.directory}/** guacamole-client-0.9.9/LICENSE0100664 0001751 0001751 00000002041 12612226251 017530 0ustar00mjumpermjumper0000000 0000000 Copyright (C) 2013 Glyptodon LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. guacamole-client-0.9.9/CONTRIBUTING0100664 0001751 0001751 00000005063 12612226251 020364 0ustar00mjumpermjumper0000000 0000000 ------------------------------------------------------------ Contributing to Guacamole ------------------------------------------------------------ Thank you for contributing to the Guacamole project! There are certain procedures that must be followed for all contributions. These procedures are necessary to allow us to allocate resources for reviewing and testing your contribution, as well as to ensure we have your legal authorization to include your contribution in Guacamole. 1) Create an issue in our JIRA All changes to Guacamole must have corresponding issues in JIRA so the change can be properly tracked: http://glyptodon.org/jira/ If you do not already have an account on our JIRA, you will need to create one before creating your new issue. 2) Make and test your changes locally The Guacamole source is maintained in git repositories hosted on GitHub: https://github.com/glyptodon/guacamole-client https://github.com/glyptodon/guacamole-server To make your changes, fork the applicable repositories and make commits to a topic branch in your fork. Commits should be made in logical units and must reference the JIRA issue number: $ git commit -m "GUAC-123: Message describing the specific changes made." Avoid commits which cover multiple, distinct goals that could (and should) be handled separately. If you do not already have an account on GitHub, you will need to create one before making your changes. 3) Sign our Contributor License Agreement (CLA) All contributors to the Guacamole project must have signed CLAs on file before we can merge their contributions: http://glyptodon.org/cla.html This is necessary to ensure we have the legal right to include your code in our repositories, that we can continue to distribute that code under the MIT license, and that you have the legal right to give us that code. If you create a pull request without first signing the CLA, you will be asked to do so before the pull request is reviewed. 4) Submit your changes via a pull request on GitHub Once your changes are ready, submit them by creating a pull request for the corresponding topic branch you created when you began working on your changes. The Guacamole team will then review your changes and, if they pass review and we have your CLA on file, your changes will be allocated to a sprint for final testing and merge, and your name will be added to the list of contributors for whichever repositories contain your contributions. guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/README0100664 0001751 0001751 00000010127 12612226251 025402 0ustar00mjumpermjumper0000000 0000000 ------------------------------------------------------------ About this README ------------------------------------------------------------ This README is intended to provide quick and to-the-point documentation for technical users intending to compile parts of Guacamole themselves. Distribution-specific packages are available from the files section of the main project page: http://sourceforge.net/projects/guacamole/files/ Distribution-specific documentation is provided on the Guacamole wiki: http://guac-dev.org/ ------------------------------------------------------------ What is guacamole-auth-jdbc? ------------------------------------------------------------ guacamole-auth-jdbc is a Java library for use with the Guacamole web application to provide database-driven authentication. guacamole-auth-jdbc provides multiple authentication provider implementations which each provide a support for a different database. These authentication providers can be set in guacamole.properties to allow authentication of Guacamole users through that type of database. Schema files are provided to create the required tables in your database of choice. ------------------------------------------------------------ Compiling and installing guacamole-auth-jdbc ------------------------------------------------------------ guacamole-auth-jdbc is built using Maven. Building guacamole-auth-jdbc compiles all classes and packages them into a redistributable .tar.gz archive. This archive contains multiple .jar files, each of this corresponds to a database-specific authentication provider implementation that can be installed in the library directory configured in guacamole.properties. 1) Run mvn package $ mvn package Maven will download any needed dependencies for building the .jar file. Once all dependencies have been downloaded, the .jar file will be created in the target/ subdirectory of the current directory. 4) Extract the .tar.gz file now present in the target/ directory, and place the .jar files from the extracted database-specific subdirectory in the library directory specified in guacamole.properties. You will likely need to do this as root. If you do not have a library directory configured in your guacamole.properties, you will need to specify one. The directory is specified using the "lib-directory" property. 5) Set up your database to authenticate Guacamole users A schema file is provided in the schema directory for creating the guacamole authentication tables in your database of choice. Additionally, a script is provided to create a default admin user with username 'guacadmin' and password 'guacadmin'. This user can be used to set up any other connections and users. 6) Configure guacamole.properties for your database There are additional properties required by JDBC drivers which must be added/changed in your guacamole.properties. These parameters are specific to the database being used. For MySQL, the following properties are available: # Database connection configuration mysql-hostname: database.host.name mysql-port: 3306 mysql-database: guacamole.database.name mysql-username: user mysql-password: pass Optionally, the authentication provider can be configured not to allow multiple users to use the same connection at the same time: mysql-disallow-simultaneous-connections: true For PostgreSQL, the properties are the same, but have different prefixes: # Database connection configuration postgresql-hostname: database.host.name postgresql-port: 5432 postgresql-database: guacamole.database.name postgresql-username: user postgresql-password: pass postgresql-disallow-simultaneous-connections: true ------------------------------------------------------------ Reporting problems ------------------------------------------------------------ Please report any bugs encountered by opening a new issue in the JIRA system hosted at: http://glyptodon.org/jira/ guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/src/main/assembly/dist.xml0100664 0001751 0001751 00000003123 12612226251 031537 0ustar00mjumpermjumper0000000 0000000 dist ${project.artifactId}-${project.version} tar.gz mysql/schema modules/guacamole-auth-jdbc-mysql/schema modules/guacamole-auth-jdbc-mysql/target mysql *.jar postgresql/schema modules/guacamole-auth-jdbc-postgresql/schema modules/guacamole-auth-jdbc-postgresql/target postgresql *.jar ././@LongLink0100644 0000000 0000000 00000000254 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/ParameterMapper.xmlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0100664 0001751 0001751 00000005633 12612226251 035366 0ustar00mjumpermjumper0000000 0000000 DELETE FROM guacamole_connection_parameter WHERE connection_id = #{identifier,jdbcType=VARCHAR} INSERT INTO guacamole_connection_parameter ( connection_id, parameter_name, parameter_value ) VALUES (#{parameter.connectionIdentifier,jdbcType=VARCHAR}, #{parameter.name,jdbcType=VARCHAR}, #{parameter.value,jdbcType=VARCHAR}) ././@LongLink0100644 0000000 0000000 00000000263 12645510255 011641 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xmlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0100664 0001751 0001751 00000021246 12645510255 035372 0ustar00mjumpermjumper0000000 0000000 INSERT INTO guacamole_connection_history ( connection_id, user_id, start_date, end_date ) VALUES ( #{record.connectionIdentifier,jdbcType=VARCHAR}, #{record.userID,jdbcType=INTEGER}, #{record.startDate,jdbcType=TIMESTAMP}, #{record.endDate,jdbcType=TIMESTAMP} ) ././@LongLink0100644 0000000 0000000 00000000255 12625262336 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionMapper.xmlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0100664 0001751 0001751 00000016251 12625262336 035374 0ustar00mjumpermjumper0000000 0000000 DELETE FROM guacamole_connection WHERE connection_id = #{identifier,jdbcType=VARCHAR} INSERT INTO guacamole_connection ( connection_name, parent_id, protocol, max_connections, max_connections_per_user ) VALUES ( #{object.name,jdbcType=VARCHAR}, #{object.parentIdentifier,jdbcType=VARCHAR}, #{object.protocol,jdbcType=VARCHAR}, #{object.maxConnections,jdbcType=INTEGER}, #{object.maxConnectionsPerUser,jdbcType=INTEGER} ) UPDATE guacamole_connection SET connection_name = #{object.name,jdbcType=VARCHAR}, parent_id = #{object.parentIdentifier,jdbcType=VARCHAR}, protocol = #{object.protocol,jdbcType=VARCHAR}, max_connections = #{object.maxConnections,jdbcType=INTEGER}, max_connections_per_user = #{object.maxConnectionsPerUser,jdbcType=INTEGER} WHERE connection_id = #{object.objectID,jdbcType=INTEGER} ././@LongLink0100644 0000000 0000000 00000000263 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/SystemPermissionMapper.xmlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0100664 0001751 0001751 00000007475 12612226251 035374 0ustar00mjumpermjumper0000000 0000000 DELETE FROM guacamole_system_permission WHERE (user_id, permission) IN (#{permission.userID,jdbcType=INTEGER}, #{permission.type,jdbcType=VARCHAR}) INSERT IGNORE INTO guacamole_system_permission ( user_id, permission ) VALUES (#{permission.userID,jdbcType=INTEGER}, #{permission.type,jdbcType=VARCHAR}) ././@LongLink0100644 0000000 0000000 00000000274 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xmlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0100664 0001751 0001751 00000012312 12612226251 035356 0ustar00mjumpermjumper0000000 0000000 DELETE FROM guacamole_connection_group_permission WHERE (user_id, permission, connection_group_id) IN (#{permission.userID,jdbcType=INTEGER}, #{permission.type,jdbcType=VARCHAR}, #{permission.objectIdentifier,jdbcType=VARCHAR}) INSERT IGNORE INTO guacamole_connection_group_permission ( user_id, permission, connection_group_id ) VALUES (#{permission.userID,jdbcType=INTEGER}, #{permission.type,jdbcType=VARCHAR}, #{permission.objectIdentifier,jdbcType=VARCHAR}) ././@LongLink0100644 0000000 0000000 00000000267 12612226251 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xmlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0100664 0001751 0001751 00000012062 12612226251 035360 0ustar00mjumpermjumper0000000 0000000 DELETE FROM guacamole_connection_permission WHERE (user_id, permission, connection_id) IN (#{permission.userID,jdbcType=INTEGER}, #{permission.type,jdbcType=VARCHAR}, #{permission.objectIdentifier,jdbcType=VARCHAR}) INSERT IGNORE INTO guacamole_connection_permission ( user_id, permission, connection_id ) VALUES (#{permission.userID,jdbcType=INTEGER}, #{permission.type,jdbcType=VARCHAR}, #{permission.objectIdentifier,jdbcType=VARCHAR}) ././@LongLink0100644 0000000 0000000 00000000261 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/UserPermissionMapper.xmlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0100664 0001751 0001751 00000013627 12612226251 035370 0ustar00mjumpermjumper0000000 0000000 DELETE FROM guacamole_user_permission USING guacamole_user_permission JOIN guacamole_user affected ON guacamole_user_permission.affected_user_id = affected.user_id WHERE (guacamole_user_permission.user_id, permission, affected.username) IN (#{permission.userID,jdbcType=INTEGER}, #{permission.type,jdbcType=VARCHAR}, #{permission.objectIdentifier,jdbcType=VARCHAR}) INSERT IGNORE INTO guacamole_user_permission ( user_id, permission, affected_user_id ) SELECT permissions.user_id, permissions.permission, guacamole_user.user_id FROM SELECT #{permission.userID,jdbcType=INTEGER} AS user_id, #{permission.type,jdbcType=VARCHAR} AS permission, #{permission.objectIdentifier,jdbcType=VARCHAR} AS username AS permissions JOIN guacamole_user ON guacamole_user.username = permissions.username; ././@LongLink0100644 0000000 0000000 00000000267 12612226251 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xmlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0100664 0001751 0001751 00000017070 12612226251 035364 0ustar00mjumpermjumper0000000 0000000 DELETE FROM guacamole_connection_group WHERE connection_group_id = #{identifier,jdbcType=VARCHAR} INSERT INTO guacamole_connection_group ( connection_group_name, parent_id, type, max_connections, max_connections_per_user ) VALUES ( #{object.name,jdbcType=VARCHAR}, #{object.parentIdentifier,jdbcType=VARCHAR}, #{object.type,jdbcType=VARCHAR}, #{object.maxConnections,jdbcType=INTEGER}, #{object.maxConnectionsPerUser,jdbcType=INTEGER} ) UPDATE guacamole_connection_group SET connection_group_name = #{object.name,jdbcType=VARCHAR}, parent_id = #{object.parentIdentifier,jdbcType=VARCHAR}, type = #{object.type,jdbcType=VARCHAR}, max_connections = #{object.maxConnections,jdbcType=INTEGER}, max_connections_per_user = #{object.maxConnectionsPerUser,jdbcType=INTEGER} WHERE connection_group_id = #{object.objectID,jdbcType=INTEGER} ././@LongLink0100644 0000000 0000000 00000000241 12625262336 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/user/UserMapper.xmlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0100664 0001751 0001751 00000015621 12625262336 035374 0ustar00mjumpermjumper0000000 0000000 DELETE FROM guacamole_user WHERE username = #{identifier,jdbcType=VARCHAR} INSERT INTO guacamole_user ( username, password_hash, password_salt, disabled, expired, access_window_start, access_window_end, valid_from, valid_until, timezone ) VALUES ( #{object.identifier,jdbcType=VARCHAR}, #{object.passwordHash,jdbcType=BINARY}, #{object.passwordSalt,jdbcType=BINARY}, #{object.disabled,jdbcType=BOOLEAN}, #{object.expired,jdbcType=BOOLEAN}, #{object.accessWindowStart,jdbcType=TIME}, #{object.accessWindowEnd,jdbcType=TIME}, #{object.validFrom,jdbcType=DATE}, #{object.validUntil,jdbcType=DATE}, #{object.timeZone,jdbcType=VARCHAR} ) UPDATE guacamole_user SET password_hash = #{object.passwordHash,jdbcType=BINARY}, password_salt = #{object.passwordSalt,jdbcType=BINARY}, disabled = #{object.disabled,jdbcType=BOOLEAN}, expired = #{object.expired,jdbcType=BOOLEAN}, access_window_start = #{object.accessWindowStart,jdbcType=TIME}, access_window_end = #{object.accessWindowEnd,jdbcType=TIME}, valid_from = #{object.validFrom,jdbcType=DATE}, valid_until = #{object.validUntil,jdbcType=DATE}, timezone = #{object.timeZone,jdbcType=VARCHAR} WHERE user_id = #{object.objectID,jdbcType=VARCHAR} ././@LongLink0100644 0000000 0000000 00000000176 12645510255 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/guac-manifest.jsonguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/res0100664 0001751 0001751 00000000540 12645510255 035364 0ustar00mjumpermjumper0000000 0000000 { "guacamoleVersion" : "0.9.9", "name" : "MySQL Authentication", "namespace" : "guac-mysql", "authProviders" : [ "net.sourceforge.guacamole.net.auth.mysql.MySQLAuthenticationProvider" ], "translations" : [ "translations/en.json", "translations/fr.json", "translations/ru.json" ] } ././@LongLink0100644 0000000 0000000 00000000255 12645510255 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/MySQLGuacamoleProperties.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/jav0100664 0001751 0001751 00000012647 12645510255 035366 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package net.sourceforge.guacamole.net.auth.mysql; import org.glyptodon.guacamole.properties.BooleanGuacamoleProperty; import org.glyptodon.guacamole.properties.IntegerGuacamoleProperty; import org.glyptodon.guacamole.properties.StringGuacamoleProperty; /** * Properties used by the MySQL Authentication plugin. * * @author James Muehlner */ public class MySQLGuacamoleProperties { /** * This class should not be instantiated. */ private MySQLGuacamoleProperties() {} /** * The hostname of the MySQL server hosting the Guacamole authentication * tables. */ public static final StringGuacamoleProperty MYSQL_HOSTNAME = new StringGuacamoleProperty() { @Override public String getName() { return "mysql-hostname"; } }; /** * The port number of the MySQL server hosting the Guacamole authentication * tables. */ public static final IntegerGuacamoleProperty MYSQL_PORT = new IntegerGuacamoleProperty() { @Override public String getName() { return "mysql-port"; } }; /** * The name of the MySQL database containing the Guacamole authentication * tables. */ public static final StringGuacamoleProperty MYSQL_DATABASE = new StringGuacamoleProperty() { @Override public String getName() { return "mysql-database"; } }; /** * The username that should be used when authenticating with the MySQL * database containing the Guacamole authentication tables. */ public static final StringGuacamoleProperty MYSQL_USERNAME = new StringGuacamoleProperty() { @Override public String getName() { return "mysql-username"; } }; /** * The password that should be used when authenticating with the MySQL * database containing the Guacamole authentication tables. */ public static final StringGuacamoleProperty MYSQL_PASSWORD = new StringGuacamoleProperty() { @Override public String getName() { return "mysql-password"; } }; /** * Whether or not multiple users accessing the same connection at the same * time should be disallowed. */ public static final BooleanGuacamoleProperty MYSQL_DISALLOW_SIMULTANEOUS_CONNECTIONS = new BooleanGuacamoleProperty() { @Override public String getName() { return "mysql-disallow-simultaneous-connections"; } }; /** * Whether or not the same user accessing the same connection or connection * group at the same time should be disallowed. */ public static final BooleanGuacamoleProperty MYSQL_DISALLOW_DUPLICATE_CONNECTIONS = new BooleanGuacamoleProperty() { @Override public String getName() { return "mysql-disallow-duplicate-connections"; } }; /** * The maximum number of concurrent connections to allow to any one * connection. Zero denotes unlimited. */ public static final IntegerGuacamoleProperty MYSQL_DEFAULT_MAX_CONNECTIONS = new IntegerGuacamoleProperty() { @Override public String getName() { return "mysql-default-max-connections"; } }; /** * The maximum number of concurrent connections to allow to any one * connection group. Zero denotes unlimited. */ public static final IntegerGuacamoleProperty MYSQL_DEFAULT_MAX_GROUP_CONNECTIONS = new IntegerGuacamoleProperty() { @Override public String getName() { return "mysql-default-max-group-connections"; } }; /** * The maximum number of concurrent connections to allow to any one * connection by an individual user. Zero denotes unlimited. */ public static final IntegerGuacamoleProperty MYSQL_DEFAULT_MAX_CONNECTIONS_PER_USER = new IntegerGuacamoleProperty() { @Override public String getName() { return "mysql-default-max-connections-per-user"; } }; /** * The maximum number of concurrent connections to allow to any one * connection group by an individual user. Zero denotes * unlimited. */ public static final IntegerGuacamoleProperty MYSQL_DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER = new IntegerGuacamoleProperty() { @Override public String getName() { return "mysql-default-max-group-connections-per-user"; } }; } ././@LongLink0100644 0000000 0000000 00000000245 12645510255 011641 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/MySQLEnvironment.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/jav0100664 0001751 0001751 00000026001 12645510255 035353 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package net.sourceforge.guacamole.net.auth.mysql; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.auth.jdbc.JDBCEnvironment; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A MySQL-specific implementation of JDBCEnvironment provides database * properties specifically for MySQL. * * @author James Muehlner * @author Michael Jumper */ public class MySQLEnvironment extends JDBCEnvironment { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(MySQLEnvironment.class); /** * The default host to connect to, if MYSQL_HOSTNAME is not specified. */ private static final String DEFAULT_HOSTNAME = "localhost"; /** * The default port to connect to, if MYSQL_PORT is not specified. */ private static final int DEFAULT_PORT = 3306; /** * The default value for the default maximum number of connections to be * allowed per user to any one connection. Note that, as long as the * legacy "disallow duplicate" and "disallow simultaneous" properties are * still supported, these cannot be constants, as the legacy properties * dictate the values that should be used in the absence of the correct * properties. */ private int DEFAULT_MAX_CONNECTIONS_PER_USER = 1; /** * The default value for the default maximum number of connections to be * allowed per user to any one connection group. Note that, as long as the * legacy "disallow duplicate" and "disallow simultaneous" properties are * still supported, these cannot be constants, as the legacy properties * dictate the values that should be used in the absence of the correct * properties. */ private int DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER = 1; /** * The default value for the default maximum number of connections to be * allowed to any one connection. Note that, as long as the legacy * "disallow duplicate" and "disallow simultaneous" properties are still * supported, these cannot be constants, as the legacy properties dictate * the values that should be used in the absence of the correct properties. */ private int DEFAULT_MAX_CONNECTIONS = 0; /** * The default value for the default maximum number of connections to be * allowed to any one connection group. Note that, as long as the legacy * "disallow duplicate" and "disallow simultaneous" properties are still * supported, these cannot be constants, as the legacy properties dictate * the values that should be used in the absence of the correct properties. */ private int DEFAULT_MAX_GROUP_CONNECTIONS = 0; /** * Constructs a new MySQLEnvironment, providing access to MySQL-specific * configuration options. * * @throws GuacamoleException * If an error occurs while setting up the underlying JDBCEnvironment * or while parsing legacy MySQL configuration options. */ public MySQLEnvironment() throws GuacamoleException { // Init underlying JDBC environment super(); // Read legacy concurrency-related property Boolean disallowSimultaneous = getProperty(MySQLGuacamoleProperties.MYSQL_DISALLOW_SIMULTANEOUS_CONNECTIONS); Boolean disallowDuplicate = getProperty(MySQLGuacamoleProperties.MYSQL_DISALLOW_DUPLICATE_CONNECTIONS); // Legacy "simultaneous" property dictates only the maximum number of // connections per connection if (disallowSimultaneous != null) { // Translate legacy property if (disallowSimultaneous) { DEFAULT_MAX_CONNECTIONS = 1; DEFAULT_MAX_GROUP_CONNECTIONS = 0; } else { DEFAULT_MAX_CONNECTIONS = 0; DEFAULT_MAX_GROUP_CONNECTIONS = 0; } // Warn of deprecation logger.warn("The \"{}\" property is deprecated. Use \"{}\" and \"{}\" instead.", MySQLGuacamoleProperties.MYSQL_DISALLOW_SIMULTANEOUS_CONNECTIONS.getName(), MySQLGuacamoleProperties.MYSQL_DEFAULT_MAX_CONNECTIONS.getName(), MySQLGuacamoleProperties.MYSQL_DEFAULT_MAX_GROUP_CONNECTIONS.getName()); // Inform of new equivalent logger.info("To achieve the same result of setting \"{}\" to \"{}\", set \"{}\" to \"{}\" and \"{}\" to \"{}\".", MySQLGuacamoleProperties.MYSQL_DISALLOW_SIMULTANEOUS_CONNECTIONS.getName(), disallowSimultaneous, MySQLGuacamoleProperties.MYSQL_DEFAULT_MAX_CONNECTIONS.getName(), DEFAULT_MAX_CONNECTIONS, MySQLGuacamoleProperties.MYSQL_DEFAULT_MAX_GROUP_CONNECTIONS.getName(), DEFAULT_MAX_GROUP_CONNECTIONS); } // Legacy "duplicate" property dictates whether connections and groups // may be used concurrently only by different users if (disallowDuplicate != null) { // Translate legacy property if (disallowDuplicate) { DEFAULT_MAX_CONNECTIONS_PER_USER = 1; DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER = 1; } else { DEFAULT_MAX_CONNECTIONS_PER_USER = 0; DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER = 0; } // Warn of deprecation logger.warn("The \"{}\" property is deprecated. Use \"{}\" and \"{}\" instead.", MySQLGuacamoleProperties.MYSQL_DISALLOW_DUPLICATE_CONNECTIONS.getName(), MySQLGuacamoleProperties.MYSQL_DEFAULT_MAX_CONNECTIONS_PER_USER.getName(), MySQLGuacamoleProperties.MYSQL_DEFAULT_MAX_GROUP_CONNECTIONS.getName()); // Inform of new equivalent logger.info("To achieve the same result of setting \"{}\" to \"{}\", set \"{}\" to \"{}\" and \"{}\" to \"{}\".", MySQLGuacamoleProperties.MYSQL_DISALLOW_DUPLICATE_CONNECTIONS.getName(), disallowDuplicate, MySQLGuacamoleProperties.MYSQL_DEFAULT_MAX_CONNECTIONS_PER_USER.getName(), DEFAULT_MAX_CONNECTIONS_PER_USER, MySQLGuacamoleProperties.MYSQL_DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER.getName(), DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER); } } @Override public int getDefaultMaxConnections() throws GuacamoleException { return getProperty( MySQLGuacamoleProperties.MYSQL_DEFAULT_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS ); } @Override public int getDefaultMaxGroupConnections() throws GuacamoleException { return getProperty( MySQLGuacamoleProperties.MYSQL_DEFAULT_MAX_GROUP_CONNECTIONS, DEFAULT_MAX_GROUP_CONNECTIONS ); } @Override public int getDefaultMaxConnectionsPerUser() throws GuacamoleException { return getProperty( MySQLGuacamoleProperties.MYSQL_DEFAULT_MAX_CONNECTIONS_PER_USER, DEFAULT_MAX_CONNECTIONS_PER_USER ); } @Override public int getDefaultMaxGroupConnectionsPerUser() throws GuacamoleException { return getProperty( MySQLGuacamoleProperties.MYSQL_DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER, DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER ); } /** * Returns the hostname of the MySQL server hosting the Guacamole * authentication tables. If unspecified, this will be "localhost". * * @return * The URL of the MySQL server. * * @throws GuacamoleException * If an error occurs while retrieving the property value. */ public String getMySQLHostname() throws GuacamoleException { return getProperty( MySQLGuacamoleProperties.MYSQL_HOSTNAME, DEFAULT_HOSTNAME ); } /** * Returns the port number of the MySQL server hosting the Guacamole * authentication tables. If unspecified, this will be the default MySQL * port of 3306. * * @return * The port number of the MySQL server. * * @throws GuacamoleException * If an error occurs while retrieving the property value. */ public int getMySQLPort() throws GuacamoleException { return getProperty(MySQLGuacamoleProperties.MYSQL_PORT, DEFAULT_PORT); } /** * Returns the name of the MySQL database containing the Guacamole * authentication tables. * * @return * The name of the MySQL database. * * @throws GuacamoleException * If an error occurs while retrieving the property value, or if the * value was not set, as this property is required. */ public String getMySQLDatabase() throws GuacamoleException { return getRequiredProperty(MySQLGuacamoleProperties.MYSQL_DATABASE); } /** * Returns the username that should be used when authenticating with the * MySQL database containing the Guacamole authentication tables. * * @return * The username for the MySQL database. * * @throws GuacamoleException * If an error occurs while retrieving the property value, or if the * value was not set, as this property is required. */ public String getMySQLUsername() throws GuacamoleException { return getRequiredProperty(MySQLGuacamoleProperties.MYSQL_USERNAME); } /** * Returns the password that should be used when authenticating with the * MySQL database containing the Guacamole authentication tables. * * @return * The password for the MySQL database. * * @throws GuacamoleException * If an error occurs while retrieving the property value, or if the * value was not set, as this property is required. */ public String getMySQLPassword() throws GuacamoleException { return getRequiredProperty(MySQLGuacamoleProperties.MYSQL_PASSWORD); } } ././@LongLink0100644 0000000 0000000 00000000241 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/package-info.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/jav0100664 0001751 0001751 00000002414 12612226251 035347 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The MySQL authentication provider. This package exists outside of * org.glyptodon for backwards-compatibility. */ package net.sourceforge.guacamole.net.auth.mysql; ././@LongLink0100644 0000000 0000000 00000000260 12645510255 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/MySQLAuthenticationProvider.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/jav0100664 0001751 0001751 00000010204 12645510255 035351 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package net.sourceforge.guacamole.net.auth.mysql; import com.google.inject.Guice; import com.google.inject.Injector; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.auth.jdbc.JDBCAuthenticationProviderModule; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticationProviderService; import org.glyptodon.guacamole.net.auth.AuthenticatedUser; /** * Provides a MySQL based implementation of the AuthenticationProvider * functionality. * * @author James Muehlner * @author Michael Jumper */ public class MySQLAuthenticationProvider implements AuthenticationProvider { /** * Injector which will manage the object graph of this authentication * provider. */ private final Injector injector; /** * Creates a new MySQLAuthenticationProvider that reads and writes * authentication data to a MySQL database defined by properties in * guacamole.properties. * * @throws GuacamoleException * If a required property is missing, or an error occurs while parsing * a property. */ public MySQLAuthenticationProvider() throws GuacamoleException { // Get local environment MySQLEnvironment environment = new MySQLEnvironment(); // Set up Guice injector. injector = Guice.createInjector( // Configure MySQL-specific authentication new MySQLAuthenticationProviderModule(environment), // Configure JDBC authentication core new JDBCAuthenticationProviderModule(this, environment) ); } @Override public String getIdentifier() { return "mysql"; } @Override public AuthenticatedUser authenticateUser(Credentials credentials) throws GuacamoleException { // Create AuthenticatedUser based on credentials, if valid AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class); return authProviderService.authenticateUser(this, credentials); } @Override public AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser, Credentials credentials) throws GuacamoleException { // No need to update authenticated users return authenticatedUser; } @Override public UserContext getUserContext(AuthenticatedUser authenticatedUser) throws GuacamoleException { // Create UserContext based on credentials, if valid AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class); return authProviderService.getUserContext(authenticatedUser); } @Override public UserContext updateUserContext(UserContext context, AuthenticatedUser authenticatedUser) throws GuacamoleException { // No need to update the context return context; } } ././@LongLink0100644 0000000 0000000 00000000266 12645510255 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/MySQLAuthenticationProviderModule.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/jav0100664 0001751 0001751 00000007250 12645510255 035360 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package net.sourceforge.guacamole.net.auth.mysql; import com.google.inject.Binder; import com.google.inject.Module; import com.google.inject.name.Names; import java.util.Properties; import org.glyptodon.guacamole.GuacamoleException; import org.mybatis.guice.datasource.helper.JdbcHelper; /** * Guice module which configures MySQL-specific injections. * * @author James Muehlner */ public class MySQLAuthenticationProviderModule implements Module { /** * MyBatis-specific configuration properties. */ private final Properties myBatisProperties = new Properties(); /** * MySQL-specific driver configuration properties. */ private final Properties driverProperties = new Properties(); /** * Creates a new MySQL authentication provider module that configures * driver and MyBatis properties using the given environment. * * @param environment * The environment to use when configuring MyBatis and the underlying * JDBC driver. * * @throws GuacamoleException * If a required property is missing, or an error occurs while parsing * a property. */ public MySQLAuthenticationProviderModule(MySQLEnvironment environment) throws GuacamoleException { // Set the MySQL-specific properties for MyBatis. myBatisProperties.setProperty("mybatis.environment.id", "guacamole"); myBatisProperties.setProperty("JDBC.host", environment.getMySQLHostname()); myBatisProperties.setProperty("JDBC.port", String.valueOf(environment.getMySQLPort())); myBatisProperties.setProperty("JDBC.schema", environment.getMySQLDatabase()); myBatisProperties.setProperty("JDBC.username", environment.getMySQLUsername()); myBatisProperties.setProperty("JDBC.password", environment.getMySQLPassword()); myBatisProperties.setProperty("JDBC.autoCommit", "false"); myBatisProperties.setProperty("mybatis.pooled.pingEnabled", "true"); myBatisProperties.setProperty("mybatis.pooled.pingQuery", "SELECT 1"); // Use UTF-8 in database driverProperties.setProperty("characterEncoding","UTF-8"); } @Override public void configure(Binder binder) { // Bind MySQL-specific properties JdbcHelper.MySQL.configure(binder); // Bind MyBatis properties Names.bindProperties(binder, myBatisProperties); // Bing JDBC driver properties binder.bind(Properties.class) .annotatedWith(Names.named("JDBC.driverProperties")) .toInstance(driverProperties); } } ././@LongLink0100644 0000000 0000000 00000000165 12645510255 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/001-create-schema.sqlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/001-c0100664 0001751 0001751 00000020167 12645510255 035047 0ustar00mjumpermjumper0000000 0000000 -- -- Copyright (C) 2013 Glyptodon LLC -- -- Permission is hereby granted, free of charge, to any person obtaining a copy -- of this software and associated documentation files (the "Software"), to deal -- in the Software without restriction, including without limitation the rights -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -- copies of the Software, and to permit persons to whom the Software is -- furnished to do so, subject to the following conditions: -- -- The above copyright notice and this permission notice shall be included in -- all copies or substantial portions of the Software. -- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -- THE SOFTWARE. -- -- -- Table of connection groups. Each connection group has a name. -- CREATE TABLE `guacamole_connection_group` ( `connection_group_id` int(11) NOT NULL AUTO_INCREMENT, `parent_id` int(11), `connection_group_name` varchar(128) NOT NULL, `type` enum('ORGANIZATIONAL', 'BALANCING') NOT NULL DEFAULT 'ORGANIZATIONAL', -- Concurrency limits `max_connections` int(11), `max_connections_per_user` int(11), PRIMARY KEY (`connection_group_id`), UNIQUE KEY `connection_group_name_parent` (`connection_group_name`, `parent_id`), CONSTRAINT `guacamole_connection_group_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `guacamole_connection_group` (`connection_group_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- -- Table of connections. Each connection has a name, protocol, and -- associated set of parameters. -- A connection may belong to a connection group. -- CREATE TABLE `guacamole_connection` ( `connection_id` int(11) NOT NULL AUTO_INCREMENT, `connection_name` varchar(128) NOT NULL, `parent_id` int(11), `protocol` varchar(32) NOT NULL, -- Concurrency limits `max_connections` int(11), `max_connections_per_user` int(11), PRIMARY KEY (`connection_id`), UNIQUE KEY `connection_name_parent` (`connection_name`, `parent_id`), CONSTRAINT `guacamole_connection_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `guacamole_connection_group` (`connection_group_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- -- Table of users. Each user has a unique username and a hashed password -- with corresponding salt. Although the authentication system will always set -- salted passwords, other systems may set unsalted passwords by simply not -- providing the salt. -- CREATE TABLE `guacamole_user` ( `user_id` int(11) NOT NULL AUTO_INCREMENT, -- Username and optionally-salted password `username` varchar(128) NOT NULL, `password_hash` binary(32) NOT NULL, `password_salt` binary(32), -- Account disabled/expired status `disabled` boolean NOT NULL DEFAULT 0, `expired` boolean NOT NULL DEFAULT 0, -- Time-based access restriction `access_window_start` TIME, `access_window_end` TIME, -- Date-based access restriction `valid_from` DATE, `valid_until` DATE, -- Timezone used for all date/time comparisons and interpretation `timezone` VARCHAR(64), PRIMARY KEY (`user_id`), UNIQUE KEY `username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- -- Table of connection parameters. Each parameter is simply a name/value pair -- associated with a connection. -- CREATE TABLE `guacamole_connection_parameter` ( `connection_id` int(11) NOT NULL, `parameter_name` varchar(128) NOT NULL, `parameter_value` varchar(4096) NOT NULL, PRIMARY KEY (`connection_id`,`parameter_name`), CONSTRAINT `guacamole_connection_parameter_ibfk_1` FOREIGN KEY (`connection_id`) REFERENCES `guacamole_connection` (`connection_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- -- Table of connection permissions. Each connection permission grants a user -- specific access to a connection. -- CREATE TABLE `guacamole_connection_permission` ( `user_id` int(11) NOT NULL, `connection_id` int(11) NOT NULL, `permission` enum('READ', 'UPDATE', 'DELETE', 'ADMINISTER') NOT NULL, PRIMARY KEY (`user_id`,`connection_id`,`permission`), CONSTRAINT `guacamole_connection_permission_ibfk_1` FOREIGN KEY (`connection_id`) REFERENCES `guacamole_connection` (`connection_id`) ON DELETE CASCADE, CONSTRAINT `guacamole_connection_permission_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `guacamole_user` (`user_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- -- Table of connection group permissions. Each group permission grants a user -- specific access to a connection group. -- CREATE TABLE `guacamole_connection_group_permission` ( `user_id` int(11) NOT NULL, `connection_group_id` int(11) NOT NULL, `permission` enum('READ', 'UPDATE', 'DELETE', 'ADMINISTER') NOT NULL, PRIMARY KEY (`user_id`,`connection_group_id`,`permission`), CONSTRAINT `guacamole_connection_group_permission_ibfk_1` FOREIGN KEY (`connection_group_id`) REFERENCES `guacamole_connection_group` (`connection_group_id`) ON DELETE CASCADE, CONSTRAINT `guacamole_connection_group_permission_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `guacamole_user` (`user_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- -- Table of system permissions. Each system permission grants a user a -- system-level privilege of some kind. -- CREATE TABLE `guacamole_system_permission` ( `user_id` int(11) NOT NULL, `permission` enum('CREATE_CONNECTION', 'CREATE_CONNECTION_GROUP', 'CREATE_USER', 'ADMINISTER') NOT NULL, PRIMARY KEY (`user_id`,`permission`), CONSTRAINT `guacamole_system_permission_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `guacamole_user` (`user_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- -- Table of user permissions. Each user permission grants a user access to -- another user (the "affected" user) for a specific type of operation. -- CREATE TABLE `guacamole_user_permission` ( `user_id` int(11) NOT NULL, `affected_user_id` int(11) NOT NULL, `permission` enum('READ', 'UPDATE', 'DELETE', 'ADMINISTER') NOT NULL, PRIMARY KEY (`user_id`,`affected_user_id`,`permission`), CONSTRAINT `guacamole_user_permission_ibfk_1` FOREIGN KEY (`affected_user_id`) REFERENCES `guacamole_user` (`user_id`) ON DELETE CASCADE, CONSTRAINT `guacamole_user_permission_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `guacamole_user` (`user_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- -- Table of connection history records. Each record defines a specific user's -- session, including the connection used, the start time, and the end time -- (if any). -- CREATE TABLE `guacamole_connection_history` ( `history_id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `connection_id` int(11) NOT NULL, `start_date` datetime NOT NULL, `end_date` datetime DEFAULT NULL, PRIMARY KEY (`history_id`), KEY `user_id` (`user_id`), KEY `connection_id` (`connection_id`), KEY `start_date` (`start_date`), KEY `end_date` (`end_date`), CONSTRAINT `guacamole_connection_history_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `guacamole_user` (`user_id`) ON DELETE CASCADE, CONSTRAINT `guacamole_connection_history_ibfk_2` FOREIGN KEY (`connection_id`) REFERENCES `guacamole_connection` (`connection_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ././@LongLink0100644 0000000 0000000 00000000171 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/002-create-admin-user.sqlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/002-c0100664 0001751 0001751 00000005066 12612226251 035043 0ustar00mjumpermjumper0000000 0000000 -- -- Copyright (C) 2015 Glyptodon LLC -- -- Permission is hereby granted, free of charge, to any person obtaining a copy -- of this software and associated documentation files (the "Software"), to deal -- in the Software without restriction, including without limitation the rights -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -- copies of the Software, and to permit persons to whom the Software is -- furnished to do so, subject to the following conditions: -- -- The above copyright notice and this permission notice shall be included in -- all copies or substantial portions of the Software. -- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -- THE SOFTWARE. -- -- Create default user "guacadmin" with password "guacadmin" INSERT INTO guacamole_user (username, password_hash, password_salt) VALUES ('guacadmin', x'CA458A7D494E3BE824F5E1E175A1556C0F8EEF2C2D7DF3633BEC4A29C4411960', -- 'guacadmin' x'FE24ADC5E11E2B25288D1704ABE67A79E342ECC26064CE69C5B3177795A82264'); -- Grant this user all system permissions INSERT INTO guacamole_system_permission SELECT user_id, permission FROM ( SELECT 'guacadmin' AS username, 'CREATE_CONNECTION' AS permission UNION SELECT 'guacadmin' AS username, 'CREATE_CONNECTION_GROUP' AS permission UNION SELECT 'guacadmin' AS username, 'CREATE_USER' AS permission UNION SELECT 'guacadmin' AS username, 'ADMINISTER' AS permission ) permissions JOIN guacamole_user ON permissions.username = guacamole_user.username; -- Grant admin permission to read/update/administer self INSERT INTO guacamole_user_permission SELECT guacamole_user.user_id, affected.user_id, permission FROM ( SELECT 'guacadmin' AS username, 'guacadmin' AS affected_username, 'READ' AS permission UNION SELECT 'guacadmin' AS username, 'guacadmin' AS affected_username, 'UPDATE' AS permission UNION SELECT 'guacadmin' AS username, 'guacadmin' AS affected_username, 'ADMINISTER' AS permission ) permissions JOIN guacamole_user ON permissions.username = guacamole_user.username JOIN guacamole_user affected ON permissions.affected_username = affected.username; ././@LongLink0100644 0000000 0000000 00000000175 12645510255 011643 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgrade/upgrade-pre-0.9.6.sqlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgra0100664 0001751 0001751 00000002757 12645510255 035452 0ustar00mjumpermjumper0000000 0000000 -- -- Copyright (C) 2015 Glyptodon LLC -- -- Permission is hereby granted, free of charge, to any person obtaining a copy -- of this software and associated documentation files (the "Software"), to deal -- in the Software without restriction, including without limitation the rights -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -- copies of the Software, and to permit persons to whom the Software is -- furnished to do so, subject to the following conditions: -- -- The above copyright notice and this permission notice shall be included in -- all copies or substantial portions of the Software. -- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -- THE SOFTWARE. -- -- -- Explicitly add permission for each user to READ him/herself -- INSERT INTO guacamole_user_permission (user_id, affected_user_id, permission) SELECT user_id, user_id, 'READ' FROM guacamole_user WHERE user_id NOT IN ( SELECT user_id FROM guacamole_user_permission WHERE user_id = affected_user_id AND permission = 'READ' ); ././@LongLink0100644 0000000 0000000 00000000175 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgrade/upgrade-pre-0.9.7.sqlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgra0100664 0001751 0001751 00000002511 12612226251 035430 0ustar00mjumpermjumper0000000 0000000 -- -- Copyright (C) 2015 Glyptodon LLC -- -- Permission is hereby granted, free of charge, to any person obtaining a copy -- of this software and associated documentation files (the "Software"), to deal -- in the Software without restriction, including without limitation the rights -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -- copies of the Software, and to permit persons to whom the Software is -- furnished to do so, subject to the following conditions: -- -- The above copyright notice and this permission notice shall be included in -- all copies or substantial portions of the Software. -- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -- THE SOFTWARE. -- -- -- Add per-user disable flag -- ALTER TABLE guacamole_user ADD COLUMN disabled BOOLEAN NOT NULL DEFAULT 0; -- -- Add per-user password expiration flag -- ALTER TABLE guacamole_user ADD COLUMN expired BOOLEAN NOT NULL DEFAULT 0; ././@LongLink0100644 0000000 0000000 00000000175 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgrade/upgrade-pre-0.8.2.sqlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgra0100664 0001751 0001751 00000006546 12612226251 035444 0ustar00mjumpermjumper0000000 0000000 -- -- Copyright (C) 2013 Glyptodon LLC -- -- Permission is hereby granted, free of charge, to any person obtaining a copy -- of this software and associated documentation files (the "Software"), to deal -- in the Software without restriction, including without limitation the rights -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -- copies of the Software, and to permit persons to whom the Software is -- furnished to do so, subject to the following conditions: -- -- The above copyright notice and this permission notice shall be included in -- all copies or substantial portions of the Software. -- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -- THE SOFTWARE. -- -- -- Table of connection groups. Each connection group has a name. -- CREATE TABLE `guacamole_connection_group` ( `connection_group_id` int(11) NOT NULL AUTO_INCREMENT, `parent_id` int(11), `connection_group_name` varchar(128) NOT NULL, `type` enum('ORGANIZATIONAL', 'BALANCING') NOT NULL DEFAULT 'ORGANIZATIONAL', PRIMARY KEY (`connection_group_id`), UNIQUE KEY `connection_group_name_parent` (`connection_group_name`, `parent_id`), CONSTRAINT `guacamole_connection_group_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `guacamole_connection_group` (`connection_group_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- -- Changes to connection table to support grouping. -- ALTER TABLE `guacamole_connection` ADD COLUMN `parent_id` int(11) AFTER `connection_name`; ALTER TABLE `guacamole_connection` DROP INDEX `connection_name`; ALTER TABLE `guacamole_connection` ADD UNIQUE KEY `connection_name_parent` (`connection_name`, `parent_id`); ALTER TABLE `guacamole_connection` ADD CONSTRAINT `guacamole_connection_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `guacamole_connection_group` (`connection_group_id`) ON DELETE CASCADE; -- -- Table of connection group permissions. Each group permission grants a user -- specific access to a connection group. -- CREATE TABLE `guacamole_connection_group_permission` ( `user_id` int(11) NOT NULL, `connection_group_id` int(11) NOT NULL, `permission` enum('READ', 'UPDATE', 'DELETE', 'ADMINISTER') NOT NULL, PRIMARY KEY (`user_id`,`connection_group_id`,`permission`), CONSTRAINT `guacamole_connection_group_permission_ibfk_1` FOREIGN KEY (`connection_group_id`) REFERENCES `guacamole_connection_group` (`connection_group_id`) ON DELETE CASCADE, CONSTRAINT `guacamole_connection_group_permission_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `guacamole_user` (`user_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ALTER TABLE `guacamole_system_permission` MODIFY `permission` enum('CREATE_CONNECTION', 'CREATE_CONNECTION_GROUP', 'CREATE_USER', 'ADMINISTER') NOT NULL; ././@LongLink0100644 0000000 0000000 00000000175 12645510255 011643 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgrade/upgrade-pre-0.9.9.sqlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgra0100664 0001751 0001751 00000002572 12645510255 035445 0ustar00mjumpermjumper0000000 0000000 -- -- Copyright (C) 2015 Glyptodon LLC -- -- Permission is hereby granted, free of charge, to any person obtaining a copy -- of this software and associated documentation files (the "Software"), to deal -- in the Software without restriction, including without limitation the rights -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -- copies of the Software, and to permit persons to whom the Software is -- furnished to do so, subject to the following conditions: -- -- The above copyright notice and this permission notice shall be included in -- all copies or substantial portions of the Software. -- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -- THE SOFTWARE. -- -- -- Ensure history entry start/end dates are indexed. -- ALTER TABLE guacamole_connection_history ADD KEY (start_date); ALTER TABLE guacamole_connection_history ADD KEY (end_date); ALTER TABLE guacamole_connection_history ADD KEY search_index (start_date, connection_id, user_id); ././@LongLink0100644 0000000 0000000 00000000175 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgrade/upgrade-pre-0.9.8.sqlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/schema/upgra0100664 0001751 0001751 00000003773 12612226251 035443 0ustar00mjumpermjumper0000000 0000000 -- -- Copyright (C) 2015 Glyptodon LLC -- -- Permission is hereby granted, free of charge, to any person obtaining a copy -- of this software and associated documentation files (the "Software"), to deal -- in the Software without restriction, including without limitation the rights -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -- copies of the Software, and to permit persons to whom the Software is -- furnished to do so, subject to the following conditions: -- -- The above copyright notice and this permission notice shall be included in -- all copies or substantial portions of the Software. -- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -- THE SOFTWARE. -- -- -- Add per-user time-based access restrictions. -- ALTER TABLE guacamole_user ADD COLUMN access_window_start TIME; ALTER TABLE guacamole_user ADD COLUMN access_window_end TIME; -- -- Add per-user date-based account validity restrictions. -- ALTER TABLE guacamole_user ADD COLUMN valid_from DATE; ALTER TABLE guacamole_user ADD COLUMN valid_until DATE; -- -- Add per-user timezone for sake of time comparisons/interpretation. -- ALTER TABLE guacamole_user ADD COLUMN timezone VARCHAR(64); -- -- Add connection concurrency limits -- ALTER TABLE guacamole_connection ADD COLUMN max_connections INT(11); ALTER TABLE guacamole_connection ADD COLUMN max_connections_per_user INT(11); -- -- Add connection group concurrency limits -- ALTER TABLE guacamole_connection_group ADD COLUMN max_connections INT(11); ALTER TABLE guacamole_connection_group ADD COLUMN max_connections_per_user INT(11); guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/pom.xml0100664 0001751 0001751 00000005410 12645510255 034453 0ustar00mjumpermjumper0000000 0000000 4.0.0 org.glyptodon.guacamole guacamole-auth-jdbc-mysql jar guacamole-auth-jdbc-mysql http://guac-dev.org/ UTF-8 org.glyptodon.guacamole guacamole-auth-jdbc 0.9.9 ../../ org.apache.maven.plugins maven-compiler-plugin 3.3 1.6 1.6 -Xlint:all -Werror true org.apache.maven.plugins maven-dependency-plugin 2.10 unpack-dependencies prepare-package unpack-dependencies runtime ${project.build.directory}/classes org.glyptodon.guacamole guacamole-ext provided org.glyptodon.guacamole guacamole-auth-jdbc-base 0.9.9 ././@LongLink0100644 0000000 0000000 00000000261 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/ParameterMapper.xmlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0100664 0001751 0001751 00000005666 12612226251 035463 0ustar00mjumpermjumper0000000 0000000 DELETE FROM guacamole_connection_parameter WHERE connection_id = #{identifier,jdbcType=INTEGER}::integer INSERT INTO guacamole_connection_parameter ( connection_id, parameter_name, parameter_value ) VALUES (#{parameter.connectionIdentifier,jdbcType=INTEGER}::integer, #{parameter.name,jdbcType=VARCHAR}, #{parameter.value,jdbcType=VARCHAR}) ././@LongLink0100644 0000000 0000000 00000000270 12645510255 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xmlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0100664 0001751 0001751 00000021306 12645510255 035456 0ustar00mjumpermjumper0000000 0000000 INSERT INTO guacamole_connection_history ( connection_id, user_id, start_date, end_date ) VALUES ( #{record.connectionIdentifier,jdbcType=INTEGER}::integer, #{record.userID,jdbcType=INTEGER}, #{record.startDate,jdbcType=TIMESTAMP}, #{record.endDate,jdbcType=TIMESTAMP} ) ././@LongLink0100644 0000000 0000000 00000000262 12625262336 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionMapper.xmlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0100664 0001751 0001751 00000016372 12625262336 035467 0ustar00mjumpermjumper0000000 0000000 DELETE FROM guacamole_connection WHERE connection_id = #{identifier,jdbcType=INTEGER}::integer INSERT INTO guacamole_connection ( connection_name, parent_id, protocol, max_connections, max_connections_per_user ) VALUES ( #{object.name,jdbcType=VARCHAR}, #{object.parentIdentifier,jdbcType=INTEGER}::integer, #{object.protocol,jdbcType=VARCHAR}, #{object.maxConnections,jdbcType=INTEGER}, #{object.maxConnectionsPerUser,jdbcType=INTEGER} ) UPDATE guacamole_connection SET connection_name = #{object.name,jdbcType=VARCHAR}, parent_id = #{object.parentIdentifier,jdbcType=INTEGER}::integer, protocol = #{object.protocol,jdbcType=VARCHAR}, max_connections = #{object.maxConnections,jdbcType=INTEGER}, max_connections_per_user = #{object.maxConnectionsPerUser,jdbcType=INTEGER} WHERE connection_id = #{object.objectID,jdbcType=INTEGER}::integer ././@LongLink0100644 0000000 0000000 00000000270 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/SystemPermissionMapper.xmlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0100664 0001751 0001751 00000007634 12612226251 035460 0ustar00mjumpermjumper0000000 0000000 DELETE FROM guacamole_system_permission WHERE (user_id, permission) IN (#{permission.userID,jdbcType=INTEGER}, #{permission.type,jdbcType=VARCHAR}::guacamole_system_permission_type) INSERT INTO guacamole_system_permission ( user_id, permission ) VALUES (#{permission.userID,jdbcType=INTEGER}, #{permission.type,jdbcType=VARCHAR}::guacamole_system_permission_type) ././@LongLink0100644 0000000 0000000 00000000301 12612226251 011624 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xmlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0100664 0001751 0001751 00000012557 12612226251 035460 0ustar00mjumpermjumper0000000 0000000 DELETE FROM guacamole_connection_group_permission WHERE (user_id, permission, connection_group_id) IN (#{permission.userID,jdbcType=INTEGER}, #{permission.type,jdbcType=VARCHAR}::guacamole_object_permission_type, #{permission.objectIdentifier,jdbcType=INTEGER}::integer) INSERT INTO guacamole_connection_group_permission ( user_id, permission, connection_group_id ) VALUES (#{permission.userID,jdbcType=INTEGER}, #{permission.type,jdbcType=VARCHAR}::guacamole_object_permission_type, #{permission.objectIdentifier,jdbcType=INTEGER}::integer) ././@LongLink0100644 0000000 0000000 00000000274 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xmlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0100664 0001751 0001751 00000012327 12612226251 035453 0ustar00mjumpermjumper0000000 0000000 DELETE FROM guacamole_connection_permission WHERE (user_id, permission, connection_id) IN (#{permission.userID,jdbcType=INTEGER}, #{permission.type,jdbcType=VARCHAR}::guacamole_object_permission_type, #{permission.objectIdentifier,jdbcType=INTEGER}::integer) INSERT INTO guacamole_connection_permission ( user_id, permission, connection_id ) VALUES (#{permission.userID,jdbcType=INTEGER}, #{permission.type,jdbcType=VARCHAR}::guacamole_object_permission_type, #{permission.objectIdentifier,jdbcType=INTEGER}::integer) ././@LongLink0100644 0000000 0000000 00000000266 12612226251 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/permission/UserPermissionMapper.xmlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0100664 0001751 0001751 00000014036 12612226251 035452 0ustar00mjumpermjumper0000000 0000000 DELETE FROM guacamole_user_permission USING guacamole_user affected WHERE guacamole_user_permission.affected_user_id = affected.user_id AND (guacamole_user_permission.user_id, permission, affected.username) IN (#{permission.userID,jdbcType=INTEGER}, #{permission.type,jdbcType=VARCHAR}::guacamole_object_permission_type, #{permission.objectIdentifier,jdbcType=INTEGER}) INSERT INTO guacamole_user_permission ( user_id, permission, affected_user_id ) SELECT permissions.user_id, permissions.permission, guacamole_user.user_id FROM SELECT #{permission.userID,jdbcType=INTEGER} AS user_id, #{permission.type,jdbcType=VARCHAR}::guacamole_object_permission_type AS permission, #{permission.objectIdentifier,jdbcType=INTEGER} AS username AS permissions JOIN guacamole_user ON guacamole_user.username = permissions.username; ././@LongLink0100644 0000000 0000000 00000000274 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xmlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0100664 0001751 0001751 00000017313 12612226251 035453 0ustar00mjumpermjumper0000000 0000000 DELETE FROM guacamole_connection_group WHERE connection_group_id = #{identifier,jdbcType=INTEGER}::integer INSERT INTO guacamole_connection_group ( connection_group_name, parent_id, type, max_connections, max_connections_per_user ) VALUES ( #{object.name,jdbcType=VARCHAR}, #{object.parentIdentifier,jdbcType=INTEGER}::integer, #{object.type,jdbcType=VARCHAR}::guacamole_connection_group_type, #{object.maxConnections,jdbcType=INTEGER}, #{object.maxConnectionsPerUser,jdbcType=INTEGER} ) UPDATE guacamole_connection_group SET connection_group_name = #{object.name,jdbcType=VARCHAR}, parent_id = #{object.parentIdentifier,jdbcType=INTEGER}::integer, type = #{object.type,jdbcType=VARCHAR}::guacamole_connection_group_type, max_connections = #{object.maxConnections,jdbcType=INTEGER}, max_connections_per_user = #{object.maxConnectionsPerUser,jdbcType=INTEGER} WHERE connection_group_id = #{object.objectID,jdbcType=INTEGER}::integer ././@LongLink0100644 0000000 0000000 00000000246 12625262336 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/glyptodon/guacamole/auth/jdbc/user/UserMapper.xmlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0100664 0001751 0001751 00000015761 12625262336 035470 0ustar00mjumpermjumper0000000 0000000 DELETE FROM guacamole_user WHERE username = #{identifier,jdbcType=VARCHAR} INSERT INTO guacamole_user ( username, password_hash, password_salt, disabled, expired, access_window_start, access_window_end, valid_from, valid_until, timezone ) VALUES ( #{object.identifier,jdbcType=VARCHAR}, #{object.passwordHash,jdbcType=BINARY}, #{object.passwordSalt,jdbcType=BINARY}, #{object.disabled,jdbcType=BOOLEAN}, #{object.expired,jdbcType=BOOLEAN}, #{object.accessWindowStart,jdbcType=TIME}, #{object.accessWindowEnd,jdbcType=TIME}, #{object.validFrom,jdbcType=DATE}, #{object.validUntil,jdbcType=DATE}, #{object.timeZone,jdbcType=VARCHAR} ) UPDATE guacamole_user SET password_hash = #{object.passwordHash,jdbcType=BINARY}, password_salt = #{object.passwordSalt,jdbcType=BINARY}, disabled = #{object.disabled,jdbcType=BOOLEAN}, expired = #{object.expired,jdbcType=BOOLEAN}, access_window_start = #{object.accessWindowStart,jdbcType=TIME}, access_window_end = #{object.accessWindowEnd,jdbcType=TIME}, valid_from = #{object.validFrom,jdbcType=DATE}, valid_until = #{object.validUntil,jdbcType=DATE}, timezone = #{object.timeZone,jdbcType=VARCHAR} WHERE user_id = #{object.objectID,jdbcType=VARCHAR} ././@LongLink0100644 0000000 0000000 00000000203 12645510255 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/guac-manifest.jsonguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0100664 0001751 0001751 00000000556 12645510255 035462 0ustar00mjumpermjumper0000000 0000000 { "guacamoleVersion" : "0.9.9", "name" : "PostgreSQL Authentication", "namespace" : "guac-postgresql", "authProviders" : [ "org.glyptodon.guacamole.auth.postgresql.PostgreSQLAuthenticationProvider" ], "translations" : [ "translations/en.json", "translations/fr.json", "translations/ru.json" ] } ././@LongLink0100644 0000000 0000000 00000000271 12645510255 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/auth/postgresql/PostgreSQLAuthenticationProvider.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0100664 0001751 0001751 00000010701 12645510255 035453 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.postgresql; import com.google.inject.Guice; import com.google.inject.Injector; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.auth.jdbc.JDBCAuthenticationProviderModule; import org.glyptodon.guacamole.auth.jdbc.JDBCEnvironment; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticationProviderService; import org.glyptodon.guacamole.net.auth.AuthenticatedUser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Provides a PostgreSQL-based implementation of the AuthenticationProvider * functionality. * * @author James Muehlner * @author Michael Jumper */ public class PostgreSQLAuthenticationProvider implements AuthenticationProvider { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(PostgreSQLAuthenticationProvider.class); /** * Injector which will manage the object graph of this authentication * provider. */ private final Injector injector; /** * Creates a new PostgreSQLAuthenticationProvider that reads and writes * authentication data to a PostgreSQL database defined by properties in * guacamole.properties. * * @throws GuacamoleException * If a required property is missing, or an error occurs while parsing * a property. */ public PostgreSQLAuthenticationProvider() throws GuacamoleException { // Get local environment PostgreSQLEnvironment environment = new PostgreSQLEnvironment(); // Set up Guice injector. injector = Guice.createInjector( // Configure PostgreSQL-specific authentication new PostgreSQLAuthenticationProviderModule(environment), // Configure JDBC authentication core new JDBCAuthenticationProviderModule(this, environment) ); } @Override public String getIdentifier() { return "postgresql"; } @Override public AuthenticatedUser authenticateUser(Credentials credentials) throws GuacamoleException { // Create AuthenticatedUser based on credentials, if valid AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class); return authProviderService.authenticateUser(this, credentials); } @Override public AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser, Credentials credentials) throws GuacamoleException { // No need to update authenticated users return authenticatedUser; } @Override public UserContext getUserContext(AuthenticatedUser authenticatedUser) throws GuacamoleException { // Create UserContext based on credentials, if valid AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class); return authProviderService.getUserContext(authenticatedUser); } @Override public UserContext updateUserContext(UserContext context, AuthenticatedUser authenticatedUser) throws GuacamoleException { // No need to update the context return context; } } ././@LongLink0100644 0000000 0000000 00000000277 12645510255 011646 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/auth/postgresql/PostgreSQLAuthenticationProviderModule.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0100664 0001751 0001751 00000007407 12645510255 035464 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.postgresql; import com.google.inject.Binder; import com.google.inject.Module; import com.google.inject.name.Names; import java.util.Properties; import org.glyptodon.guacamole.GuacamoleException; import org.mybatis.guice.datasource.helper.JdbcHelper; /** * Guice module which configures PostgreSQL-specific injections. * * @author James Muehlner * @author Michael Jumper */ public class PostgreSQLAuthenticationProviderModule implements Module { /** * MyBatis-specific configuration properties. */ private final Properties myBatisProperties = new Properties(); /** * PostgreSQL-specific driver configuration properties. */ private final Properties driverProperties = new Properties(); /** * Creates a new PostgreSQL authentication provider module that configures * driver and MyBatis properties using the given environment. * * @param environment * The environment to use when configuring MyBatis and the underlying * JDBC driver. * * @throws GuacamoleException * If a required property is missing, or an error occurs while parsing * a property. */ public PostgreSQLAuthenticationProviderModule(PostgreSQLEnvironment environment) throws GuacamoleException { // Set the PostgreSQL-specific properties for MyBatis. myBatisProperties.setProperty("mybatis.environment.id", "guacamole"); myBatisProperties.setProperty("JDBC.host", environment.getPostgreSQLHostname()); myBatisProperties.setProperty("JDBC.port", String.valueOf(environment.getPostgreSQLPort())); myBatisProperties.setProperty("JDBC.schema", environment.getPostgreSQLDatabase()); myBatisProperties.setProperty("JDBC.username", environment.getPostgreSQLUsername()); myBatisProperties.setProperty("JDBC.password", environment.getPostgreSQLPassword()); myBatisProperties.setProperty("JDBC.autoCommit", "false"); myBatisProperties.setProperty("mybatis.pooled.pingEnabled", "true"); myBatisProperties.setProperty("mybatis.pooled.pingQuery", "SELECT 1"); // Use UTF-8 in database driverProperties.setProperty("characterEncoding","UTF-8"); } @Override public void configure(Binder binder) { // Bind PostgreSQL-specific properties JdbcHelper.PostgreSQL.configure(binder); // Bind MyBatis properties Names.bindProperties(binder, myBatisProperties); // Bing JDBC driver properties binder.bind(Properties.class) .annotatedWith(Names.named("JDBC.driverProperties")) .toInstance(driverProperties); } } ././@LongLink0100644 0000000 0000000 00000000245 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/auth/postgresql/package-info.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0100664 0001751 0001751 00000002303 12612226251 035444 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The PostgreSQL authentication provider. */ package org.glyptodon.guacamole.auth.postgresql; ././@LongLink0100644 0000000 0000000 00000000266 12625262336 011646 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/auth/postgresql/PostgreSQLGuacamoleProperties.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0100664 0001751 0001751 00000013200 12625262336 035452 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.postgresql; import org.glyptodon.guacamole.properties.BooleanGuacamoleProperty; import org.glyptodon.guacamole.properties.IntegerGuacamoleProperty; import org.glyptodon.guacamole.properties.StringGuacamoleProperty; /** * Properties used by the PostgreSQL Authentication plugin. * * @author James Muehlner * @author Michael Jumper */ public class PostgreSQLGuacamoleProperties { /** * This class should not be instantiated. */ private PostgreSQLGuacamoleProperties() {} /** * The URL of the PostgreSQL server hosting the Guacamole authentication tables. */ public static final StringGuacamoleProperty POSTGRESQL_HOSTNAME = new StringGuacamoleProperty() { @Override public String getName() { return "postgresql-hostname"; } }; /** * The port of the PostgreSQL server hosting the Guacamole authentication * tables. */ public static final IntegerGuacamoleProperty POSTGRESQL_PORT = new IntegerGuacamoleProperty() { @Override public String getName() { return "postgresql-port"; } }; /** * The name of the PostgreSQL database containing the Guacamole * authentication tables. */ public static final StringGuacamoleProperty POSTGRESQL_DATABASE = new StringGuacamoleProperty() { @Override public String getName() { return "postgresql-database"; } }; /** * The username used to authenticate to the PostgreSQL database containing * the Guacamole authentication tables. */ public static final StringGuacamoleProperty POSTGRESQL_USERNAME = new StringGuacamoleProperty() { @Override public String getName() { return "postgresql-username"; } }; /** * The password used to authenticate to the PostgreSQL database containing * the Guacamole authentication tables. */ public static final StringGuacamoleProperty POSTGRESQL_PASSWORD = new StringGuacamoleProperty() { @Override public String getName() { return "postgresql-password"; } }; /** * Whether or not multiple users accessing the same connection at the same * time should be disallowed. */ public static final BooleanGuacamoleProperty POSTGRESQL_DISALLOW_SIMULTANEOUS_CONNECTIONS = new BooleanGuacamoleProperty() { @Override public String getName() { return "postgresql-disallow-simultaneous-connections"; } }; /** * Whether or not the same user accessing the same connection or connection * group at the same time should be disallowed. */ public static final BooleanGuacamoleProperty POSTGRESQL_DISALLOW_DUPLICATE_CONNECTIONS = new BooleanGuacamoleProperty() { @Override public String getName() { return "postgresql-disallow-duplicate-connections"; } }; /** * The maximum number of concurrent connections to allow to any one * connection. Zero denotes unlimited. */ public static final IntegerGuacamoleProperty POSTGRESQL_DEFAULT_MAX_CONNECTIONS = new IntegerGuacamoleProperty() { @Override public String getName() { return "postgresql-default-max-connections"; } }; /** * The maximum number of concurrent connections to allow to any one * connection group. Zero denotes unlimited. */ public static final IntegerGuacamoleProperty POSTGRESQL_DEFAULT_MAX_GROUP_CONNECTIONS = new IntegerGuacamoleProperty() { @Override public String getName() { return "postgresql-default-max-group-connections"; } }; /** * The maximum number of concurrent connections to allow to any one * connection by an individual user. Zero denotes unlimited. */ public static final IntegerGuacamoleProperty POSTGRESQL_DEFAULT_MAX_CONNECTIONS_PER_USER = new IntegerGuacamoleProperty() { @Override public String getName() { return "postgresql-default-max-connections-per-user"; } }; /** * The maximum number of concurrent connections to allow to any one * connection group by an individual user. Zero denotes * unlimited. */ public static final IntegerGuacamoleProperty POSTGRESQL_DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER = new IntegerGuacamoleProperty() { @Override public String getName() { return "postgresql-default-max-group-connections-per-user"; } }; } ././@LongLink0100644 0000000 0000000 00000000256 12645510255 011643 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/glyptodon/guacamole/auth/postgresql/PostgreSQLEnvironment.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/mai0100664 0001751 0001751 00000026553 12645510255 035467 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.postgresql; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.auth.jdbc.JDBCEnvironment; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A PostgreSQL-specific implementation of JDBCEnvironment provides database * properties specifically for PostgreSQL. * * @author Michael Jumper */ public class PostgreSQLEnvironment extends JDBCEnvironment { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(PostgreSQLEnvironment.class); /** * The default host to connect to, if POSTGRESQL_HOSTNAME is not specified. */ private static final String DEFAULT_HOSTNAME = "localhost"; /** * The default port to connect to, if POSTGRESQL_PORT is not specified. */ private static final int DEFAULT_PORT = 5432; /** * The default value for the default maximum number of connections to be * allowed per user to any one connection. Note that, as long as the * legacy "disallow duplicate" and "disallow simultaneous" properties are * still supported, these cannot be constants, as the legacy properties * dictate the values that should be used in the absence of the correct * properties. */ private int DEFAULT_MAX_CONNECTIONS_PER_USER = 1; /** * The default value for the default maximum number of connections to be * allowed per user to any one connection group. Note that, as long as the * legacy "disallow duplicate" and "disallow simultaneous" properties are * still supported, these cannot be constants, as the legacy properties * dictate the values that should be used in the absence of the correct * properties. */ private int DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER = 1; /** * The default value for the default maximum number of connections to be * allowed to any one connection. Note that, as long as the legacy * "disallow duplicate" and "disallow simultaneous" properties are still * supported, these cannot be constants, as the legacy properties dictate * the values that should be used in the absence of the correct properties. */ private int DEFAULT_MAX_CONNECTIONS = 0; /** * The default value for the default maximum number of connections to be * allowed to any one connection group. Note that, as long as the legacy * "disallow duplicate" and "disallow simultaneous" properties are still * supported, these cannot be constants, as the legacy properties dictate * the values that should be used in the absence of the correct properties. */ private int DEFAULT_MAX_GROUP_CONNECTIONS = 0; /** * Constructs a new PostgreSQLEnvironment, providing access to PostgreSQL-specific * configuration options. * * @throws GuacamoleException * If an error occurs while setting up the underlying JDBCEnvironment * or while parsing legacy PostgreSQL configuration options. */ public PostgreSQLEnvironment() throws GuacamoleException { // Init underlying JDBC environment super(); // Read legacy concurrency-related property Boolean disallowSimultaneous = getProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_DISALLOW_SIMULTANEOUS_CONNECTIONS); Boolean disallowDuplicate = getProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_DISALLOW_DUPLICATE_CONNECTIONS); // Legacy "simultaneous" property dictates only the maximum number of // connections per connection if (disallowSimultaneous != null) { // Translate legacy property if (disallowSimultaneous) { DEFAULT_MAX_CONNECTIONS = 1; DEFAULT_MAX_GROUP_CONNECTIONS = 0; } else { DEFAULT_MAX_CONNECTIONS = 0; DEFAULT_MAX_GROUP_CONNECTIONS = 0; } // Warn of deprecation logger.warn("The \"{}\" property is deprecated. Use \"{}\" and \"{}\" instead.", PostgreSQLGuacamoleProperties.POSTGRESQL_DISALLOW_SIMULTANEOUS_CONNECTIONS.getName(), PostgreSQLGuacamoleProperties.POSTGRESQL_DEFAULT_MAX_CONNECTIONS.getName(), PostgreSQLGuacamoleProperties.POSTGRESQL_DEFAULT_MAX_GROUP_CONNECTIONS.getName()); // Inform of new equivalent logger.info("To achieve the same result of setting \"{}\" to \"{}\", set \"{}\" to \"{}\" and \"{}\" to \"{}\".", PostgreSQLGuacamoleProperties.POSTGRESQL_DISALLOW_SIMULTANEOUS_CONNECTIONS.getName(), disallowSimultaneous, PostgreSQLGuacamoleProperties.POSTGRESQL_DEFAULT_MAX_CONNECTIONS.getName(), DEFAULT_MAX_CONNECTIONS, PostgreSQLGuacamoleProperties.POSTGRESQL_DEFAULT_MAX_GROUP_CONNECTIONS.getName(), DEFAULT_MAX_GROUP_CONNECTIONS); } // Legacy "duplicate" property dictates whether connections and groups // may be used concurrently only by different users if (disallowDuplicate != null) { // Translate legacy property if (disallowDuplicate) { DEFAULT_MAX_CONNECTIONS_PER_USER = 1; DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER = 1; } else { DEFAULT_MAX_CONNECTIONS_PER_USER = 0; DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER = 0; } // Warn of deprecation logger.warn("The \"{}\" property is deprecated. Use \"{}\" and \"{}\" instead.", PostgreSQLGuacamoleProperties.POSTGRESQL_DISALLOW_DUPLICATE_CONNECTIONS.getName(), PostgreSQLGuacamoleProperties.POSTGRESQL_DEFAULT_MAX_CONNECTIONS_PER_USER.getName(), PostgreSQLGuacamoleProperties.POSTGRESQL_DEFAULT_MAX_GROUP_CONNECTIONS.getName()); // Inform of new equivalent logger.info("To achieve the same result of setting \"{}\" to \"{}\", set \"{}\" to \"{}\" and \"{}\" to \"{}\".", PostgreSQLGuacamoleProperties.POSTGRESQL_DISALLOW_DUPLICATE_CONNECTIONS.getName(), disallowDuplicate, PostgreSQLGuacamoleProperties.POSTGRESQL_DEFAULT_MAX_CONNECTIONS_PER_USER.getName(), DEFAULT_MAX_CONNECTIONS_PER_USER, PostgreSQLGuacamoleProperties.POSTGRESQL_DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER.getName(), DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER); } } @Override public int getDefaultMaxConnections() throws GuacamoleException { return getProperty( PostgreSQLGuacamoleProperties.POSTGRESQL_DEFAULT_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS ); } @Override public int getDefaultMaxGroupConnections() throws GuacamoleException { return getProperty( PostgreSQLGuacamoleProperties.POSTGRESQL_DEFAULT_MAX_GROUP_CONNECTIONS, DEFAULT_MAX_GROUP_CONNECTIONS ); } @Override public int getDefaultMaxConnectionsPerUser() throws GuacamoleException { return getProperty( PostgreSQLGuacamoleProperties.POSTGRESQL_DEFAULT_MAX_CONNECTIONS_PER_USER, DEFAULT_MAX_CONNECTIONS_PER_USER ); } @Override public int getDefaultMaxGroupConnectionsPerUser() throws GuacamoleException { return getProperty( PostgreSQLGuacamoleProperties.POSTGRESQL_DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER, DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER ); } /** * Returns the hostname of the PostgreSQL server hosting the Guacamole * authentication tables. If unspecified, this will be "localhost". * * @return * The URL of the PostgreSQL server. * * @throws GuacamoleException * If an error occurs while retrieving the property value. */ public String getPostgreSQLHostname() throws GuacamoleException { return getProperty( PostgreSQLGuacamoleProperties.POSTGRESQL_HOSTNAME, DEFAULT_HOSTNAME ); } /** * Returns the port number of the PostgreSQL server hosting the Guacamole * authentication tables. If unspecified, this will be the default * PostgreSQL port of 5432. * * @return * The port number of the PostgreSQL server. * * @throws GuacamoleException * If an error occurs while retrieving the property value. */ public int getPostgreSQLPort() throws GuacamoleException { return getProperty( PostgreSQLGuacamoleProperties.POSTGRESQL_PORT, DEFAULT_PORT ); } /** * Returns the name of the PostgreSQL database containing the Guacamole * authentication tables. * * @return * The name of the PostgreSQL database. * * @throws GuacamoleException * If an error occurs while retrieving the property value, or if the * value was not set, as this property is required. */ public String getPostgreSQLDatabase() throws GuacamoleException { return getRequiredProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_DATABASE); } /** * Returns the username that should be used when authenticating with the * PostgreSQL database containing the Guacamole authentication tables. * * @return * The username for the PostgreSQL database. * * @throws GuacamoleException * If an error occurs while retrieving the property value, or if the * value was not set, as this property is required. */ public String getPostgreSQLUsername() throws GuacamoleException { return getRequiredProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_USERNAME); } /** * Returns the password that should be used when authenticating with the * PostgreSQL database containing the Guacamole authentication tables. * * @return * The password for the PostgreSQL database. * * @throws GuacamoleException * If an error occurs while retrieving the property value, or if the * value was not set, as this property is required. */ public String getPostgreSQLPassword() throws GuacamoleException { return getRequiredProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_PASSWORD); } } ././@LongLink0100644 0000000 0000000 00000000172 12645510255 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/001-create-schema.sqlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/0100664 0001751 0001751 00000020516 12645510255 035442 0ustar00mjumpermjumper0000000 0000000 -- -- Copyright (C) 2015 Glyptodon LLC -- -- Permission is hereby granted, free of charge, to any person obtaining a copy -- of this software and associated documentation files (the "Software"), to deal -- in the Software without restriction, including without limitation the rights -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -- copies of the Software, and to permit persons to whom the Software is -- furnished to do so, subject to the following conditions: -- -- The above copyright notice and this permission notice shall be included in -- all copies or substantial portions of the Software. -- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -- THE SOFTWARE. -- -- -- Connection group types -- CREATE TYPE guacamole_connection_group_type AS ENUM( 'ORGANIZATIONAL', 'BALANCING' ); -- -- Object permission types -- CREATE TYPE guacamole_object_permission_type AS ENUM( 'READ', 'UPDATE', 'DELETE', 'ADMINISTER' ); -- -- System permission types -- CREATE TYPE guacamole_system_permission_type AS ENUM( 'CREATE_CONNECTION', 'CREATE_CONNECTION_GROUP', 'CREATE_USER', 'ADMINISTER' ); -- -- Table of connection groups. Each connection group has a name. -- CREATE TABLE guacamole_connection_group ( connection_group_id serial NOT NULL, parent_id integer, connection_group_name varchar(128) NOT NULL, type guacamole_connection_group_type NOT NULL DEFAULT 'ORGANIZATIONAL', -- Concurrency limits max_connections integer, max_connections_per_user integer, PRIMARY KEY (connection_group_id), CONSTRAINT connection_group_name_parent UNIQUE (connection_group_name, parent_id), CONSTRAINT guacamole_connection_group_ibfk_1 FOREIGN KEY (parent_id) REFERENCES guacamole_connection_group (connection_group_id) ON DELETE CASCADE ); CREATE INDEX ON guacamole_connection_group(parent_id); -- -- Table of connections. Each connection has a name, protocol, and -- associated set of parameters. -- A connection may belong to a connection group. -- CREATE TABLE guacamole_connection ( connection_id serial NOT NULL, connection_name varchar(128) NOT NULL, parent_id integer, protocol varchar(32) NOT NULL, -- Concurrency limits max_connections integer, max_connections_per_user integer, PRIMARY KEY (connection_id), CONSTRAINT connection_name_parent UNIQUE (connection_name, parent_id), CONSTRAINT guacamole_connection_ibfk_1 FOREIGN KEY (parent_id) REFERENCES guacamole_connection_group (connection_group_id) ON DELETE CASCADE ); CREATE INDEX ON guacamole_connection(parent_id); -- -- Table of users. Each user has a unique username and a hashed password -- with corresponding salt. Although the authentication system will always set -- salted passwords, other systems may set unsalted passwords by simply not -- providing the salt. -- CREATE TABLE guacamole_user ( user_id serial NOT NULL, -- Username and optionally-salted password username varchar(128) NOT NULL, password_hash bytea NOT NULL, password_salt bytea, -- Account disabled/expired status disabled boolean NOT NULL DEFAULT FALSE, expired boolean NOT NULL DEFAULT FALSE, -- Time-based access restriction access_window_start time, access_window_end time, -- Date-based access restriction valid_from date, valid_until date, -- Timezone used for all date/time comparisons and interpretation timezone varchar(64), PRIMARY KEY (user_id), CONSTRAINT username UNIQUE (username) ); -- -- Table of connection parameters. Each parameter is simply a name/value pair -- associated with a connection. -- CREATE TABLE guacamole_connection_parameter ( connection_id integer NOT NULL, parameter_name varchar(128) NOT NULL, parameter_value varchar(4096) NOT NULL, PRIMARY KEY (connection_id,parameter_name), CONSTRAINT guacamole_connection_parameter_ibfk_1 FOREIGN KEY (connection_id) REFERENCES guacamole_connection (connection_id) ON DELETE CASCADE ); CREATE INDEX ON guacamole_connection_parameter(connection_id); -- -- Table of connection permissions. Each connection permission grants a user -- specific access to a connection. -- CREATE TABLE guacamole_connection_permission ( user_id integer NOT NULL, connection_id integer NOT NULL, permission guacamole_object_permission_type NOT NULL, PRIMARY KEY (user_id,connection_id,permission), CONSTRAINT guacamole_connection_permission_ibfk_1 FOREIGN KEY (connection_id) REFERENCES guacamole_connection (connection_id) ON DELETE CASCADE, CONSTRAINT guacamole_connection_permission_ibfk_2 FOREIGN KEY (user_id) REFERENCES guacamole_user (user_id) ON DELETE CASCADE ); CREATE INDEX ON guacamole_connection_permission(connection_id); CREATE INDEX ON guacamole_connection_permission(user_id); -- -- Table of connection group permissions. Each group permission grants a user -- specific access to a connection group. -- CREATE TABLE guacamole_connection_group_permission ( user_id integer NOT NULL, connection_group_id integer NOT NULL, permission guacamole_object_permission_type NOT NULL, PRIMARY KEY (user_id,connection_group_id,permission), CONSTRAINT guacamole_connection_group_permission_ibfk_1 FOREIGN KEY (connection_group_id) REFERENCES guacamole_connection_group (connection_group_id) ON DELETE CASCADE, CONSTRAINT guacamole_connection_group_permission_ibfk_2 FOREIGN KEY (user_id) REFERENCES guacamole_user (user_id) ON DELETE CASCADE ); CREATE INDEX ON guacamole_connection_group_permission(connection_group_id); CREATE INDEX ON guacamole_connection_group_permission(user_id); -- -- Table of system permissions. Each system permission grants a user a -- system-level privilege of some kind. -- CREATE TABLE guacamole_system_permission ( user_id integer NOT NULL, permission guacamole_system_permission_type NOT NULL, PRIMARY KEY (user_id,permission), CONSTRAINT guacamole_system_permission_ibfk_1 FOREIGN KEY (user_id) REFERENCES guacamole_user (user_id) ON DELETE CASCADE ); CREATE INDEX ON guacamole_system_permission(user_id); -- -- Table of user permissions. Each user permission grants a user access to -- another user (the "affected" user) for a specific type of operation. -- CREATE TABLE guacamole_user_permission ( user_id integer NOT NULL, affected_user_id integer NOT NULL, permission guacamole_object_permission_type NOT NULL, PRIMARY KEY (user_id,affected_user_id,permission), CONSTRAINT guacamole_user_permission_ibfk_1 FOREIGN KEY (affected_user_id) REFERENCES guacamole_user (user_id) ON DELETE CASCADE, CONSTRAINT guacamole_user_permission_ibfk_2 FOREIGN KEY (user_id) REFERENCES guacamole_user (user_id) ON DELETE CASCADE ); CREATE INDEX ON guacamole_user_permission(affected_user_id); CREATE INDEX ON guacamole_user_permission(user_id); -- -- Table of connection history records. Each record defines a specific user's -- session, including the connection used, the start time, and the end time -- (if any). -- CREATE TABLE guacamole_connection_history ( history_id serial NOT NULL, user_id integer NOT NULL, connection_id integer NOT NULL, start_date timestamptz NOT NULL, end_date timestamptz DEFAULT NULL, PRIMARY KEY (history_id), CONSTRAINT guacamole_connection_history_ibfk_1 FOREIGN KEY (user_id) REFERENCES guacamole_user (user_id) ON DELETE CASCADE, CONSTRAINT guacamole_connection_history_ibfk_2 FOREIGN KEY (connection_id) REFERENCES guacamole_connection (connection_id) ON DELETE CASCADE ); CREATE INDEX ON guacamole_connection_history(user_id); CREATE INDEX ON guacamole_connection_history(connection_id); CREATE INDEX ON guacamole_connection_history(start_date); CREATE INDEX ON guacamole_connection_history(end_date); ././@LongLink0100644 0000000 0000000 00000000176 12612226251 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/002-create-admin-user.sqlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/0100664 0001751 0001751 00000004612 12612226251 035433 0ustar00mjumpermjumper0000000 0000000 -- -- Copyright (C) 2015 Glyptodon LLC -- -- Permission is hereby granted, free of charge, to any person obtaining a copy -- of this software and associated documentation files (the "Software"), to deal -- in the Software without restriction, including without limitation the rights -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -- copies of the Software, and to permit persons to whom the Software is -- furnished to do so, subject to the following conditions: -- -- The above copyright notice and this permission notice shall be included in -- all copies or substantial portions of the Software. -- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -- THE SOFTWARE. -- -- Create default user "guacadmin" with password "guacadmin" INSERT INTO guacamole_user (username, password_hash, password_salt) VALUES ('guacadmin', E'\\xCA458A7D494E3BE824F5E1E175A1556C0F8EEF2C2D7DF3633BEC4A29C4411960', -- 'guacadmin' E'\\xFE24ADC5E11E2B25288D1704ABE67A79E342ECC26064CE69C5B3177795A82264'); -- Grant this user all system permissions INSERT INTO guacamole_system_permission SELECT user_id, permission::guacamole_system_permission_type FROM ( VALUES ('guacadmin', 'CREATE_CONNECTION'), ('guacadmin', 'CREATE_CONNECTION_GROUP'), ('guacadmin', 'CREATE_USER'), ('guacadmin', 'ADMINISTER') ) permissions (username, permission) JOIN guacamole_user ON permissions.username = guacamole_user.username; -- Grant admin permission to read/update/administer self INSERT INTO guacamole_user_permission SELECT guacamole_user.user_id, affected.user_id, permission::guacamole_object_permission_type FROM ( VALUES ('guacadmin', 'guacadmin', 'READ'), ('guacadmin', 'guacadmin', 'UPDATE'), ('guacadmin', 'guacadmin', 'ADMINISTER') ) permissions (username, affected_username, permission) JOIN guacamole_user ON permissions.username = guacamole_user.username JOIN guacamole_user affected ON permissions.affected_username = affected.username; ././@LongLink0100644 0000000 0000000 00000000202 12612226251 011624 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/upgrade/upgrade-pre-0.9.7.sqlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/0100664 0001751 0001751 00000002521 12612226251 035430 0ustar00mjumpermjumper0000000 0000000 -- -- Copyright (C) 2015 Glyptodon LLC -- -- Permission is hereby granted, free of charge, to any person obtaining a copy -- of this software and associated documentation files (the "Software"), to deal -- in the Software without restriction, including without limitation the rights -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -- copies of the Software, and to permit persons to whom the Software is -- furnished to do so, subject to the following conditions: -- -- The above copyright notice and this permission notice shall be included in -- all copies or substantial portions of the Software. -- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -- THE SOFTWARE. -- -- -- Add per-user disable flag -- ALTER TABLE guacamole_user ADD COLUMN disabled boolean NOT NULL DEFAULT FALSE; -- -- Add per-user password expiration flag -- ALTER TABLE guacamole_user ADD COLUMN expired boolean NOT NULL DEFAULT FALSE; ././@LongLink0100644 0000000 0000000 00000000202 12645510255 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/upgrade/upgrade-pre-0.9.9.sqlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/0100664 0001751 0001751 00000002610 12645510255 035435 0ustar00mjumpermjumper0000000 0000000 -- -- Copyright (C) 2015 Glyptodon LLC -- -- Permission is hereby granted, free of charge, to any person obtaining a copy -- of this software and associated documentation files (the "Software"), to deal -- in the Software without restriction, including without limitation the rights -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -- copies of the Software, and to permit persons to whom the Software is -- furnished to do so, subject to the following conditions: -- -- The above copyright notice and this permission notice shall be included in -- all copies or substantial portions of the Software. -- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -- THE SOFTWARE. -- -- -- Ensure history entry start/end dates are indexed. -- CREATE INDEX ON guacamole_connection_history(start_date); CREATE INDEX ON guacamole_connection_history(end_date); CREATE INDEX guacamole_connection_history_search_index ON guacamole_connection_history(start_date, connection_id, user_id); ././@LongLink0100644 0000000 0000000 00000000202 12612226251 011624 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/upgrade/upgrade-pre-0.9.8.sqlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/schema/0100664 0001751 0001751 00000003773 12612226251 035442 0ustar00mjumpermjumper0000000 0000000 -- -- Copyright (C) 2015 Glyptodon LLC -- -- Permission is hereby granted, free of charge, to any person obtaining a copy -- of this software and associated documentation files (the "Software"), to deal -- in the Software without restriction, including without limitation the rights -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -- copies of the Software, and to permit persons to whom the Software is -- furnished to do so, subject to the following conditions: -- -- The above copyright notice and this permission notice shall be included in -- all copies or substantial portions of the Software. -- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -- THE SOFTWARE. -- -- -- Add per-user time-based access restrictions. -- ALTER TABLE guacamole_user ADD COLUMN access_window_start time; ALTER TABLE guacamole_user ADD COLUMN access_window_end time; -- -- Add per-user date-based account validity restrictions. -- ALTER TABLE guacamole_user ADD COLUMN valid_from date; ALTER TABLE guacamole_user ADD COLUMN valid_until date; -- -- Add per-user timezone for sake of time comparisons/interpretation. -- ALTER TABLE guacamole_user ADD COLUMN timezone varchar(64); -- -- Add connection concurrency limits -- ALTER TABLE guacamole_connection ADD COLUMN max_connections integer; ALTER TABLE guacamole_connection ADD COLUMN max_connections_per_user integer; -- -- Add connection group concurrency limits -- ALTER TABLE guacamole_connection_group ADD COLUMN max_connections integer; ALTER TABLE guacamole_connection_group ADD COLUMN max_connections_per_user integer; ././@LongLink0100644 0000000 0000000 00000000145 12645510255 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/pom.xmlguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/pom.xml0100664 0001751 0001751 00000005422 12645510255 035514 0ustar00mjumpermjumper0000000 0000000 4.0.0 org.glyptodon.guacamole guacamole-auth-jdbc-postgresql jar guacamole-auth-jdbc-postgresql http://guac-dev.org/ UTF-8 org.glyptodon.guacamole guacamole-auth-jdbc 0.9.9 ../../ org.apache.maven.plugins maven-compiler-plugin 3.3 1.6 1.6 -Xlint:all -Werror true org.apache.maven.plugins maven-dependency-plugin 2.10 unpack-dependencies prepare-package unpack-dependencies runtime ${project.build.directory}/classes org.glyptodon.guacamole guacamole-ext provided org.glyptodon.guacamole guacamole-auth-jdbc-base 0.9.9 ././@LongLink0100644 0000000 0000000 00000000177 12612226251 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/fr.jsonguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/reso0100664 0001751 0001751 00000000453 12612226251 035305 0ustar00mjumpermjumper0000000 0000000 { "LOGIN" : { "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", "FIELD_HEADER_NEW_PASSWORD" : "Mot de passe", "FIELD_HEADER_CONFIRM_NEW_PASSWORD" : "Répéter mot de passe" } } ././@LongLink0100644 0000000 0000000 00000000177 12612226251 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/en.jsonguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/reso0100664 0001751 0001751 00000003732 12612226251 035310 0ustar00mjumpermjumper0000000 0000000 { "LOGIN" : { "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", "ERROR_PASSWORD_SAME" : "The new password must be different from the expired password.", "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", "ERROR_NOT_VALID" : "This user account is not currently valid.", "ERROR_NOT_ACCESSIBLE" : "Access to this account is not currently allowed. Please try again later.", "INFO_PASSWORD_EXPIRED" : "Your password has expired and must be reset. Please enter a new password to continue.", "FIELD_HEADER_NEW_PASSWORD" : "New password", "FIELD_HEADER_CONFIRM_NEW_PASSWORD" : "Confirm new password" }, "CONNECTION_ATTRIBUTES" : { "FIELD_HEADER_MAX_CONNECTIONS" : "Maximum number of connections:", "FIELD_HEADER_MAX_CONNECTIONS_PER_USER" : "Maximum number of connections per user:", "SECTION_HEADER_CONCURRENCY" : "Concurrency Limits" }, "CONNECTION_GROUP_ATTRIBUTES" : { "FIELD_HEADER_MAX_CONNECTIONS" : "Maximum number of connections:", "FIELD_HEADER_MAX_CONNECTIONS_PER_USER" : "Maximum number of connections per user:", "SECTION_HEADER_CONCURRENCY" : "Concurrency Limits (Balancing Groups)" }, "DATA_SOURCE_MYSQL" : { "NAME" : "MySQL" }, "DATA_SOURCE_POSTGRESQL" : { "NAME" : "PostgreSQL" }, "USER_ATTRIBUTES" : { "FIELD_HEADER_DISABLED" : "Login disabled:", "FIELD_HEADER_EXPIRED" : "Password expired:", "FIELD_HEADER_ACCESS_WINDOW_END" : "Do not allow access after:", "FIELD_HEADER_ACCESS_WINDOW_START" : "Allow access after:", "FIELD_HEADER_TIMEZONE" : "User time zone:", "FIELD_HEADER_VALID_FROM" : "Enable account after:", "FIELD_HEADER_VALID_UNTIL" : "Disable account after:", "SECTION_HEADER_RESTRICTIONS" : "Account Restrictions" } } ././@LongLink0100644 0000000 0000000 00000000177 12612226251 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/ru.jsonguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/reso0100664 0001751 0001751 00000000507 12612226251 035305 0ustar00mjumpermjumper0000000 0000000 { "LOGIN" : { "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", "FIELD_HEADER_NEW_PASSWORD" : "Ðовый пароль", "FIELD_HEADER_CONFIRM_NEW_PASSWORD" : "Подтверждение паролÑ" } } ././@LongLink0100644 0000000 0000000 00000000257 12645510255 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000036002 12645510255 035263 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.base; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Set; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.auth.jdbc.permission.ObjectPermissionMapper; import org.glyptodon.guacamole.auth.jdbc.permission.ObjectPermissionModel; import org.glyptodon.guacamole.auth.jdbc.user.UserModel; import org.glyptodon.guacamole.net.auth.Identifiable; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; /** * Service which provides convenience methods for creating, retrieving, and * manipulating objects within directories. This service will automatically * enforce the permissions of the current user. * * @author Michael Jumper * @param * The specific internal implementation of the type of object this service * provides access to. * * @param * The external interface or implementation of the type of object this * service provides access to, as defined by the guacamole-ext API. * * @param * The underlying model object used to represent InternalType in the * database. */ public abstract class ModeledDirectoryObjectService, ExternalType extends Identifiable, ModelType extends ObjectModel> implements DirectoryObjectService { /** * All object permissions which are implicitly granted upon creation to the * creator of the object. */ private static final ObjectPermission.Type[] IMPLICIT_OBJECT_PERMISSIONS = { ObjectPermission.Type.READ, ObjectPermission.Type.UPDATE, ObjectPermission.Type.DELETE, ObjectPermission.Type.ADMINISTER }; /** * Returns an instance of a mapper for the type of object used by this * service. * * @return * A mapper which provides access to the model objects associated with * the objects used by this service. */ protected abstract ModeledDirectoryObjectMapper getObjectMapper(); /** * Returns an instance of a mapper for the type of permissions that affect * the type of object used by this service. * * @return * A mapper which provides access to the model objects associated with * the permissions that affect the objects used by this service. */ protected abstract ObjectPermissionMapper getPermissionMapper(); /** * Returns an instance of an object which is backed by the given model * object. * * @param currentUser * The user for whom this object is being created. * * @param model * The model object to use to back the returned object. * * @return * An object which is backed by the given model object. */ protected abstract InternalType getObjectInstance(AuthenticatedUser currentUser, ModelType model); /** * Returns an instance of a model object which is based on the given * object. * * @param currentUser * The user for whom this model object is being created. * * @param object * The object to use to produce the returned model object. * * @return * A model object which is based on the given object. */ protected abstract ModelType getModelInstance(AuthenticatedUser currentUser, ExternalType object); /** * Returns whether the given user has permission to create the type of * objects that this directory object service manages. * * @param user * The user being checked. * * @return * true if the user has object creation permission relevant to this * directory object service, false otherwise. * * @throws GuacamoleException * If permission to read the user's permissions is denied. */ protected abstract boolean hasCreatePermission(AuthenticatedUser user) throws GuacamoleException; /** * Returns whether the given user has permission to perform a certain * action on a specific object managed by this directory object service. * * @param user * The user being checked. * * @param identifier * The identifier of the object to check. * * @param type * The type of action that will be performed. * * @return * true if the user has object permission relevant described, false * otherwise. * * @throws GuacamoleException * If permission to read the user's permissions is denied. */ protected boolean hasObjectPermission(AuthenticatedUser user, String identifier, ObjectPermission.Type type) throws GuacamoleException { // Get object permissions ObjectPermissionSet permissionSet = getPermissionSet(user); // Return whether permission is granted return user.getUser().isAdministrator() || permissionSet.hasPermission(type, identifier); } /** * Returns the permission set associated with the given user and related * to the type of objects handled by this directory object service. * * @param user * The user whose permissions are being retrieved. * * @return * A permission set which contains the permissions associated with the * given user and related to the type of objects handled by this * directory object service. * * @throws GuacamoleException * If permission to read the user's permissions is denied. */ protected abstract ObjectPermissionSet getPermissionSet(AuthenticatedUser user) throws GuacamoleException; /** * Returns a collection of objects which are backed by the models in the * given collection. * * @param currentUser * The user for whom these objects are being created. * * @param models * The model objects to use to back the objects within the returned * collection. * * @return * A collection of objects which are backed by the models in the given * collection. */ protected Collection getObjectInstances(AuthenticatedUser currentUser, Collection models) { // Create new collection of objects by manually converting each model Collection objects = new ArrayList(models.size()); for (ModelType model : models) objects.add(getObjectInstance(currentUser, model)); return objects; } /** * Called before any object is created through this directory object * service. This function serves as a final point of validation before * the create operation occurs. In its default implementation, * beforeCreate() performs basic permissions checks. * * @param user * The user creating the object. * * @param model * The model of the object being created. * * @throws GuacamoleException * If the object is invalid, or an error prevents validating the given * object. */ protected void beforeCreate(AuthenticatedUser user, ModelType model ) throws GuacamoleException { // Verify permission to create objects if (!user.getUser().isAdministrator() && !hasCreatePermission(user)) throw new GuacamoleSecurityException("Permission denied."); } /** * Called before any object is updated through this directory object * service. This function serves as a final point of validation before * the update operation occurs. In its default implementation, * beforeUpdate() performs basic permissions checks. * * @param user * The user updating the existing object. * * @param model * The model of the object being updated. * * @throws GuacamoleException * If the object is invalid, or an error prevents validating the given * object. */ protected void beforeUpdate(AuthenticatedUser user, ModelType model) throws GuacamoleException { // By default, do nothing. if (!hasObjectPermission(user, model.getIdentifier(), ObjectPermission.Type.UPDATE)) throw new GuacamoleSecurityException("Permission denied."); } /** * Called before any object is deleted through this directory object * service. This function serves as a final point of validation before * the delete operation occurs. In its default implementation, * beforeDelete() performs basic permissions checks. * * @param user * The user deleting the existing object. * * @param identifier * The identifier of the object being deleted. * * @throws GuacamoleException * If the object is invalid, or an error prevents validating the given * object. */ protected void beforeDelete(AuthenticatedUser user, String identifier) throws GuacamoleException { // Verify permission to delete objects if (!hasObjectPermission(user, identifier, ObjectPermission.Type.DELETE)) throw new GuacamoleSecurityException("Permission denied."); } @Override public InternalType retrieveObject(AuthenticatedUser user, String identifier) throws GuacamoleException { // Pull objects having given identifier Collection objects = retrieveObjects(user, Collections.singleton(identifier)); // If no such object, return null if (objects.isEmpty()) return null; // The object collection will have exactly one element unless the // database has seriously lost integrity assert(objects.size() == 1); // Return first and only object return objects.iterator().next(); } @Override public Collection retrieveObjects(AuthenticatedUser user, Collection identifiers) throws GuacamoleException { // Do not query if no identifiers given if (identifiers.isEmpty()) return Collections.emptyList(); Collection objects; // Bypass permission checks if the user is a system admin if (user.getUser().isAdministrator()) objects = getObjectMapper().select(identifiers); // Otherwise only return explicitly readable identifiers else objects = getObjectMapper().selectReadable(user.getUser().getModel(), identifiers); // Return collection of requested objects return getObjectInstances(user, objects); } /** * Returns a collection of permissions that should be granted due to the * creation of the given object. These permissions need not be granted * solely to the user creating the object. * * @param user * The user creating the object. * * @param model * The object being created. * * @return * The collection of implicit permissions that should be granted due to * the creation of the given object. */ protected Collection getImplicitPermissions(AuthenticatedUser user, ModelType model) { // Build list of implicit permissions Collection implicitPermissions = new ArrayList(IMPLICIT_OBJECT_PERMISSIONS.length); UserModel userModel = user.getUser().getModel(); for (ObjectPermission.Type permission : IMPLICIT_OBJECT_PERMISSIONS) { // Create model which grants this permission to the current user ObjectPermissionModel permissionModel = new ObjectPermissionModel(); permissionModel.setUserID(userModel.getObjectID()); permissionModel.setUsername(userModel.getIdentifier()); permissionModel.setType(permission); permissionModel.setObjectIdentifier(model.getIdentifier()); // Add permission implicitPermissions.add(permissionModel); } return implicitPermissions; } @Override public InternalType createObject(AuthenticatedUser user, ExternalType object) throws GuacamoleException { ModelType model = getModelInstance(user, object); beforeCreate(user, model); // Create object getObjectMapper().insert(model); // Set identifier on original object object.setIdentifier(model.getIdentifier()); // Add implicit permissions getPermissionMapper().insert(getImplicitPermissions(user, model)); return getObjectInstance(user, model); } @Override public void deleteObject(AuthenticatedUser user, String identifier) throws GuacamoleException { beforeDelete(user, identifier); // Delete object getObjectMapper().delete(identifier); } @Override public void updateObject(AuthenticatedUser user, InternalType object) throws GuacamoleException { ModelType model = object.getModel(); beforeUpdate(user, model); // Update object getObjectMapper().update(model); } @Override public Set getIdentifiers(AuthenticatedUser user) throws GuacamoleException { // Bypass permission checks if the user is a system admin if (user.getUser().isAdministrator()) return getObjectMapper().selectIdentifiers(); // Otherwise only return explicitly readable identifiers else return getObjectMapper().selectReadableIdentifiers(user.getUser().getModel()); } } ././@LongLink0100644 0000000 0000000 00000000244 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/GroupedObjectModel.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000004510 12612226251 035254 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.base; /** * Object representation of a Guacamole object, such as a user or connection, * as represented in the database. * * @author Michael Jumper */ public abstract class GroupedObjectModel extends ObjectModel { /** * The unique identifier which identifies the parent of this object. */ private String parentIdentifier; /** * Creates a new, empty object. */ public GroupedObjectModel() { } /** * Returns the identifier of the parent connection group, or null if the * parent connection group is the root connection group. * * @return * The identifier of the parent connection group, or null if the parent * connection group is the root connection group. */ public String getParentIdentifier() { return parentIdentifier; } /** * Sets the identifier of the parent connection group. * * @param parentIdentifier * The identifier of the parent connection group, or null if the parent * connection group is the root connection group. */ public void setParentIdentifier(String parentIdentifier) { this.parentIdentifier = parentIdentifier; } } ././@LongLink0100644 0000000 0000000 00000000250 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/ModeledDirectoryObject.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000003660 12612226251 035261 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.base; import org.glyptodon.guacamole.net.auth.Identifiable; /** * Common base class for objects that will ultimately be made available through * the Directory class and are persisted to an underlying database model. All * such objects will need the same base set of queries to fulfill the needs of * the Directory class. * * @author Michael Jumper * @param * The type of model object that corresponds to this object. */ public abstract class ModeledDirectoryObject extends ModeledObject implements Identifiable { @Override public String getIdentifier() { return getModel().getIdentifier(); } @Override public void setIdentifier(String identifier) { getModel().setIdentifier(identifier); } } ././@LongLink0100644 0000000 0000000 00000000266 12645510255 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/ModeledGroupedDirectoryObjectService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000016673 12645510255 035277 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.base; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.net.auth.Identifiable; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; /** * Service which provides convenience methods for creating, retrieving, and * manipulating objects that can be within connection groups. This service will * automatically enforce the permissions of the current user. * * @author Michael Jumper * @param * The specific internal implementation of the type of object this service * provides access to. * * @param * The external interface or implementation of the type of object this * service provides access to, as defined by the guacamole-ext API. * * @param * The underlying model object used to represent InternalType in the * database. */ public abstract class ModeledGroupedDirectoryObjectService, ExternalType extends Identifiable, ModelType extends GroupedObjectModel> extends ModeledDirectoryObjectService { /** * Returns the set of parent connection groups that are modified by the * given model object (by virtue of the object changing parent groups). If * the model is not changing parents, the resulting collection will be * empty. * * @param user * The user making the given changes to the model. * * @param identifier * The identifier of the object that has been modified, if it exists. * If the object is being created, this will be null. * * @param model * The model that has been modified, if any. If the object is being * deleted, this will be null. * * @return * A collection of the identifiers of all parent connection groups * that will be affected (updated) by the change. * * @throws GuacamoleException * If an error occurs while determining which parent connection groups * are affected. */ protected Collection getModifiedGroups(AuthenticatedUser user, String identifier, ModelType model) throws GuacamoleException { // Get old parent identifier String oldParentIdentifier = null; if (identifier != null) { ModelType current = retrieveObject(user, identifier).getModel(); oldParentIdentifier = current.getParentIdentifier(); } // Get new parent identifier String parentIdentifier = null; if (model != null) { parentIdentifier = model.getParentIdentifier(); // If both parents have the same identifier, nothing has changed if (parentIdentifier != null && parentIdentifier.equals(oldParentIdentifier)) return Collections.emptyList(); } // Return collection of all non-root groups involved Collection groups = new ArrayList(2); if (oldParentIdentifier != null) groups.add(oldParentIdentifier); if (parentIdentifier != null) groups.add(parentIdentifier); return groups; } /** * Returns whether the given user has permission to modify the parent * connection groups affected by the modifications made to the given model * object. * * @param user * The user who changed the model object. * * @param identifier * The identifier of the object that has been modified, if it exists. * If the object is being created, this will be null. * * @param model * The model that has been modified, if any. If the object is being * deleted, this will be null. * * @return * true if the user has update permission for all modified groups, * false otherwise. * * @throws GuacamoleException * If an error occurs while determining which parent connection groups * are affected. */ protected boolean canUpdateModifiedGroups(AuthenticatedUser user, String identifier, ModelType model) throws GuacamoleException { // If user is an administrator, no need to check if (user.getUser().isAdministrator()) return true; // Verify that we have permission to modify any modified groups Collection modifiedGroups = getModifiedGroups(user, identifier, model); if (!modifiedGroups.isEmpty()) { ObjectPermissionSet permissionSet = user.getUser().getConnectionGroupPermissions(); Collection updateableGroups = permissionSet.getAccessibleObjects( Collections.singleton(ObjectPermission.Type.UPDATE), modifiedGroups ); return updateableGroups.size() == modifiedGroups.size(); } return true; } @Override protected void beforeCreate(AuthenticatedUser user, ModelType model) throws GuacamoleException { super.beforeCreate(user, model); // Validate that we can update all applicable parent groups if (!canUpdateModifiedGroups(user, null, model)) throw new GuacamoleSecurityException("Permission denied."); } @Override protected void beforeUpdate(AuthenticatedUser user, ModelType model) throws GuacamoleException { super.beforeUpdate(user, model); // Validate that we can update all applicable parent groups if (!canUpdateModifiedGroups(user, model.getIdentifier(), model)) throw new GuacamoleSecurityException("Permission denied."); } @Override protected void beforeDelete(AuthenticatedUser user, String identifier) throws GuacamoleException { super.beforeDelete(user, identifier); // Validate that we can update all applicable parent groups if (!canUpdateModifiedGroups(user, identifier, null)) throw new GuacamoleSecurityException("Permission denied."); } } ././@LongLink0100644 0000000 0000000 00000000242 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/RestrictedObject.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000005116 12612226251 035257 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.base; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; /** * Common base class for objects that are associated with the users that * obtain them. * * @author Michael Jumper */ public abstract class RestrictedObject { /** * The user this object belongs to. Access is based on his/her permission * settings. */ private AuthenticatedUser currentUser; /** * Initializes this object, associating it with the current authenticated * user and populating it with data from the given model object * * @param currentUser * The user that created or retrieved this object. */ public void init(AuthenticatedUser currentUser) { setCurrentUser(currentUser); } /** * Returns the user that created or queried this object. This user's * permissions dictate what operations can be performed on or through this * object. * * @return * The user that created or queried this object. */ public AuthenticatedUser getCurrentUser() { return currentUser; } /** * Sets the user that created or queried this object. This user's * permissions dictate what operations can be performed on or through this * object. * * @param currentUser * The user that created or queried this object. */ public void setCurrentUser(AuthenticatedUser currentUser) { this.currentUser = currentUser; } } ././@LongLink0100644 0000000 0000000 00000000256 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/ModeledDirectoryObjectMapper.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000012143 12612226251 035255 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.base; import java.util.Collection; import java.util.Set; import org.glyptodon.guacamole.auth.jdbc.user.UserModel; import org.apache.ibatis.annotations.Param; /** * Common interface for objects that will ultimately be made available through * the Directory class. All such objects will need the same base set of queries * to fulfill the needs of the Directory class. * * @author Michael Jumper * @param * The type of object contained within the directory whose objects are * mapped by this mapper. */ public interface ModeledDirectoryObjectMapper { /** * Selects the identifiers of all objects, regardless of whether they * are readable by any particular user. This should only be called on * behalf of a system administrator. If identifiers are needed by a non- * administrative user who must have explicit read rights, use * selectReadableIdentifiers() instead. * * @return * A Set containing all identifiers of all objects. */ Set selectIdentifiers(); /** * Selects the identifiers of all objects that are explicitly readable by * the given user. If identifiers are needed by a system administrator * (who, by definition, does not need explicit read rights), use * selectIdentifiers() instead. * * @param user * The user whose permissions should determine whether an identifier * is returned. * * @return * A Set containing all identifiers of all readable objects. */ Set selectReadableIdentifiers(@Param("user") UserModel user); /** * Selects all objects which have the given identifiers. If an identifier * has no corresponding object, it will be ignored. This should only be * called on behalf of a system administrator. If objects are needed by a * non-administrative user who must have explicit read rights, use * selectReadable() instead. * * @param identifiers * The identifiers of the objects to return. * * @return * A Collection of all objects having the given identifiers. */ Collection select(@Param("identifiers") Collection identifiers); /** * Selects all objects which have the given identifiers and are explicitly * readably by the given user. If an identifier has no corresponding * object, or the corresponding object is unreadable, it will be ignored. * If objects are needed by a system administrator (who, by definition, * does not need explicit read rights), use select() instead. * * @param user * The user whose permissions should determine whether an object * is returned. * * @param identifiers * The identifiers of the objects to return. * * @return * A Collection of all objects having the given identifiers. */ Collection selectReadable(@Param("user") UserModel user, @Param("identifiers") Collection identifiers); /** * Inserts the given object into the database. If the object already * exists, this will result in an error. * * @param object * The object to insert. * * @return * The number of rows inserted. */ int insert(@Param("object") ModelType object); /** * Deletes the given object into the database. If the object does not * exist, this operation has no effect. * * @param identifier * The identifier of the object to delete. * * @return * The number of rows deleted. */ int delete(@Param("identifier") String identifier); /** * Updates the given existing object in the database. If the object does * not actually exist, this operation has no effect. * * @param object * The object to update. * * @return * The number of rows updated. */ int update(@Param("object") ModelType object); }././@LongLink0100644 0000000 0000000 00000000250 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/DirectoryObjectService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000012450 12612226251 035256 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.base; import java.util.Collection; import java.util.Set; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.GuacamoleException; /** * Service which provides convenience methods for creating, retrieving, and * manipulating objects that have unique identifiers, such as the objects * within directories. This service will automatically enforce the permissions * of the current user. * * @author Michael Jumper * @param * The specific internal implementation of the type of object this service * provides access to. * * @param * The external interface or implementation of the type of object this * service provides access to, as defined by the guacamole-ext API. */ public interface DirectoryObjectService { /** * Retrieves the single object that has the given identifier, if it exists * and the user has permission to read it. * * @param user * The user retrieving the object. * * @param identifier * The identifier of the object to retrieve. * * @return * The object having the given identifier, or null if no such object * exists. * * @throws GuacamoleException * If an error occurs while retrieving the requested object. */ InternalType retrieveObject(AuthenticatedUser user, String identifier) throws GuacamoleException; /** * Retrieves all objects that have the identifiers in the given collection. * Only objects that the user has permission to read will be returned. * * @param user * The user retrieving the objects. * * @param identifiers * The identifiers of the objects to retrieve. * * @return * The objects having the given identifiers. * * @throws GuacamoleException * If an error occurs while retrieving the requested objects. */ Collection retrieveObjects(AuthenticatedUser user, Collection identifiers) throws GuacamoleException; /** * Creates the given object. If the object already exists, an error will be * thrown. * * @param user * The user creating the object. * * @param object * The object to create. * * @return * The newly-created object. * * @throws GuacamoleException * If the user lacks permission to create the object, or an error * occurs while creating the object. */ InternalType createObject(AuthenticatedUser user, ExternalType object) throws GuacamoleException; /** * Deletes the object having the given identifier. If no such object * exists, this function has no effect. * * @param user * The user deleting the object. * * @param identifier * The identifier of the object to delete. * * @throws GuacamoleException * If the user lacks permission to delete the object, or an error * occurs while deleting the object. */ void deleteObject(AuthenticatedUser user, String identifier) throws GuacamoleException; /** * Updates the given object, applying any changes that have been made. If * no such object exists, this function has no effect. * * @param user * The user updating the object. * * @param object * The object to update. * * @throws GuacamoleException * If the user lacks permission to update the object, or an error * occurs while updating the object. */ void updateObject(AuthenticatedUser user, InternalType object) throws GuacamoleException; /** * Returns the set of all identifiers for all objects that the user has * read access to. * * @param user * The user retrieving the identifiers. * * @return * The set of all identifiers for all objects. * * @throws GuacamoleException * If an error occurs while reading identifiers. */ Set getIdentifiers(AuthenticatedUser user) throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000236 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/package-info.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000002500 12612226251 035251 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Base classes supporting JDBC-driven authentication providers and defining * the relationships between the model and the implementations of guacamole-ext * classes. */ package org.glyptodon.guacamole.auth.jdbc.base; ././@LongLink0100644 0000000 0000000 00000000237 12612226251 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/ModeledObject.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000005340 12612226251 035256 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.base; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; /** * Common base class for objects have an underlying model. For the purposes of * JDBC-driven authentication providers, all modeled objects are also * restricted. * * @author Michael Jumper * @param * The type of model object which corresponds to this object. */ public abstract class ModeledObject extends RestrictedObject { /** * The internal model object containing the values which represent this * object in the database. */ private ModelType model; /** * Initializes this object, associating it with the current authenticated * user and populating it with data from the given model object * * @param currentUser * The user that created or retrieved this object. * * @param model * The backing model object. */ public void init(AuthenticatedUser currentUser, ModelType model) { super.init(currentUser); setModel(model); } /** * Returns the backing model object. Changes to the model object will * affect this object, and changes to this object will affect the model * object. * * @return * The backing model object. */ public ModelType getModel() { return model; } /** * Sets the backing model object. This will effectively replace all data * contained within this object. * * @param model * The backing model object. */ public void setModel(ModelType model) { this.model = model; } } ././@LongLink0100644 0000000 0000000 00000000257 12612226251 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/ModeledGroupedDirectoryObject.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000005631 12612226251 035261 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.base; import org.glyptodon.guacamole.auth.jdbc.connectiongroup.RootConnectionGroup; /** * Common base class for objects that will ultimately be made available through * the Directory class. All such objects will need the same base set of queries * to fulfill the needs of the Directory class. * * @author Michael Jumper * @param * The type of model object that corresponds to this object. */ public abstract class ModeledGroupedDirectoryObject extends ModeledDirectoryObject { /** * Returns the identifier of the parent connection group, which cannot be * null. If the parent is the root connection group, this will be * RootConnectionGroup.IDENTIFIER. * * @return * The identifier of the parent connection group. */ public String getParentIdentifier() { // Translate null parent to proper identifier String parentIdentifier = getModel().getParentIdentifier(); if (parentIdentifier == null) return RootConnectionGroup.IDENTIFIER; return parentIdentifier; } /** * Sets the identifier of the associated parent connection group. If the * parent is the root connection group, this should be * RootConnectionGroup.IDENTIFIER. * * @param parentIdentifier * The identifier of the connection group to associate as this object's * parent. */ public void setParentIdentifier(String parentIdentifier) { // Translate root identifier back into null if (parentIdentifier != null && parentIdentifier.equals(RootConnectionGroup.IDENTIFIER)) parentIdentifier = null; getModel().setParentIdentifier(parentIdentifier); } } ././@LongLink0100644 0000000 0000000 00000000235 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/base/ObjectModel.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000005207 12612226251 035260 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.base; /** * Object representation of a Guacamole object, such as a user or connection, * as represented in the database. * * @author Michael Jumper */ public abstract class ObjectModel { /** * The ID of this object in the database, if any. */ private Integer objectID; /** * The unique identifier which identifies this object. */ private String identifier; /** * Creates a new, empty object. */ public ObjectModel() { } /** * Returns the identifier that uniquely identifies this object. * * @return * The identifier that uniquely identifies this object. */ public String getIdentifier() { return identifier; } /** * Sets the identifier that uniquely identifies this object. * * @param identifier * The identifier that uniquely identifies this object. */ public void setIdentifier(String identifier) { this.identifier = identifier; } /** * Returns the ID of this object in the database, if it exists. * * @return * The ID of this object in the database, or null if this object was * not retrieved from the database. */ public Integer getObjectID() { return objectID; } /** * Sets the ID of this object to the given value. * * @param objectID * The ID to assign to this object. */ public void setObjectID(Integer objectID) { this.objectID = objectID; } } ././@LongLink0100644 0000000 0000000 00000000253 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionDirectory.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000006235 12612226251 035262 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connection; import com.google.inject.Inject; import java.util.Collection; import java.util.Collections; import java.util.Set; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.auth.jdbc.base.RestrictedObject; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.Directory; import org.mybatis.guice.transactional.Transactional; /** * Implementation of the Connection Directory which is driven by an underlying, * arbitrary database. * * @author James Muehlner * @author Michael Jumper */ public class ConnectionDirectory extends RestrictedObject implements Directory { /** * Service for managing connection objects. */ @Inject private ConnectionService connectionService; @Override public Connection get(String identifier) throws GuacamoleException { return connectionService.retrieveObject(getCurrentUser(), identifier); } @Override @Transactional public Collection getAll(Collection identifiers) throws GuacamoleException { Collection objects = connectionService.retrieveObjects(getCurrentUser(), identifiers); return Collections.unmodifiableCollection(objects); } @Override @Transactional public Set getIdentifiers() throws GuacamoleException { return connectionService.getIdentifiers(getCurrentUser()); } @Override @Transactional public void add(Connection object) throws GuacamoleException { connectionService.createObject(getCurrentUser(), object); } @Override @Transactional public void update(Connection object) throws GuacamoleException { ModeledConnection connection = (ModeledConnection) object; connectionService.updateObject(getCurrentUser(), connection); } @Override @Transactional public void remove(String identifier) throws GuacamoleException { connectionService.deleteObject(getCurrentUser(), identifier); } } ././@LongLink0100644 0000000 0000000 00000000247 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionModel.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000013131 12612226251 035253 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connection; import org.glyptodon.guacamole.auth.jdbc.base.GroupedObjectModel; /** * Object representation of a Guacamole connection, as represented in the * database. * * @author Michael Jumper */ public class ConnectionModel extends GroupedObjectModel { /** * The human-readable name associated with this connection. */ private String name; /** * The name of the protocol to use when connecting to this connection. */ private String protocol; /** * The maximum number of connections that can be established to this * connection concurrently, zero if no restriction applies, or null if the * default restrictions should be applied. */ private Integer maxConnections; /** * The maximum number of connections that can be established to this * connection concurrently by any one user, zero if no restriction applies, * or null if the default restrictions should be applied. */ private Integer maxConnectionsPerUser; /** * Creates a new, empty connection. */ public ConnectionModel() { } /** * Returns the name associated with this connection. * * @return * The name associated with this connection. */ public String getName() { return name; } /** * Sets the name associated with this connection. * * @param name * The name to associate with this connection. */ public void setName(String name) { this.name = name; } /** * Returns the name of the protocol to use when connecting to this * connection. * * @return * The name of the protocol to use when connecting to this connection. */ public String getProtocol() { return protocol; } /** * Sets the name of the protocol to use when connecting to this connection. * * @param protocol * The name of the protocol to use when connecting to this connection. */ public void setProtocol(String protocol) { this.protocol = protocol; } /** * Returns the maximum number of connections that can be established to * this connection concurrently. * * @return * The maximum number of connections that can be established to this * connection concurrently, zero if no restriction applies, or null if * the default restrictions should be applied. */ public Integer getMaxConnections() { return maxConnections; } /** * Sets the maximum number of connections that can be established to this * connection concurrently. * * @param maxConnections * The maximum number of connections that can be established to this * connection concurrently, zero if no restriction applies, or null if * the default restrictions should be applied. */ public void setMaxConnections(Integer maxConnections) { this.maxConnections = maxConnections; } /** * Returns the maximum number of connections that can be established to * this connection concurrently by any one user. * * @return * The maximum number of connections that can be established to this * connection concurrently by any one user, zero if no restriction * applies, or null if the default restrictions should be applied. */ public Integer getMaxConnectionsPerUser() { return maxConnectionsPerUser; } /** * Sets the maximum number of connections that can be established to this * connection concurrently by any one user. * * @param maxConnectionsPerUser * The maximum number of connections that can be established to this * connection concurrently by any one user, zero if no restriction * applies, or null if the default restrictions should be applied. */ public void setMaxConnectionsPerUser(Integer maxConnectionsPerUser) { this.maxConnectionsPerUser = maxConnectionsPerUser; } @Override public String getIdentifier() { // If no associated ID, then no associated identifier Integer id = getObjectID(); if (id == null) return null; // Otherwise, the identifier is the ID as a string return id.toString(); } @Override public void setIdentifier(String identifier) { throw new UnsupportedOperationException("Connection identifiers are derived from IDs. They cannot be set."); } } ././@LongLink0100644 0000000 0000000 00000000251 12645510255 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000044142 12645510255 035267 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connection; import com.google.inject.Inject; import com.google.inject.Provider; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.base.ModeledDirectoryObjectMapper; import org.glyptodon.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.auth.jdbc.base.ModeledGroupedDirectoryObjectService; import org.glyptodon.guacamole.auth.jdbc.permission.ConnectionPermissionMapper; import org.glyptodon.guacamole.auth.jdbc.permission.ObjectPermissionMapper; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionRecord; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; /** * Service which provides convenience methods for creating, retrieving, and * manipulating connections. * * @author Michael Jumper, James Muehlner */ public class ConnectionService extends ModeledGroupedDirectoryObjectService { /** * Mapper for accessing connections. */ @Inject private ConnectionMapper connectionMapper; /** * Mapper for manipulating connection permissions. */ @Inject private ConnectionPermissionMapper connectionPermissionMapper; /** * Mapper for accessing connection parameters. */ @Inject private ParameterMapper parameterMapper; /** * Mapper for accessing connection history. */ @Inject private ConnectionRecordMapper connectionRecordMapper; /** * Provider for creating connections. */ @Inject private Provider connectionProvider; /** * Service for creating and tracking tunnels. */ @Inject private GuacamoleTunnelService tunnelService; @Override protected ModeledDirectoryObjectMapper getObjectMapper() { return connectionMapper; } @Override protected ObjectPermissionMapper getPermissionMapper() { return connectionPermissionMapper; } @Override protected ModeledConnection getObjectInstance(AuthenticatedUser currentUser, ConnectionModel model) { ModeledConnection connection = connectionProvider.get(); connection.init(currentUser, model); return connection; } @Override protected ConnectionModel getModelInstance(AuthenticatedUser currentUser, final Connection object) { // Create new ModeledConnection backed by blank model ConnectionModel model = new ConnectionModel(); ModeledConnection connection = getObjectInstance(currentUser, model); // Set model contents through ModeledConnection, copying the provided connection connection.setParentIdentifier(object.getParentIdentifier()); connection.setName(object.getName()); connection.setConfiguration(object.getConfiguration()); connection.setAttributes(object.getAttributes()); return model; } @Override protected boolean hasCreatePermission(AuthenticatedUser user) throws GuacamoleException { // Return whether user has explicit connection creation permission SystemPermissionSet permissionSet = user.getUser().getSystemPermissions(); return permissionSet.hasPermission(SystemPermission.Type.CREATE_CONNECTION); } @Override protected ObjectPermissionSet getPermissionSet(AuthenticatedUser user) throws GuacamoleException { // Return permissions related to connections return user.getUser().getConnectionPermissions(); } @Override protected void beforeCreate(AuthenticatedUser user, ConnectionModel model) throws GuacamoleException { super.beforeCreate(user, model); // Name must not be blank if (model.getName() == null || model.getName().trim().isEmpty()) throw new GuacamoleClientException("Connection names must not be blank."); // Do not attempt to create duplicate connections ConnectionModel existing = connectionMapper.selectOneByName(model.getParentIdentifier(), model.getName()); if (existing != null) throw new GuacamoleClientException("The connection \"" + model.getName() + "\" already exists."); } @Override protected void beforeUpdate(AuthenticatedUser user, ConnectionModel model) throws GuacamoleException { super.beforeUpdate(user, model); // Name must not be blank if (model.getName() == null || model.getName().trim().isEmpty()) throw new GuacamoleClientException("Connection names must not be blank."); // Check whether such a connection is already present ConnectionModel existing = connectionMapper.selectOneByName(model.getParentIdentifier(), model.getName()); if (existing != null) { // If the specified name matches a DIFFERENT existing connection, the update cannot continue if (!existing.getObjectID().equals(model.getObjectID())) throw new GuacamoleClientException("The connection \"" + model.getName() + "\" already exists."); } } /** * Given an arbitrary Guacamole connection, produces a collection of * parameter model objects containing the name/value pairs of that * connection's parameters. * * @param connection * The connection whose configuration should be used to produce the * collection of parameter models. * * @return * A collection of parameter models containing the name/value pairs * of the given connection's parameters. */ private Collection getParameterModels(ModeledConnection connection) { Map parameters = connection.getConfiguration().getParameters(); // Convert parameters to model objects Collection parameterModels = new ArrayList(parameters.size()); for (Map.Entry parameterEntry : parameters.entrySet()) { // Get parameter name and value String name = parameterEntry.getKey(); String value = parameterEntry.getValue(); // There is no need to insert empty parameters if (value == null || value.isEmpty()) continue; // Produce model object from parameter ParameterModel model = new ParameterModel(); model.setConnectionIdentifier(connection.getIdentifier()); model.setName(name); model.setValue(value); // Add model to list parameterModels.add(model); } return parameterModels; } @Override public ModeledConnection createObject(AuthenticatedUser user, Connection object) throws GuacamoleException { // Create connection ModeledConnection connection = super.createObject(user, object); connection.setConfiguration(object.getConfiguration()); // Insert new parameters, if any Collection parameterModels = getParameterModels(connection); if (!parameterModels.isEmpty()) parameterMapper.insert(parameterModels); return connection; } @Override public void updateObject(AuthenticatedUser user, ModeledConnection object) throws GuacamoleException { // Update connection super.updateObject(user, object); // Replace existing parameters with new parameters, if any Collection parameterModels = getParameterModels(object); parameterMapper.delete(object.getIdentifier()); if (!parameterModels.isEmpty()) parameterMapper.insert(parameterModels); } /** * Returns the set of all identifiers for all connections within the * connection group having the given identifier. Only connections that the * user has read access to will be returned. * * Permission to read the connection group having the given identifier is * NOT checked. * * @param user * The user retrieving the identifiers. * * @param identifier * The identifier of the parent connection group, or null to check the * root connection group. * * @return * The set of all identifiers for all connections in the connection * group having the given identifier that the user has read access to. * * @throws GuacamoleException * If an error occurs while reading identifiers. */ public Set getIdentifiersWithin(AuthenticatedUser user, String identifier) throws GuacamoleException { // Bypass permission checks if the user is a system admin if (user.getUser().isAdministrator()) return connectionMapper.selectIdentifiersWithin(identifier); // Otherwise only return explicitly readable identifiers else return connectionMapper.selectReadableIdentifiersWithin(user.getUser().getModel(), identifier); } /** * Retrieves all parameters visible to the given user and associated with * the connection having the given identifier. If the given user has no * access to such parameters, or no such connection exists, the returned * map will be empty. * * @param user * The user retrieving connection parameters. * * @param identifier * The identifier of the connection whose parameters are being * retrieved. * * @return * A new map of all parameter name/value pairs that the given user has * access to. */ public Map retrieveParameters(AuthenticatedUser user, String identifier) { Map parameterMap = new HashMap(); // Determine whether we have permission to read parameters boolean canRetrieveParameters; try { canRetrieveParameters = hasObjectPermission(user, identifier, ObjectPermission.Type.UPDATE); } // Provide empty (but mutable) map if unable to check permissions catch (GuacamoleException e) { return parameterMap; } // Populate parameter map if we have permission to do so if (canRetrieveParameters) { for (ParameterModel parameter : parameterMapper.select(identifier)) parameterMap.put(parameter.getName(), parameter.getValue()); } return parameterMap; } /** * Returns a connection records object which is backed by the given model. * * @param model * The model object to use to back the returned connection record * object. * * @return * A connection record object which is backed by the given model. */ protected ConnectionRecord getObjectInstance(ConnectionRecordModel model) { return new ModeledConnectionRecord(model); } /** * Returns a list of connection records objects which are backed by the * models in the given list. * * @param models * The model objects to use to back the connection record objects * within the returned list. * * @return * A list of connection record objects which are backed by the models * in the given list. */ protected List getObjectInstances(List models) { // Create new list of records by manually converting each model List objects = new ArrayList(models.size()); for (ConnectionRecordModel model : models) objects.add(getObjectInstance(model)); return objects; } /** * Retrieves the connection history of the given connection, including any * active connections. * * @param user * The user retrieving the connection history. * * @param connection * The connection whose history is being retrieved. * * @return * The connection history of the given connection, including any * active connections. * * @throws GuacamoleException * If permission to read the connection history is denied. */ public List retrieveHistory(AuthenticatedUser user, ModeledConnection connection) throws GuacamoleException { String identifier = connection.getIdentifier(); // Retrieve history only if READ permission is granted if (hasObjectPermission(user, identifier, ObjectPermission.Type.READ)) { // Retrieve history List models = connectionRecordMapper.select(identifier); // Get currently-active connections List records = new ArrayList(tunnelService.getActiveConnections(connection)); Collections.reverse(records); // Add past connections from model objects for (ConnectionRecordModel model : models) records.add(getObjectInstance(model)); // Return converted history list return records; } // The user does not have permission to read the history throw new GuacamoleSecurityException("Permission denied."); } /** * Retrieves the connection history records matching the given criteria. * Retrieves up to limit connection history records matching * the given terms and sorted by the given predicates. Only history records * associated with data that the given user can read are returned. * * @param user * The user retrieving the connection history. * * @param requiredContents * The search terms that must be contained somewhere within each of the * returned records. * * @param sortPredicates * A list of predicates to sort the returned records by, in order of * priority. * * @param limit * The maximum number of records that should be returned. * * @return * The connection history of the given connection, including any * active connections. * * @throws GuacamoleException * If permission to read the connection history is denied. */ public List retrieveHistory(AuthenticatedUser user, Collection requiredContents, List sortPredicates, int limit) throws GuacamoleException { List searchResults; // Bypass permission checks if the user is a system admin if (user.getUser().isAdministrator()) searchResults = connectionRecordMapper.search(requiredContents, sortPredicates, limit); // Otherwise only return explicitly readable history records else searchResults = connectionRecordMapper.searchReadable(user.getUser().getModel(), requiredContents, sortPredicates, limit); return getObjectInstances(searchResults); } /** * Connects to the given connection as the given user, using the given * client information. If the user does not have permission to read the * connection, permission will be denied. * * @param user * The user connecting to the connection. * * @param connection * The connection being connected to. * * @param info * Information associated with the connecting client. * * @return * A connected GuacamoleTunnel associated with a newly-established * connection. * * @throws GuacamoleException * If permission to connect to this connection is denied. */ public GuacamoleTunnel connect(AuthenticatedUser user, ModeledConnection connection, GuacamoleClientInformation info) throws GuacamoleException { // Connect only if READ permission is granted if (hasObjectPermission(user, connection.getIdentifier(), ObjectPermission.Type.READ)) return tunnelService.getGuacamoleTunnel(user, connection, info); // The user does not have permission to connect throw new GuacamoleSecurityException("Permission denied."); } } ././@LongLink0100644 0000000 0000000 00000000262 12645510255 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordSearchTerm.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000022122 12645510255 035261 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connection; import java.util.Calendar; import java.util.Date; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A search term for querying historical connection records. This will contain * a the search term in string form and, if that string appears to be a date. a * corresponding date range. * * @author James Muehlner */ public class ConnectionRecordSearchTerm { /** * A pattern that can match a year, year and month, or year and month and * day. */ private static final Pattern DATE_PATTERN = Pattern.compile("(\\d+)(?:-(\\d+)?(?:-(\\d+)?)?)?"); /** * The index of the group within DATE_PATTERN containing the * year number. */ private static final int YEAR_GROUP = 1; /** * The index of the group within DATE_PATTERN containing the * month number, if any. */ private static final int MONTH_GROUP = 2; /** * The index of the group within DATE_PATTERN containing the * day number, if any. */ private static final int DAY_GROUP = 3; /** * The start of the date range for records that should be retrieved, if the * provided search term appears to be a date. */ private final Date startDate; /** * The end of the date range for records that should be retrieved, if the * provided search term appears to be a date. */ private final Date endDate; /** * The string that should be searched for. */ private final String term; /** * Parse the given string as an integer, returning the provided default * value if the string is null. * * @param str * The string to parse as an integer. * * @param defaultValue * The value to return if str is null. * * @return * The parsed value, or the provided default value if str * is null. */ private static int parseInt(String str, int defaultValue) { if (str == null) return defaultValue; return Integer.parseInt(str); } /** * Returns a new calendar representing the last millisecond of the same * year as calendar. * * @param calendar * The calendar defining the year whose end (last millisecond) is to be * returned. * * @return * A new calendar representing the last millisecond of the same year as * calendar. */ private static Calendar getEndOfYear(Calendar calendar) { // Get first day of next year Calendar endOfYear = Calendar.getInstance(); endOfYear.clear(); endOfYear.set(Calendar.YEAR, calendar.get(Calendar.YEAR) + 1); // Transform into the last millisecond of the given year endOfYear.add(Calendar.MILLISECOND, -1); return endOfYear; } /** * Returns a new calendar representing the last millisecond of the same * month and year as calendar. * * @param calendar * The calendar defining the month and year whose end (last millisecond) * is to be returned. * * @return * A new calendar representing the last millisecond of the same month * and year as calendar. */ private static Calendar getEndOfMonth(Calendar calendar) { // Copy given calender only up to given month Calendar endOfMonth = Calendar.getInstance(); endOfMonth.clear(); endOfMonth.set(Calendar.YEAR, calendar.get(Calendar.YEAR)); endOfMonth.set(Calendar.MONTH, calendar.get(Calendar.MONTH)); // Advance to the last millisecond of the given month endOfMonth.add(Calendar.MONTH, 1); endOfMonth.add(Calendar.MILLISECOND, -1); return endOfMonth; } /** * Returns a new calendar representing the last millisecond of the same * year, month, and day as calendar. * * @param calendar * The calendar defining the year, month, and day whose end * (last millisecond) is to be returned. * * @return * A new calendar representing the last millisecond of the same year, * month, and day as calendar. */ private static Calendar getEndOfDay(Calendar calendar) { // Copy given calender only up to given month Calendar endOfMonth = Calendar.getInstance(); endOfMonth.clear(); endOfMonth.set(Calendar.YEAR, calendar.get(Calendar.YEAR)); endOfMonth.set(Calendar.MONTH, calendar.get(Calendar.MONTH)); endOfMonth.set(Calendar.DAY_OF_MONTH, calendar.get(Calendar.DAY_OF_MONTH)); // Advance to the last millisecond of the given day endOfMonth.add(Calendar.DAY_OF_MONTH, 1); endOfMonth.add(Calendar.MILLISECOND, -1); return endOfMonth; } /** * Creates a new ConnectionRecordSearchTerm representing the given string. * If the given string appears to be a date, the start and end dates of the * implied date range will be automatically determined and made available * via getStartDate() and getEndDate() respectively. * * @param term * The string that should be searched for. */ public ConnectionRecordSearchTerm(String term) { // Search terms absolutely must not be null if (term == null) throw new NullPointerException("Search terms may not be null"); this.term = term; // Parse start/end of date range if term appears to be a date Matcher matcher = DATE_PATTERN.matcher(term); if (matcher.matches()) { // Retrieve date components from term String year = matcher.group(YEAR_GROUP); String month = matcher.group(MONTH_GROUP); String day = matcher.group(DAY_GROUP); // Parse start date from term Calendar startCalendar = Calendar.getInstance(); startCalendar.clear(); startCalendar.set( Integer.parseInt(year), parseInt(month, 1) - 1, parseInt(day, 1) ); Calendar endCalendar; // Derive end date from start date if (month == null) { endCalendar = getEndOfYear(startCalendar); } else if (day == null) { endCalendar = getEndOfMonth(startCalendar); } else { endCalendar = getEndOfDay(startCalendar); } // Convert results back into dates this.startDate = startCalendar.getTime(); this.endDate = endCalendar.getTime(); } // The search term doesn't look like a date else { this.startDate = null; this.endDate = null; } } /** * Returns the start of the date range for records that should be retrieved, * if the provided search term appears to be a date. * * @return * The start of the date range. */ public Date getStartDate() { return startDate; } /** * Returns the end of the date range for records that should be retrieved, * if the provided search term appears to be a date. * * @return * The end of the date range. */ public Date getEndDate() { return endDate; } /** * Returns the string that should be searched for. * * @return * The search term. */ public String getTerm() { return term; } @Override public int hashCode() { return term.hashCode(); } @Override public boolean equals(Object obj) { if (obj == null || !(obj instanceof ConnectionRecordSearchTerm)) return false; return ((ConnectionRecordSearchTerm) obj).getTerm().equals(getTerm()); } } ././@LongLink0100644 0000000 0000000 00000000250 12625262336 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionMapper.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000007260 12625262336 035271 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connection; import java.util.Set; import org.glyptodon.guacamole.auth.jdbc.base.ModeledDirectoryObjectMapper; import org.glyptodon.guacamole.auth.jdbc.user.UserModel; import org.apache.ibatis.annotations.Param; /** * Mapper for connection objects. * * @author Michael Jumper */ public interface ConnectionMapper extends ModeledDirectoryObjectMapper { /** * Selects the identifiers of all connections within the given parent * connection group, regardless of whether they are readable by any * particular user. This should only be called on behalf of a system * administrator. If identifiers are needed by a non-administrative user * who must have explicit read rights, use * selectReadableIdentifiersWithin() instead. * * @param parentIdentifier * The identifier of the parent connection group, or null if the root * connection group is to be queried. * * @return * A Set containing all identifiers of all objects. */ Set selectIdentifiersWithin(@Param("parentIdentifier") String parentIdentifier); /** * Selects the identifiers of all connections within the given parent * connection group that are explicitly readable by the given user. If * identifiers are needed by a system administrator (who, by definition, * does not need explicit read rights), use selectIdentifiersWithin() * instead. * * @param user * The user whose permissions should determine whether an identifier * is returned. * * @param parentIdentifier * The identifier of the parent connection group, or null if the root * connection group is to be queried. * * @return * A Set containing all identifiers of all readable objects. */ Set selectReadableIdentifiersWithin(@Param("user") UserModel user, @Param("parentIdentifier") String parentIdentifier); /** * Selects the connection within the given parent group and having the * given name. If no such connection exists, null is returned. * * @param parentIdentifier * The identifier of the parent group to search within. * * @param name * The name of the connection to find. * * @return * The connection having the given name within the given parent group, * or null if no such connection exists. */ ConnectionModel selectOneByName(@Param("parentIdentifier") String parentIdentifier, @Param("name") String name); }././@LongLink0100644 0000000 0000000 00000000246 12612226251 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ParameterModel.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000005652 12612226251 035264 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connection; /** * A single parameter name/value pair belonging to a connection. * * @author Michael Jumper */ public class ParameterModel { /** * The identifier of the connection associated with this parameter. */ private String connectionIdentifier; /** * The name of the parameter. */ private String name; /** * The value the parameter is set to. */ private String value; /** * Returns the identifier of the connection associated with this parameter. * * @return * The identifier of the connection associated with this parameter. */ public String getConnectionIdentifier() { return connectionIdentifier; } /** * Sets the identifier of the connection associated with this parameter. * * @param connectionIdentifier * The identifier of the connection to associate with this parameter. */ public void setConnectionIdentifier(String connectionIdentifier) { this.connectionIdentifier = connectionIdentifier; } /** * Returns the name of this parameter. * * @return * The name of this parameter. */ public String getName() { return name; } /** * Sets the name of this parameter. * * @param name * The name of this parameter. */ public void setName(String name) { this.name = name; } /** * Returns the value of this parameter. * * @return * The value of this parameter. */ public String getValue() { return value; } /** * Sets the value of this parameter. * * @param value * The value of this parameter. */ public void setValue(String value) { this.value = value; } } ././@LongLink0100644 0000000 0000000 00000000257 12645510255 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ModeledConnectionRecord.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000005120 12645510255 035260 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connection; import java.util.Date; import org.glyptodon.guacamole.net.auth.ConnectionRecord; /** * A ConnectionRecord which is backed by a database model. * * @author James Muehlner * @author Michael Jumper */ public class ModeledConnectionRecord implements ConnectionRecord { /** * The model object backing this connection record. */ private final ConnectionRecordModel model; /** * Creates a new ModeledConnectionRecord backed by the given model object. * Changes to this record will affect the backing model object, and changes * to the backing model object will affect this record. * * @param model * The model object to use to back this connection record. */ public ModeledConnectionRecord(ConnectionRecordModel model) { this.model = model; } @Override public String getConnectionIdentifier() { return model.getConnectionIdentifier(); } @Override public String getConnectionName() { return model.getConnectionName(); } @Override public Date getStartDate() { return model.getStartDate(); } @Override public Date getEndDate() { return model.getEndDate(); } @Override public String getRemoteHost() { return null; } @Override public String getUsername() { return model.getUsername(); } @Override public boolean isActive() { return false; } } ././@LongLink0100644 0000000 0000000 00000000265 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ModeledGuacamoleConfiguration.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000007403 12612226251 035260 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connection; import com.google.inject.Inject; import java.util.Map; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; /** * Implementation of GuacamoleConfiguration which loads parameter values only * if necessary, and only if allowed. * * @author Michael Jumper */ public class ModeledGuacamoleConfiguration extends GuacamoleConfiguration { /** * The user this configuration belongs to. Access is based on his/her * permission settings. */ private AuthenticatedUser currentUser; /** * The internal model object containing the values which represent the * connection associated with this configuration. */ private ConnectionModel connectionModel; /** * Service for managing connection parameters. */ @Inject private ConnectionService connectionService; /** * The manually-set parameter map, if any. */ private Map parameters = null; /** * Creates a new, empty ModelGuacamoleConfiguration. */ public ModeledGuacamoleConfiguration() { } /** * Initializes this configuration, associating it with the current * authenticated user and populating it with data from the given model * object. * * @param currentUser * The user that created or retrieved this configuration. * * @param connectionModel * The model object backing this configuration. */ public void init(AuthenticatedUser currentUser, ConnectionModel connectionModel) { this.currentUser = currentUser; this.connectionModel = connectionModel; } @Override public String getProtocol() { return connectionModel.getProtocol(); } @Override public void setProtocol(String protocol) { super.setProtocol(protocol); connectionModel.setProtocol(protocol); } @Override public void setParameters(Map parameters) { this.parameters = parameters; super.setParameters(parameters); } @Override public Map getParameters() { // Retrieve visible parameters, if not overridden by setParameters() if (parameters == null) { // Retrieve all visible parameters Map visibleParameters = connectionService.retrieveParameters(currentUser, connectionModel.getIdentifier()); // Use retrieved parameters to back future operations super.setParameters(visibleParameters); } return super.getParameters(); } } ././@LongLink0100644 0000000 0000000 00000000265 12645510255 011643 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordSortPredicate.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000005356 12645510255 035273 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connection; import org.glyptodon.guacamole.net.auth.ConnectionRecordSet; /** * A sort predicate which species the property to use when sorting connection * records, along with the sort order. * * @author James Muehlner */ public class ConnectionRecordSortPredicate { /** * The property to use when sorting ConnectionRecords. */ private final ConnectionRecordSet.SortableProperty property; /** * Whether the sort order is descending (true) or ascending (false). */ private final boolean descending; /** * Creates a new ConnectionRecordSortPredicate with the given sort property * and sort order. * * @param property * The property to use when sorting ConnectionRecords. * * @param descending * Whether the sort order is descending (true) or ascending (false). */ public ConnectionRecordSortPredicate(ConnectionRecordSet.SortableProperty property, boolean descending) { this.property = property; this.descending = descending; } /** * Returns the property that should be used when sorting ConnectionRecords. * * @return * The property that should be used when sorting ConnectionRecords. */ public ConnectionRecordSet.SortableProperty getProperty() { return property; } /** * Returns whether the sort order is descending. * * @return * true if the sort order is descending, false if the sort order is * ascending. */ public boolean isDescending() { return descending; } } ././@LongLink0100644 0000000 0000000 00000000244 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/package-info.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000002341 12612226251 035254 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes related to connections and their parameters and history. */ package org.glyptodon.guacamole.auth.jdbc.connection; ././@LongLink0100644 0000000 0000000 00000000256 12645510255 011643 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordMapper.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000010727 12645510255 035271 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connection; import java.util.Collection; import java.util.List; import org.apache.ibatis.annotations.Param; import org.glyptodon.guacamole.auth.jdbc.user.UserModel; /** * Mapper for connection record objects. * * @author Michael Jumper */ public interface ConnectionRecordMapper { /** * Returns a collection of all connection records associated with the * connection having the given identifier. * * @param identifier * The identifier of the connection whose records are to be retrieved. * * @return * A collection of all connection records associated with the * connection having the given identifier. This collection will be * empty if no such connection exists. */ List select(@Param("identifier") String identifier); /** * Inserts the given connection record. * * @param record * The connection record to insert. * * @return * The number of rows inserted. */ int insert(@Param("record") ConnectionRecordModel record); /** * Searches for up to limit connection records that contain * the given terms, sorted by the given predicates, regardless of whether * the data they are associated with is is readable by any particular user. * This should only be called on behalf of a system administrator. If * records are needed by a non-administrative user who must have explicit * read rights, use searchReadable() instead. * * @param terms * The search terms that must match the returned records. * * @param sortPredicates * A list of predicates to sort the returned records by, in order of * priority. * * @param limit * The maximum number of records that should be returned. * * @return * The results of the search performed with the given parameters. */ List search(@Param("terms") Collection terms, @Param("sortPredicates") List sortPredicates, @Param("limit") int limit); /** * Searches for up to limit connection records that contain * the given terms, sorted by the given predicates. Only records that are * associated with data explicitly readable by the given user will be * returned. If records are needed by a system administrator (who, by * definition, does not need explicit read rights), use search() instead. * * @param user * The user whose permissions should determine whether a record is * returned. * * @param terms * The search terms that must match the returned records. * * @param sortPredicates * A list of predicates to sort the returned records by, in order of * priority. * * @param limit * The maximum number of records that should be returned. * * @return * The results of the search performed with the given parameters. */ List searchReadable(@Param("user") UserModel user, @Param("terms") Collection terms, @Param("sortPredicates") List sortPredicates, @Param("limit") int limit); } ././@LongLink0100644 0000000 0000000 00000000253 12645510255 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordSet.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000007631 12645510255 035271 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connection; import com.google.inject.Inject; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.auth.jdbc.base.RestrictedObject; import org.glyptodon.guacamole.net.auth.ConnectionRecord; /** * A JDBC implementation of ConnectionRecordSet. Calls to asCollection() will * query connection history records from the database. Which records are * returned will be determined by the values passed in earlier. * * @author James Muehlner */ public class ConnectionRecordSet extends RestrictedObject implements org.glyptodon.guacamole.net.auth.ConnectionRecordSet { /** * Service for managing connection objects. */ @Inject private ConnectionService connectionService; /** * The set of strings that each must occur somewhere within the returned * connection records, whether within the associated username, the name of * the associated connection, or any associated date. If non-empty, any * connection record not matching each of the strings within the collection * will be excluded from the results. */ private final Set requiredContents = new HashSet(); /** * The maximum number of connection history records that should be returned * by a call to asCollection(). */ private int limit = Integer.MAX_VALUE; /** * A list of predicates to apply while sorting the resulting connection * records, describing the properties involved and the sort order for those * properties. */ private final List connectionRecordSortPredicates = new ArrayList(); @Override public Collection asCollection() throws GuacamoleException { return connectionService.retrieveHistory(getCurrentUser(), requiredContents, connectionRecordSortPredicates, limit); } @Override public ConnectionRecordSet contains(String value) throws GuacamoleException { requiredContents.add(new ConnectionRecordSearchTerm(value)); return this; } @Override public ConnectionRecordSet limit(int limit) throws GuacamoleException { this.limit = Math.min(this.limit, limit); return this; } @Override public ConnectionRecordSet sort(SortableProperty property, boolean desc) throws GuacamoleException { connectionRecordSortPredicates.add(new ConnectionRecordSortPredicate( property, desc )); return this; } } ././@LongLink0100644 0000000 0000000 00000000247 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ParameterMapper.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000005064 12612226251 035261 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connection; import java.util.Collection; import org.apache.ibatis.annotations.Param; /** * Mapper for connection parameter objects. * * @author Michael Jumper */ public interface ParameterMapper { /** * Returns a collection of all parameters associated with the connection * having the given identifier. * * @param identifier * The identifier of the connection whose parameters are to be * retrieved. * * @return * A collection of all parameters associated with the connection * having the given identifier. This collection will be empty if no * such connection exists. */ Collection select(@Param("identifier") String identifier); /** * Inserts each of the parameter model objects in the given collection as * new connection parameters. * * @param parameters * The connection parameters to insert. * * @return * The number of rows inserted. */ int insert(@Param("parameters") Collection parameters); /** * Deletes all parameters associated with the connection having the given * identifier. * * @param identifier * The identifier of the connection whose parameters should be * deleted. * * @return * The number of rows deleted. */ int delete(@Param("identifier") String identifier); } ././@LongLink0100644 0000000 0000000 00000000255 12645510255 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ConnectionRecordModel.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000013171 12645510255 035265 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connection; import java.util.Date; /** * A single connection record representing a past usage of a particular * connection. * * @author Michael Jumper */ public class ConnectionRecordModel { /** * The identifier of the connection associated with this connection record. */ private String connectionIdentifier; /** * The name of the connection associated with this connection record. */ private String connectionName; /** * The database ID of the user associated with this connection record. */ private Integer userID; /** * The username of the user associated with this connection record. */ private String username; /** * The time the connection was initiated by the associated user. */ private Date startDate; /** * The time the connection ended, or null if the end time is not known or * the connection is still running. */ private Date endDate; /** * Returns the identifier of the connection associated with this connection * record. * * @return * The identifier of the connection associated with this connection * record. */ public String getConnectionIdentifier() { return connectionIdentifier; } /** * Sets the identifier of the connection associated with this connection * record. * * @param connectionIdentifier * The identifier of the connection to associate with this connection * record. */ public void setConnectionIdentifier(String connectionIdentifier) { this.connectionIdentifier = connectionIdentifier; } /** * Returns the name of the connection associated with this connection * record. * * @return * The name of the connection associated with this connection * record. */ public String getConnectionName() { return connectionName; } /** * Sets the name of the connection associated with this connection * record. * * @param connectionName * The name of the connection to associate with this connection * record. */ public void setConnectionName(String connectionName) { this.connectionName = connectionName; } /** * Returns the database ID of the user associated with this connection * record. * * @return * The database ID of the user associated with this connection record. */ public Integer getUserID() { return userID; } /** * Sets the database ID of the user associated with this connection record. * * @param userID * The database ID of the user to associate with this connection * record. */ public void setUserID(Integer userID) { this.userID = userID; } /** * Returns the username of the user associated with this connection record. * * @return * The username of the user associated with this connection record. */ public String getUsername() { return username; } /** * Sets the username of the user associated with this connection record. * * @param username * The username of the user to associate with this connection record. */ public void setUsername(String username) { this.username = username; } /** * Returns the date that the associated connection was established. * * @return * The date the associated connection was established. */ public Date getStartDate() { return startDate; } /** * Sets the date that the associated connection was established. * * @param startDate * The date that the associated connection was established. */ public void setStartDate(Date startDate) { this.startDate = startDate; } /** * Returns the date that the associated connection ended, or null if no * end date was recorded. The lack of an end date does not necessarily * mean that the connection is still active. * * @return * The date the associated connection ended, or null if no end date was * recorded. */ public Date getEndDate() { return endDate; } /** * Sets the date that the associated connection ended. * * @param endDate * The date that the associated connection ended. */ public void setEndDate(Date endDate) { this.endDate = endDate; } } ././@LongLink0100644 0000000 0000000 00000000251 12645510255 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connection/ModeledConnection.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000020777 12645510255 035277 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connection; import com.google.inject.Inject; import com.google.inject.Provider; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.glyptodon.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.auth.jdbc.JDBCEnvironment; import org.glyptodon.guacamole.auth.jdbc.base.ModeledGroupedDirectoryObject; import org.glyptodon.guacamole.form.Field; import org.glyptodon.guacamole.form.Form; import org.glyptodon.guacamole.form.NumericField; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionRecord; import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * An implementation of the Connection object which is backed by a database * model. * * @author James Muehlner * @author Michael Jumper */ public class ModeledConnection extends ModeledGroupedDirectoryObject implements Connection { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(ModeledConnection.class); /** * The name of the attribute which controls the maximum number of * concurrent connections. */ public static final String MAX_CONNECTIONS_NAME = "max-connections"; /** * The name of the attribute which controls the maximum number of * concurrent connections per user. */ public static final String MAX_CONNECTIONS_PER_USER_NAME = "max-connections-per-user"; /** * All attributes related to restricting user accounts, within a logical * form. */ public static final Form CONCURRENCY_LIMITS = new Form("concurrency", Arrays.asList( new NumericField(MAX_CONNECTIONS_NAME), new NumericField(MAX_CONNECTIONS_PER_USER_NAME) )); /** * All possible attributes of connection objects organized as individual, * logical forms. */ public static final Collection ATTRIBUTES = Collections.unmodifiableCollection(Arrays.asList( CONCURRENCY_LIMITS )); /** * The environment of the Guacamole server. */ @Inject private JDBCEnvironment environment; /** * Service for managing connections. */ @Inject private ConnectionService connectionService; /** * Service for creating and tracking tunnels. */ @Inject private GuacamoleTunnelService tunnelService; /** * Provider for lazy-loaded, permission-controlled configurations. */ @Inject private Provider configProvider; /** * The manually-set GuacamoleConfiguration, if any. */ private GuacamoleConfiguration config = null; /** * Creates a new, empty ModeledConnection. */ public ModeledConnection() { } @Override public String getName() { return getModel().getName(); } @Override public void setName(String name) { getModel().setName(name); } @Override public GuacamoleConfiguration getConfiguration() { // If configuration has been manually set, return that if (config != null) return config; // Otherwise, return permission-controlled configuration ModeledGuacamoleConfiguration restrictedConfig = configProvider.get(); restrictedConfig.init(getCurrentUser(), getModel()); return restrictedConfig; } @Override public void setConfiguration(GuacamoleConfiguration config) { // Store manually-set configuration internally this.config = config; // Update model getModel().setProtocol(config.getProtocol()); } @Override public List getHistory() throws GuacamoleException { return connectionService.retrieveHistory(getCurrentUser(), this); } @Override public GuacamoleTunnel connect(GuacamoleClientInformation info) throws GuacamoleException { return connectionService.connect(getCurrentUser(), this, info); } @Override public int getActiveConnections() { return tunnelService.getActiveConnections(this).size(); } @Override public Map getAttributes() { Map attributes = new HashMap(); // Set connection limit attribute attributes.put(MAX_CONNECTIONS_NAME, NumericField.format(getModel().getMaxConnections())); // Set per-user connection limit attribute attributes.put(MAX_CONNECTIONS_PER_USER_NAME, NumericField.format(getModel().getMaxConnectionsPerUser())); return attributes; } @Override public void setAttributes(Map attributes) { // Translate connection limit attribute try { getModel().setMaxConnections(NumericField.parse(attributes.get(MAX_CONNECTIONS_NAME))); } catch (NumberFormatException e) { logger.warn("Not setting maximum connections: {}", e.getMessage()); logger.debug("Unable to parse numeric attribute.", e); } // Translate per-user connection limit attribute try { getModel().setMaxConnectionsPerUser(NumericField.parse(attributes.get(MAX_CONNECTIONS_PER_USER_NAME))); } catch (NumberFormatException e) { logger.warn("Not setting maximum connections per user: {}", e.getMessage()); logger.debug("Unable to parse numeric attribute.", e); } } /** * Returns the maximum number of connections that should be allowed to this * connection overall. If no limit applies, zero is returned. * * @return * The maximum number of connections that should be allowed to this * connection overall, or zero if no limit applies. * * @throws GuacamoleException * If an error occurs while parsing the concurrency limit properties * specified within guacamole.properties. */ public int getMaxConnections() throws GuacamoleException { // Pull default from environment if connection limit is unset Integer value = getModel().getMaxConnections(); if (value == null) return environment.getDefaultMaxConnections(); // Otherwise use defined value return value; } /** * Returns the maximum number of connections that should be allowed to this * connection for any individual user. If no limit applies, zero is * returned. * * @return * The maximum number of connections that should be allowed to this * connection for any individual user, or zero if no limit applies. * * @throws GuacamoleException * If an error occurs while parsing the concurrency limit properties * specified within guacamole.properties. */ public int getMaxConnectionsPerUser() throws GuacamoleException { // Pull default from environment if per-user connection limit is unset Integer value = getModel().getMaxConnectionsPerUser(); if (value == null) return environment.getDefaultMaxConnectionsPerUser(); // Otherwise use defined value return value; } } ././@LongLink0100644 0000000 0000000 00000000252 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/GuacamoleTunnelService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000013572 12612226251 035264 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.tunnel; import java.util.Collection; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; /** * Service which creates pre-configured GuacamoleSocket instances for * connections and balancing groups, applying concurrent usage rules. * * @author Michael Jumper */ public interface GuacamoleTunnelService { /** * Returns a collection containing connection records representing all * currently-active connections visible by the given user. * * @param user * The user retrieving active connections. * * @return * A collection containing connection records representing all * currently-active connections. * * @throws GuacamoleException * If an error occurs while retrieving all active connections, or if * permission is denied. */ public Collection getActiveConnections(AuthenticatedUser user) throws GuacamoleException; /** * Creates a socket for the given user which connects to the given * connection. The given client information will be passed to guacd when * the connection is established. This function will apply any concurrent * usage rules in effect, but will NOT test object- or system-level * permissions. * * @param user * The user for whom the connection is being established. * * @param connection * The connection the user is connecting to. * * @param info * Information describing the Guacamole client connecting to the given * connection. * * @return * A new GuacamoleTunnel which is configured and connected to the given * connection. * * @throws GuacamoleException * If the connection cannot be established due to concurrent usage * rules. */ GuacamoleTunnel getGuacamoleTunnel(AuthenticatedUser user, ModeledConnection connection, GuacamoleClientInformation info) throws GuacamoleException; /** * Returns a collection containing connection records representing all * currently-active connections using the given connection. These records * will have usernames and start dates, but no end date, and will be * sorted in ascending order by start date. * * @param connection * The connection to check. * * @return * A collection containing connection records representing all * currently-active connections. */ public Collection getActiveConnections(Connection connection); /** * Creates a socket for the given user which connects to the given * connection group. The given client information will be passed to guacd * when the connection is established. This function will apply any * concurrent usage rules in effect, but will NOT test object- or * system-level permissions. * * @param user * The user for whom the connection is being established. * * @param connectionGroup * The connection group the user is connecting to. * * @param info * Information describing the Guacamole client connecting to the given * connection group. * * @return * A new GuacamoleTunnel which is configured and connected to the given * connection group. * * @throws GuacamoleException * If the connection cannot be established due to concurrent usage * rules, or if the connection group is not balancing. */ GuacamoleTunnel getGuacamoleTunnel(AuthenticatedUser user, ModeledConnectionGroup connectionGroup, GuacamoleClientInformation info) throws GuacamoleException; /** * Returns a collection containing connection records representing all * currently-active connections using the given connection group. These * records will have usernames and start dates, but no end date, and will * be sorted in ascending order by start date. * * @param connectionGroup * The connection group to check. * * @return * A collection containing connection records representing all * currently-active connections. */ public Collection getActiveConnections(ConnectionGroup connectionGroup); } ././@LongLink0100644 0000000 0000000 00000000255 12612226251 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/ManagedSSLGuacamoleSocket.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000005255 12612226251 035263 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.tunnel; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.SSLGuacamoleSocket; /** * Implementation of GuacamoleSocket which connects via SSL to a given hostname * and port. If the socket is closed for any reason, a given task is run. * * @author Michael Jumper */ public class ManagedSSLGuacamoleSocket extends SSLGuacamoleSocket { /** * The task to run when the socket is closed. */ private final Runnable socketClosedTask; /** * Creates a new socket which connects via SSL to a given hostname and * port. If the socket is closed for any reason, the given task is run. * * @param hostname * The hostname of the Guacamole proxy server to connect to. * * @param port * The port of the Guacamole proxy server to connect to. * * @param socketClosedTask * The task to run when the socket is closed. This task will NOT be * run if an exception occurs during connection, and this * ManagedInetGuacamoleSocket instance is ultimately not created. * * @throws GuacamoleException * If an error occurs while connecting to the Guacamole proxy server. */ public ManagedSSLGuacamoleSocket(String hostname, int port, Runnable socketClosedTask) throws GuacamoleException { super(hostname, port); this.socketClosedTask = socketClosedTask; } @Override public void close() throws GuacamoleException { super.close(); socketClosedTask.run(); } } ././@LongLink0100644 0000000 0000000 00000000256 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/ManagedInetGuacamoleSocket.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000005261 12612226251 035260 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.tunnel; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.InetGuacamoleSocket; /** * Implementation of GuacamoleSocket which connects via TCP to a given hostname * and port. If the socket is closed for any reason, a given task is run. * * @author Michael Jumper */ public class ManagedInetGuacamoleSocket extends InetGuacamoleSocket { /** * The task to run when the socket is closed. */ private final Runnable socketClosedTask; /** * Creates a new socket which connects via TCP to a given hostname and * port. If the socket is closed for any reason, the given task is run. * * @param hostname * The hostname of the Guacamole proxy server to connect to. * * @param port * The port of the Guacamole proxy server to connect to. * * @param socketClosedTask * The task to run when the socket is closed. This task will NOT be * run if an exception occurs during connection, and this * ManagedInetGuacamoleSocket instance is ultimately not created. * * @throws GuacamoleException * If an error occurs while connecting to the Guacamole proxy server. */ public ManagedInetGuacamoleSocket(String hostname, int port, Runnable socketClosedTask) throws GuacamoleException { super(hostname, port); this.socketClosedTask = socketClosedTask; } @Override public void close() throws GuacamoleException { super.close(); socketClosedTask.run(); } } ././@LongLink0100644 0000000 0000000 00000000264 12645510255 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/RestrictedGuacamoleTunnelService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000017612 12645510255 035271 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.tunnel; import com.google.common.collect.ConcurrentHashMultiset; import com.google.inject.Singleton; import java.util.Arrays; import java.util.Comparator; import java.util.List; import org.glyptodon.guacamole.GuacamoleClientTooManyException; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleResourceConflictException; import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; /** * GuacamoleTunnelService implementation which restricts concurrency for each * connection and group according to a maximum number of connections and * maximum number of connections per user. * * @author James Muehlner * @author Michael Jumper */ @Singleton public class RestrictedGuacamoleTunnelService extends AbstractGuacamoleTunnelService { /** * Set of all currently-active user/connection pairs (seats). */ private final ConcurrentHashMultiset activeSeats = ConcurrentHashMultiset.create(); /** * Set of all currently-active connections. */ private final ConcurrentHashMultiset activeConnections = ConcurrentHashMultiset.create(); /** * Set of all currently-active user/connection group pairs (seats). */ private final ConcurrentHashMultiset activeGroupSeats = ConcurrentHashMultiset.create(); /** * Set of all currently-active connection groups. */ private final ConcurrentHashMultiset activeGroups = ConcurrentHashMultiset.create(); /** * Attempts to add a single instance of the given value to the given * multiset without exceeding the specified maximum number of values. If * the value cannot be added without exceeding the maximum, false is * returned. * * @param * The type of values contained within the multiset. * * @param multiset * The multiset to attempt to add a value to. * * @param value * The value to attempt to add. * * @param max * The maximum number of each distinct value that the given multiset * should hold, or zero if no limit applies. * * @return * true if the value was successfully added without exceeding the * specified maximum, false if the value could not be added. */ private boolean tryAdd(ConcurrentHashMultiset multiset, T value, int max) { // Repeatedly attempt to add a new value to the given multiset until we // explicitly succeed or explicitly fail while (true) { // Get current number of values int count = multiset.count(value); // Bail out if the maximum has already been reached if (count >= max && max != 0) return false; // Attempt to add one more value if (multiset.setCount(value, count, count+1)) return true; // Try again if unsuccessful } } @Override protected ModeledConnection acquire(AuthenticatedUser user, List connections) throws GuacamoleException { // Get username String username = user.getUser().getIdentifier(); // Sort connections in ascending order of usage ModeledConnection[] sortedConnections = connections.toArray(new ModeledConnection[connections.size()]); Arrays.sort(sortedConnections, new Comparator() { @Override public int compare(ModeledConnection a, ModeledConnection b) { return getActiveConnections(a).size() - getActiveConnections(b).size(); } }); // Track whether acquire fails due to user-specific limits boolean userSpecificFailure = true; // Return the first unreserved connection for (ModeledConnection connection : sortedConnections) { // Attempt to aquire connection according to per-user limits Seat seat = new Seat(username, connection.getIdentifier()); if (tryAdd(activeSeats, seat, connection.getMaxConnectionsPerUser())) { // Attempt to aquire connection according to overall limits if (tryAdd(activeConnections, connection.getIdentifier(), connection.getMaxConnections())) return connection; // Acquire failed - retry with next connection activeSeats.remove(seat); // Failure to acquire is not user-specific userSpecificFailure = false; } } // Too many connections by this user if (userSpecificFailure) throw new GuacamoleClientTooManyException("Cannot connect. Connection already in use by this user."); // Too many connections, but not necessarily due purely to this user else throw new GuacamoleResourceConflictException("Cannot connect. This connection is in use."); } @Override protected void release(AuthenticatedUser user, ModeledConnection connection) { activeSeats.remove(new Seat(user.getUser().getIdentifier(), connection.getIdentifier())); activeConnections.remove(connection.getIdentifier()); } @Override protected void acquire(AuthenticatedUser user, ModeledConnectionGroup connectionGroup) throws GuacamoleException { // Get username String username = user.getUser().getIdentifier(); // Attempt to aquire connection group according to per-user limits Seat seat = new Seat(username, connectionGroup.getIdentifier()); if (tryAdd(activeGroupSeats, seat, connectionGroup.getMaxConnectionsPerUser())) { // Attempt to aquire connection group according to overall limits if (tryAdd(activeGroups, connectionGroup.getIdentifier(), connectionGroup.getMaxConnections())) return; // Acquire failed activeGroupSeats.remove(seat); // Failure to acquire is not user-specific throw new GuacamoleResourceConflictException("Cannot connect. This connection group is in use."); } // Already in use by this user throw new GuacamoleClientTooManyException("Cannot connect. Connection group already in use by this user."); } @Override protected void release(AuthenticatedUser user, ModeledConnectionGroup connectionGroup) { activeGroupSeats.remove(new Seat(user.getUser().getIdentifier(), connectionGroup.getIdentifier())); activeGroups.remove(connectionGroup.getIdentifier()); } } ././@LongLink0100644 0000000 0000000 00000000262 12645510255 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000051305 12645510255 035266 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.tunnel; import com.google.inject.Inject; import com.google.inject.Provider; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; import org.glyptodon.guacamole.auth.jdbc.connection.ConnectionRecordMapper; import org.glyptodon.guacamole.auth.jdbc.connection.ParameterMapper; import org.glyptodon.guacamole.auth.jdbc.connection.ConnectionModel; import org.glyptodon.guacamole.auth.jdbc.connection.ConnectionRecordModel; import org.glyptodon.guacamole.auth.jdbc.connection.ParameterModel; import org.glyptodon.guacamole.auth.jdbc.user.UserModel; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.auth.jdbc.JDBCEnvironment; import org.glyptodon.guacamole.auth.jdbc.connection.ConnectionMapper; import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.net.GuacamoleSocket; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.protocol.ConfiguredGuacamoleSocket; import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; import org.glyptodon.guacamole.token.StandardTokens; import org.glyptodon.guacamole.token.TokenFilter; import org.mybatis.guice.transactional.Transactional; /** * Base implementation of the GuacamoleTunnelService, handling retrieval of * connection parameters, load balancing, and connection usage counts. The * implementation of concurrency rules is up to policy-specific subclasses. * * @author Michael Jumper */ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelService { /** * The environment of the Guacamole server. */ @Inject private JDBCEnvironment environment; /** * Mapper for accessing connections. */ @Inject private ConnectionMapper connectionMapper; /** * Provider for creating connections. */ @Inject private Provider connectionProvider; /** * Mapper for accessing connection parameters. */ @Inject private ParameterMapper parameterMapper; /** * Mapper for accessing connection history. */ @Inject private ConnectionRecordMapper connectionRecordMapper; /** * The hostname to use when connecting to guacd if no hostname is provided * within guacamole.properties. */ private static final String DEFAULT_GUACD_HOSTNAME = "localhost"; /** * The port to use when connecting to guacd if no port is provided within * guacamole.properties. */ private static final int DEFAULT_GUACD_PORT = 4822; /** * All active connections through the tunnel having a given UUID. */ private final Map activeTunnels = new ConcurrentHashMap(); /** * All active connections to a connection having a given identifier. */ private final ActiveConnectionMultimap activeConnections = new ActiveConnectionMultimap(); /** * All active connections to a connection group having a given identifier. */ private final ActiveConnectionMultimap activeConnectionGroups = new ActiveConnectionMultimap(); /** * Acquires possibly-exclusive access to any one of the given connections * on behalf of the given user. If access is denied for any reason, or if * no connection is available, an exception is thrown. * * @param user * The user acquiring access. * * @param connections * The connections being accessed. * * @return * The connection that has been acquired on behalf of the given user. * * @throws GuacamoleException * If access is denied to the given user for any reason. */ protected abstract ModeledConnection acquire(AuthenticatedUser user, List connections) throws GuacamoleException; /** * Releases possibly-exclusive access to the given connection on behalf of * the given user. If the given user did not already have access, the * behavior of this function is undefined. * * @param user * The user releasing access. * * @param connection * The connection being released. */ protected abstract void release(AuthenticatedUser user, ModeledConnection connection); /** * Acquires possibly-exclusive access to the given connection group on * behalf of the given user. If access is denied for any reason, an * exception is thrown. * * @param user * The user acquiring access. * * @param connectionGroup * The connection group being accessed. * * @throws GuacamoleException * If access is denied to the given user for any reason. */ protected abstract void acquire(AuthenticatedUser user, ModeledConnectionGroup connectionGroup) throws GuacamoleException; /** * Releases possibly-exclusive access to the given connection group on * behalf of the given user. If the given user did not already have access, * the behavior of this function is undefined. * * @param user * The user releasing access. * * @param connectionGroup * The connection group being released. */ protected abstract void release(AuthenticatedUser user, ModeledConnectionGroup connectionGroup); /** * Returns a guacamole configuration containing the protocol and parameters * from the given connection. If tokens are used in the connection * parameter values, credentials from the given user will be substituted * appropriately. * * @param user * The user whose credentials should be used if necessary. * * @param connection * The connection whose protocol and parameters should be added to the * returned configuration. * * @return * A GuacamoleConfiguration containing the protocol and parameters from * the given connection. */ private GuacamoleConfiguration getGuacamoleConfiguration(AuthenticatedUser user, ModeledConnection connection) { // Generate configuration from available data GuacamoleConfiguration config = new GuacamoleConfiguration(); // Set protocol from connection ConnectionModel model = connection.getModel(); config.setProtocol(model.getProtocol()); // Set parameters from associated data Collection parameters = parameterMapper.select(connection.getIdentifier()); for (ParameterModel parameter : parameters) config.setParameter(parameter.getName(), parameter.getValue()); // Build token filter containing credential tokens TokenFilter tokenFilter = new TokenFilter(); StandardTokens.addStandardTokens(tokenFilter, user.getCredentials()); // Filter the configuration tokenFilter.filterValues(config.getParameters()); return config; } /** * Saves the given ActiveConnectionRecord to the database. The end date of * the saved record will be populated with the current time. * * @param record * The record to save. */ private void saveConnectionRecord(ActiveConnectionRecord record) { // Get associated connection ModeledConnection connection = record.getConnection(); // Get associated models AuthenticatedUser user = record.getUser(); UserModel userModel = user.getUser().getModel(); ConnectionRecordModel recordModel = new ConnectionRecordModel(); // Copy user information and timestamps into new record recordModel.setUserID(userModel.getObjectID()); recordModel.setUsername(userModel.getIdentifier()); recordModel.setConnectionIdentifier(connection.getIdentifier()); recordModel.setStartDate(record.getStartDate()); recordModel.setEndDate(new Date()); // Insert connection record connectionRecordMapper.insert(recordModel); } /** * Returns an unconfigured GuacamoleSocket that is already connected to * guacd as specified in guacamole.properties, using SSL if necessary. * * @return * An unconfigured GuacamoleSocket, already connected to guacd. * * @throws GuacamoleException * If an error occurs while connecting to guacd, or while parsing * guacd-related properties. */ private GuacamoleSocket getUnconfiguredGuacamoleSocket(Runnable socketClosedCallback) throws GuacamoleException { // Use SSL if requested if (environment.getProperty(Environment.GUACD_SSL, false)) return new ManagedSSLGuacamoleSocket( environment.getProperty(Environment.GUACD_HOSTNAME, DEFAULT_GUACD_HOSTNAME), environment.getProperty(Environment.GUACD_PORT, DEFAULT_GUACD_PORT), socketClosedCallback ); // Otherwise, just use straight TCP return new ManagedInetGuacamoleSocket( environment.getProperty(Environment.GUACD_HOSTNAME, DEFAULT_GUACD_HOSTNAME), environment.getProperty(Environment.GUACD_PORT, DEFAULT_GUACD_PORT), socketClosedCallback ); } /** * Task which handles cleanup of a connection associated with some given * ActiveConnectionRecord. */ private class ConnectionCleanupTask implements Runnable { /** * Whether this task has run. */ private final AtomicBoolean hasRun = new AtomicBoolean(false); /** * The ActiveConnectionRecord whose connection will be cleaned up once * this task runs. */ private final ActiveConnectionRecord activeConnection; /** * Creates a new task which automatically cleans up after the * connection associated with the given ActiveConnectionRecord. The * connection and parent group will be removed from the maps of active * connections and groups, and exclusive access will be released. * * @param activeConnection * The ActiveConnectionRecord whose associated connection should be * cleaned up once this task runs. */ public ConnectionCleanupTask(ActiveConnectionRecord activeConnection) { this.activeConnection = activeConnection; } @Override public void run() { // Only run once if (!hasRun.compareAndSet(false, true)) return; // Get original user and connection AuthenticatedUser user = activeConnection.getUser(); ModeledConnection connection = activeConnection.getConnection(); // Get associated identifiers String identifier = connection.getIdentifier(); String parentIdentifier = connection.getParentIdentifier(); // Release connection activeTunnels.remove(activeConnection.getUUID().toString()); activeConnections.remove(identifier, activeConnection); activeConnectionGroups.remove(parentIdentifier, activeConnection); release(user, connection); // Release any associated group if (activeConnection.hasBalancingGroup()) release(user, activeConnection.getBalancingGroup()); // Save history record to database saveConnectionRecord(activeConnection); } } /** * Creates a tunnel for the given user which connects to the given * connection, which MUST already be acquired via acquire(). The given * client information will be passed to guacd when the connection is * established. * * The connection will be automatically released when it closes, or if it * fails to establish entirely. * * @param activeConnection * The active connection record of the connection in use. * * @param info * Information describing the Guacamole client connecting to the given * connection. * * @return * A new GuacamoleTunnel which is configured and connected to the given * connection. * * @throws GuacamoleException * If an error occurs while the connection is being established, or * while connection configuration information is being retrieved. */ private GuacamoleTunnel assignGuacamoleTunnel(ActiveConnectionRecord activeConnection, GuacamoleClientInformation info) throws GuacamoleException { ModeledConnection connection = activeConnection.getConnection(); // Record new active connection Runnable cleanupTask = new ConnectionCleanupTask(activeConnection); activeTunnels.put(activeConnection.getUUID().toString(), activeConnection); activeConnections.put(connection.getIdentifier(), activeConnection); activeConnectionGroups.put(connection.getParentIdentifier(), activeConnection); try { // Obtain socket which will automatically run the cleanup task GuacamoleSocket socket = new ConfiguredGuacamoleSocket( getUnconfiguredGuacamoleSocket(cleanupTask), getGuacamoleConfiguration(activeConnection.getUser(), connection), info ); // Assign and return new tunnel return activeConnection.assignGuacamoleTunnel(socket); } // Execute cleanup if socket could not be created catch (GuacamoleException e) { cleanupTask.run(); throw e; } } /** * Returns a list of all balanced connections within a given connection * group. If the connection group is not balancing, or it contains no * connections, an empty list is returned. * * @param user * The user on whose behalf the balanced connections within the given * connection group are being retrieved. * * @param connectionGroup * The connection group to retrieve the balanced connections of. * * @return * A list containing all balanced connections within the given group, * or an empty list if there are no such connections. */ private List getBalancedConnections(AuthenticatedUser user, ModeledConnectionGroup connectionGroup) { // If not a balancing group, there are no balanced connections if (connectionGroup.getType() != ConnectionGroup.Type.BALANCING) return Collections.emptyList(); // If group has no children, there are no balanced connections Collection identifiers = connectionMapper.selectIdentifiersWithin(connectionGroup.getIdentifier()); if (identifiers.isEmpty()) return Collections.emptyList(); // Retrieve all children Collection models = connectionMapper.select(identifiers); List connections = new ArrayList(models.size()); // Convert each retrieved model to a modeled connection for (ConnectionModel model : models) { ModeledConnection connection = connectionProvider.get(); connection.init(user, model); connections.add(connection); } return connections; } @Override public Collection getActiveConnections(AuthenticatedUser user) throws GuacamoleException { // Simply return empty list if there are no active tunnels Collection records = activeTunnels.values(); if (records.isEmpty()) return Collections.emptyList(); // A system administrator can view all connections; no need to filter if (user.getUser().isAdministrator()) return records; // Build set of all connection identifiers associated with active tunnels Set identifiers = new HashSet(records.size()); for (ActiveConnectionRecord record : records) identifiers.add(record.getConnection().getIdentifier()); // Produce collection of readable connection identifiers Collection connections = connectionMapper.selectReadable(user.getUser().getModel(), identifiers); // Ensure set contains only identifiers of readable connections identifiers.clear(); for (ConnectionModel connection : connections) identifiers.add(connection.getIdentifier()); // Produce readable subset of records Collection visibleRecords = new ArrayList(records.size()); for (ActiveConnectionRecord record : records) { if (identifiers.contains(record.getConnection().getIdentifier())) visibleRecords.add(record); } return visibleRecords; } @Override @Transactional public GuacamoleTunnel getGuacamoleTunnel(final AuthenticatedUser user, final ModeledConnection connection, GuacamoleClientInformation info) throws GuacamoleException { // Acquire and connect to single connection acquire(user, Collections.singletonList(connection)); return assignGuacamoleTunnel(new ActiveConnectionRecord(user, connection), info); } @Override public Collection getActiveConnections(Connection connection) { return activeConnections.get(connection.getIdentifier()); } @Override @Transactional public GuacamoleTunnel getGuacamoleTunnel(AuthenticatedUser user, ModeledConnectionGroup connectionGroup, GuacamoleClientInformation info) throws GuacamoleException { // If group has no associated balanced connections, cannot connect List connections = getBalancedConnections(user, connectionGroup); if (connections.isEmpty()) throw new GuacamoleSecurityException("Permission denied."); // Acquire group acquire(user, connectionGroup); // Attempt to acquire to any child ModeledConnection connection; try { connection = acquire(user, connections); } // Ensure connection group is always released if child acquire fails catch (GuacamoleException e) { release(user, connectionGroup); throw e; } // Connect to acquired child return assignGuacamoleTunnel(new ActiveConnectionRecord(user, connectionGroup, connection), info); } @Override public Collection getActiveConnections(ConnectionGroup connectionGroup) { // If not a balancing group, assume no connections if (connectionGroup.getType() != ConnectionGroup.Type.BALANCING) return Collections.emptyList(); return activeConnectionGroups.get(connectionGroup.getIdentifier()); } } ././@LongLink0100644 0000000 0000000 00000000254 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/ActiveConnectionMultimap.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000011114 12612226251 035252 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.tunnel; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; /** * Mapping of object identifiers to lists of connection records. Records are * added or removed individually, and the overall list of current records * associated with a given object can be retrieved at any time. The public * methods of this class are all threadsafe. * * @author Michael Jumper */ public class ActiveConnectionMultimap { /** * All active connections to a connection having a given identifier. */ private final Map> records = new HashMap>(); /** * Stores the given connection record in the list of active connections * associated with the object having the given identifier. * * @param identifier * The identifier of the object being connected to. * * @param record * The record associated with the active connection. */ public void put(String identifier, ActiveConnectionRecord record) { synchronized (records) { // Get set of active connection records, creating if necessary Set connections = records.get(identifier); if (connections == null) { connections = Collections.synchronizedSet(Collections.newSetFromMap(new LinkedHashMap())); records.put(identifier, connections); } // Add active connection connections.add(record); } } /** * Removes the given connection record from the list of active connections * associated with the object having the given identifier. * * @param identifier * The identifier of the object being disconnected from. * * @param record * The record associated with the active connection. */ public void remove(String identifier, ActiveConnectionRecord record) { synchronized (records) { // Get set of active connection records Set connections = records.get(identifier); assert(connections != null); // Remove old record connections.remove(record); // If now empty, clean the tracking entry if (connections.isEmpty()) records.remove(identifier); } } /** * Returns a collection of active connection records associated with the * object having the given identifier. The collection will be sorted in * insertion order. If there are no such connections, an empty collection is * returned. * * @param identifier * The identifier of the object to check. * * @return * An immutable collection of records associated with the object having * the given identifier, or an empty collection if there are no such * records. */ public Collection get(String identifier) { synchronized (records) { // Get set of active connection records Collection connections = records.get(identifier); if (connections != null) return Collections.unmodifiableCollection(connections); return Collections.emptyList(); } } } ././@LongLink0100644 0000000 0000000 00000000240 12612226251 011626 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/package-info.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000002404 12612226251 035254 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes related to obtaining/configuring Guacamole tunnels, and restricting * access to those tunnels. */ package org.glyptodon.guacamole.auth.jdbc.tunnel; ././@LongLink0100644 0000000 0000000 00000000230 12612226251 011625 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/Seat.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000005355 12612226251 035264 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.tunnel; /** * A unique pairing of user and connection or connection group. * * @author Michael Jumper */ public class Seat { /** * The user using this seat. */ private final String username; /** * The connection or connection group associated with this seat. */ private final String identifier; /** * Creates a new seat which associated the given user with the given * connection or connection group. * * @param username * The username of the user using this seat. * * @param identifier * The identifier of the connection or connection group associated with * this seat. */ public Seat(String username, String identifier) { this.username = username; this.identifier = identifier; } @Override public int hashCode() { // The various properties will never be null assert(username != null); assert(identifier != null); // Derive hashcode from username and connection identifier int hash = 5; hash = 37 * hash + username.hashCode(); hash = 37 * hash + identifier.hashCode(); return hash; } @Override public boolean equals(Object object) { // We are only comparing against other seats here assert(object instanceof Seat); Seat seat = (Seat) object; // The various properties will never be null assert(seat.username != null); assert(seat.identifier != null); return username.equals(seat.username) && identifier.equals(seat.identifier); } } ././@LongLink0100644 0000000 0000000 00000000252 12645510255 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000017653 12645510255 035276 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.tunnel; import java.util.Date; import java.util.UUID; import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.net.AbstractGuacamoleTunnel; import org.glyptodon.guacamole.net.GuacamoleSocket; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.ConnectionRecord; /** * A connection record implementation that describes an active connection. As * the associated connection has not yet ended, getEndDate() will always return * null, and isActive() will always return true. The associated start date will * be the time of this objects creation. * * @author Michael Jumper */ public class ActiveConnectionRecord implements ConnectionRecord { /** * The user that connected to the connection associated with this connection * record. */ private final AuthenticatedUser user; /** * The balancing group from which the associated connection was chosen, if * any. If no balancing group was used, this will be null. */ private final ModeledConnectionGroup balancingGroup; /** * The connection associated with this connection record. */ private final ModeledConnection connection; /** * The time this connection record was created. */ private final Date startDate = new Date(); /** * The UUID that will be assigned to the underlying tunnel. */ private final UUID uuid = UUID.randomUUID(); /** * The GuacamoleTunnel used by the connection associated with this * connection record. */ private GuacamoleTunnel tunnel; /** * Creates a new connection record associated with the given user, * connection, and balancing connection group. The given balancing * connection group MUST be the connection group from which the given * connection was chosen. The start date of this connection record will be * the time of its creation. * * @param user * The user that connected to the connection associated with this * connection record. * * @param balancingGroup * The balancing group from which the given connection was chosen. * * @param connection * The connection to associate with this connection record. */ public ActiveConnectionRecord(AuthenticatedUser user, ModeledConnectionGroup balancingGroup, ModeledConnection connection) { this.user = user; this.balancingGroup = balancingGroup; this.connection = connection; } /** * Creates a new connection record associated with the given user and * connection. The start date of this connection record will be the time of * its creation. * * @param user * The user that connected to the connection associated with this * connection record. * * @param connection * The connection to associate with this connection record. */ public ActiveConnectionRecord(AuthenticatedUser user, ModeledConnection connection) { this(user, null, connection); } /** * Returns the user that connected to the connection associated with this * connection record. * * @return * The user that connected to the connection associated with this * connection record. */ public AuthenticatedUser getUser() { return user; } /** * Returns the balancing group from which the connection associated with * this connection record was chosen. * * @return * The balancing group from which the connection associated with this * connection record was chosen. */ public ModeledConnectionGroup getBalancingGroup() { return balancingGroup; } /** * Returns the connection associated with this connection record. * * @return * The connection associated with this connection record. */ public ModeledConnection getConnection() { return connection; } /** * Returns whether the connection associated with this connection record * was chosen from a balancing group. * * @return * true if the connection associated with this connection record was * chosen from a balancing group, false otherwise. */ public boolean hasBalancingGroup() { return balancingGroup != null; } @Override public String getConnectionIdentifier() { return connection.getIdentifier(); } @Override public String getConnectionName() { return connection.getName(); } @Override public Date getStartDate() { return startDate; } @Override public Date getEndDate() { // Active connections have not yet ended return null; } @Override public String getRemoteHost() { return user.getRemoteHost(); } @Override public String getUsername() { return user.getUser().getIdentifier(); } @Override public boolean isActive() { // Active connections are active by definition return true; } /** * Returns the GuacamoleTunnel currently associated with the active * connection represented by this connection record. * * @return * The GuacamoleTunnel currently associated with the active connection * represented by this connection record. */ public GuacamoleTunnel getTunnel() { return tunnel; } /** * Associates a new GuacamoleTunnel with this connection record using the * given socket. * * @param socket * The GuacamoleSocket to use to create the tunnel associated with this * connection record. * * @return * The newly-created tunnel associated with this connection record. */ public GuacamoleTunnel assignGuacamoleTunnel(final GuacamoleSocket socket) { // Create tunnel with given socket this.tunnel = new AbstractGuacamoleTunnel() { @Override public GuacamoleSocket getSocket() { return socket; } @Override public UUID getUUID() { return uuid; } }; // Return newly-created tunnel return this.tunnel; } /** * Returns the UUID of the underlying tunnel. If there is no underlying * tunnel, this will be the UUID assigned to the underlying tunnel when the * tunnel is set. * * @return * The current or future UUID of the underlying tunnel. */ public UUID getUUID() { return uuid; } } ././@LongLink0100644 0000000 0000000 00000000250 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/PermissionMapper.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000004761 12612226251 035264 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; import java.util.Collection; import org.glyptodon.guacamole.auth.jdbc.user.UserModel; import org.apache.ibatis.annotations.Param; /** * Generic base for mappers which handle permissions. * * @author Michael Jumper * @param * The type of permission model object handled by this mapper. */ public interface PermissionMapper { /** * Retrieves all permissions associated with the given user. * * @param user * The user to retrieve permissions for. * * @return * All permissions associated with the given user. */ Collection select(@Param("user") UserModel user); /** * Inserts the given permissions into the database. If any permissions * already exist, they will be ignored. * * @param permissions * The permissions to insert. * * @return * The number of rows inserted. */ int insert(@Param("permissions") Collection permissions); /** * Deletes the given permissions from the database. If any permissions do * not exist, they will be ignored. * * @param permissions * The permissions to delete. * * @return * The number of rows deleted. */ int delete(@Param("permissions") Collection permissions); }././@LongLink0100644 0000000 0000000 00000000256 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/SystemPermissionMapper.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000004041 12612226251 035253 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; import org.glyptodon.guacamole.auth.jdbc.user.UserModel; import org.apache.ibatis.annotations.Param; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; /** * Mapper for system-level permissions. * * @author Michael Jumper */ public interface SystemPermissionMapper extends PermissionMapper { /** * Retrieve the permission of the given type associated with the given * user, if it exists. If no such permission exists, null is returned. * * @param user * The user to retrieve permissions for. * * @param type * The type of permission to return. * * @return * The requested permission, or null if no such permission is granted * to the given user. */ SystemPermissionModel selectOne(@Param("user") UserModel user, @Param("type") SystemPermission.Type type); }././@LongLink0100644 0000000 0000000 00000000256 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/ObjectPermissionMapper.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000006311 12612226251 035255 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; import java.util.Collection; import org.apache.ibatis.annotations.Param; import org.glyptodon.guacamole.auth.jdbc.user.UserModel; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; /** * Mapper for object-related permissions. * * @author Michael Jumper */ public interface ObjectPermissionMapper extends PermissionMapper { /** * Retrieve the permission of the given type associated with the given * user and object, if it exists. If no such permission exists, null is * returned. * * @param user * The user to retrieve permissions for. * * @param type * The type of permission to return. * * @param identifier * The identifier of the object affected by the permission to return. * * @return * The requested permission, or null if no such permission is granted * to the given user for the given object. */ ObjectPermissionModel selectOne(@Param("user") UserModel user, @Param("type") ObjectPermission.Type type, @Param("identifier") String identifier); /** * Retrieves the subset of the given identifiers for which the given user * has at least one of the given permissions. * * @param user * The user to check permissions of. * * @param permissions * The permissions to check. An identifier will be included in the * resulting collection if at least one of these permissions is granted * for the associated object * * @param identifiers * The identifiers of the objects affected by the permissions being * checked. * * @return * A collection containing the subset of identifiers for which at least * one of the specified permissions is granted. */ Collection selectAccessibleIdentifiers(@Param("user") UserModel user, @Param("permissions") Collection permissions, @Param("identifiers") Collection identifiers); } ././@LongLink0100644 0000000 0000000 00000000255 12612226251 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/ObjectPermissionModel.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000004404 12612226251 035256 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; /** * Object representation of an object-related Guacamole permission, as * represented in the database. * * @author Michael Jumper */ public class ObjectPermissionModel extends PermissionModel { /** * The unique identifier of the object affected by this permission. */ private String objectIdentifier; /** * Creates a new, empty object permission. */ public ObjectPermissionModel() { } /** * Returns the unique identifier of the object affected by this permission. * * @return * The unique identifier of the object affected by this permission. */ public String getObjectIdentifier() { return objectIdentifier; } /** * Sets the unique identifier of the object affected by this permission. * * @param objectIdentifier * The unique identifier of the object affected by this permission. */ public void setObjectIdentifier(String objectIdentifier) { this.objectIdentifier = objectIdentifier; } } ././@LongLink0100644 0000000 0000000 00000000257 12612226251 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/ConnectionPermissionSet.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000003404 12612226251 035255 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; import com.google.inject.Inject; /** * A database implementation of ObjectPermissionSet which uses an injected * service to query and manipulate the connection permissions associated with * a particular user. * * @author Michael Jumper */ public class ConnectionPermissionSet extends ObjectPermissionSet { /** * Service for querying and manipulating connection permissions. */ @Inject private ConnectionPermissionService connectionPermissionService; @Override protected ObjectPermissionService getObjectPermissionService() { return connectionPermissionService; } } ././@LongLink0100644 0000000 0000000 00000000270 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/ConnectionGroupPermissionService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000005037 12612226251 035261 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; import com.google.inject.Inject; import com.google.inject.Provider; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.auth.jdbc.user.ModeledUser; /** * Service which provides convenience methods for creating, retrieving, and * deleting connection group permissions. This service will automatically * enforce the permissions of the current user. * * @author Michael Jumper */ public class ConnectionGroupPermissionService extends ModeledObjectPermissionService { /** * Mapper for connection group permissions. */ @Inject private ConnectionGroupPermissionMapper connectionGroupPermissionMapper; /** * Provider for connection group permission sets. */ @Inject private Provider connectionGroupPermissionSetProvider; @Override protected ObjectPermissionMapper getPermissionMapper() { return connectionGroupPermissionMapper; } @Override public ObjectPermissionSet getPermissionSet(AuthenticatedUser user, ModeledUser targetUser) throws GuacamoleException { // Create permission set for requested user ObjectPermissionSet permissionSet = connectionGroupPermissionSetProvider.get(); permissionSet.init(user, targetUser); return permissionSet; } } ././@LongLink0100644 0000000 0000000 00000000261 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/AbstractPermissionService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000007001 12612226251 035252 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.user.ModeledUser; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.Permission; import org.glyptodon.guacamole.net.auth.permission.PermissionSet; /** * Abstract PermissionService implementation which provides additional * convenience methods for enforcing the permission model. * * @author Michael Jumper * @param * The type of permission sets this service provides access to. * * @param * The type of permission this service provides access to. */ public abstract class AbstractPermissionService, PermissionType extends Permission> implements PermissionService { /** * Determines whether the given user can read the permissions currently * granted to the given target user. If the reading user and the target * user are not the same, then explicit READ or SYSTEM_ADMINISTER access is * required. * * @param user * The user attempting to read permissions. * * @param targetUser * The user whose permissions are being read. * * @return * true if permission is granted, false otherwise. * * @throws GuacamoleException * If an error occurs while checking permission status, or if * permission is denied to read the current user's permissions. */ protected boolean canReadPermissions(AuthenticatedUser user, ModeledUser targetUser) throws GuacamoleException { // A user can always read their own permissions if (user.getUser().getIdentifier().equals(targetUser.getIdentifier())) return true; // A system adminstrator can do anything if (user.getUser().isAdministrator()) return true; // Can read permissions on target user if explicit READ is granted ObjectPermissionSet userPermissionSet = user.getUser().getUserPermissions(); return userPermissionSet.hasPermission(ObjectPermission.Type.READ, targetUser.getIdentifier()); } } ././@LongLink0100644 0000000 0000000 00000000255 12612226251 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/UserPermissionService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000004656 12612226251 035267 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; import com.google.inject.Inject; import com.google.inject.Provider; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.auth.jdbc.user.ModeledUser; /** * Service which provides convenience methods for creating, retrieving, and * deleting user permissions. This service will automatically enforce the * permissions of the current user. * * @author Michael Jumper */ public class UserPermissionService extends ModeledObjectPermissionService { /** * Mapper for user permissions. */ @Inject private UserPermissionMapper userPermissionMapper; /** * Provider for user permission sets. */ @Inject private Provider userPermissionSetProvider; @Override protected ObjectPermissionMapper getPermissionMapper() { return userPermissionMapper; } @Override public ObjectPermissionSet getPermissionSet(AuthenticatedUser user, ModeledUser targetUser) throws GuacamoleException { // Create permission set for requested user ObjectPermissionSet permissionSet = userPermissionSetProvider.get(); permissionSet.init(user, targetUser); return permissionSet; } } ././@LongLink0100644 0000000 0000000 00000000253 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/ObjectPermissionSet.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000011634 12612226251 035261 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; import org.glyptodon.guacamole.auth.jdbc.user.ModeledUser; import java.util.Collection; import java.util.Collections; import java.util.Set; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.auth.jdbc.base.RestrictedObject; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; /** * A database implementation of ObjectPermissionSet which uses an injected * service to query and manipulate the object-level permissions associated with * a particular user. * * @author Michael Jumper */ public abstract class ObjectPermissionSet extends RestrictedObject implements org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet { /** * The user associated with this permission set. Each of the permissions in * this permission set is granted to this user. */ private ModeledUser user; /** * Creates a new ObjectPermissionSet. The resulting permission set * must still be initialized by a call to init(), or the information * necessary to read and modify this set will be missing. */ public ObjectPermissionSet() { } /** * Initializes this permission set with the current user and the user * to whom the permissions in this set are granted. * * @param currentUser * The user who queried this permission set, and whose permissions * dictate the access level of all operations performed on this set. * * @param user * The user to whom the permissions in this set are granted. */ public void init(AuthenticatedUser currentUser, ModeledUser user) { super.init(currentUser); this.user = user; } /** * Returns an ObjectPermissionService implementation for manipulating the * type of permissions contained within this permission set. * * @return * An object permission service for manipulating the type of * permissions contained within this permission set. */ protected abstract ObjectPermissionService getObjectPermissionService(); @Override public Set getPermissions() throws GuacamoleException { return getObjectPermissionService().retrievePermissions(getCurrentUser(), user); } @Override public boolean hasPermission(ObjectPermission.Type permission, String identifier) throws GuacamoleException { return getObjectPermissionService().retrievePermission(getCurrentUser(), user, permission, identifier) != null; } @Override public void addPermission(ObjectPermission.Type permission, String identifier) throws GuacamoleException { addPermissions(Collections.singleton(new ObjectPermission(permission, identifier))); } @Override public void removePermission(ObjectPermission.Type permission, String identifier) throws GuacamoleException { removePermissions(Collections.singleton(new ObjectPermission(permission, identifier))); } @Override public Collection getAccessibleObjects(Collection permissions, Collection identifiers) throws GuacamoleException { return getObjectPermissionService().retrieveAccessibleIdentifiers(getCurrentUser(), user, permissions, identifiers); } @Override public void addPermissions(Set permissions) throws GuacamoleException { getObjectPermissionService().createPermissions(getCurrentUser(), user, permissions); } @Override public void removePermissions(Set permissions) throws GuacamoleException { getObjectPermissionService().deletePermissions(getCurrentUser(), user, permissions); } } ././@LongLink0100644 0000000 0000000 00000000267 12612226251 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000002472 12612226251 035261 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; /** * Mapper for connection group permissions. * * @author Michael Jumper */ public interface ConnectionGroupPermissionMapper extends ObjectPermissionMapper {} ././@LongLink0100644 0000000 0000000 00000000263 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/ConnectionPermissionService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000004752 12612226251 035264 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; import com.google.inject.Inject; import com.google.inject.Provider; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.auth.jdbc.user.ModeledUser; /** * Service which provides convenience methods for creating, retrieving, and * deleting connection permissions. This service will automatically enforce the * permissions of the current user. * * @author Michael Jumper */ public class ConnectionPermissionService extends ModeledObjectPermissionService { /** * Mapper for connection permissions. */ @Inject private ConnectionPermissionMapper connectionPermissionMapper; /** * Provider for connection permission sets. */ @Inject private Provider connectionPermissionSetProvider; @Override protected ObjectPermissionMapper getPermissionMapper() { return connectionPermissionMapper; } @Override public ObjectPermissionSet getPermissionSet(AuthenticatedUser user, ModeledUser targetUser) throws GuacamoleException { // Create permission set for requested user ObjectPermissionSet permissionSet = connectionPermissionSetProvider.get(); permissionSet.init(user, targetUser); return permissionSet; } } ././@LongLink0100644 0000000 0000000 00000000266 12612226251 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/ModeledObjectPermissionService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000020401 12612226251 035251 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.user.ModeledUser; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; /** * Service which provides convenience methods for creating, retrieving, and * deleting object permissions within a backend database model. This service * will automatically enforce the permissions of the current user. * * @author Michael Jumper */ public abstract class ModeledObjectPermissionService extends ModeledPermissionService implements ObjectPermissionService { @Override protected abstract ObjectPermissionMapper getPermissionMapper(); @Override protected ObjectPermission getPermissionInstance(ObjectPermissionModel model) { return new ObjectPermission(model.getType(), model.getObjectIdentifier()); } @Override protected ObjectPermissionModel getModelInstance(ModeledUser targetUser, ObjectPermission permission) { ObjectPermissionModel model = new ObjectPermissionModel(); // Populate model object with data from user and permission model.setUserID(targetUser.getModel().getObjectID()); model.setUsername(targetUser.getModel().getIdentifier()); model.setType(permission.getType()); model.setObjectIdentifier(permission.getObjectIdentifier()); return model; } /** * Determines whether the current user has permission to update the given * target user, adding or removing the given permissions. Such permission * depends on whether the current user is a system administrator, whether * they have explicit UPDATE permission on the target user, and whether * they have explicit ADMINISTER permission on all affected objects. * * @param user * The user who is changing permissions. * * @param targetUser * The user whose permissions are being changed. * * @param permissions * The permissions that are being added or removed from the target * user. * * @return * true if the user has permission to change the target users * permissions as specified, false otherwise. * * @throws GuacamoleException * If an error occurs while checking permission status, or if * permission is denied to read the current user's permissions. */ protected boolean canAlterPermissions(AuthenticatedUser user, ModeledUser targetUser, Collection permissions) throws GuacamoleException { // A system adminstrator can do anything if (user.getUser().isAdministrator()) return true; // Verify user has update permission on the target user ObjectPermissionSet userPermissionSet = user.getUser().getUserPermissions(); if (!userPermissionSet.hasPermission(ObjectPermission.Type.UPDATE, targetUser.getIdentifier())) return false; // Produce collection of affected identifiers Collection affectedIdentifiers = new HashSet(permissions.size()); for (ObjectPermission permission : permissions) affectedIdentifiers.add(permission.getObjectIdentifier()); // Determine subset of affected identifiers that we have admin access to ObjectPermissionSet affectedPermissionSet = getPermissionSet(user, user.getUser()); Collection allowedSubset = affectedPermissionSet.getAccessibleObjects( Collections.singleton(ObjectPermission.Type.ADMINISTER), affectedIdentifiers ); // The permissions can be altered if and only if the set of objects we // are allowed to administer is equal to the set of objects we will be // affecting. return affectedIdentifiers.size() == allowedSubset.size(); } @Override public void createPermissions(AuthenticatedUser user, ModeledUser targetUser, Collection permissions) throws GuacamoleException { // Create permissions only if user has permission to do so if (canAlterPermissions(user, targetUser, permissions)) { Collection models = getModelInstances(targetUser, permissions); getPermissionMapper().insert(models); return; } // User lacks permission to create object permissions throw new GuacamoleSecurityException("Permission denied."); } @Override public void deletePermissions(AuthenticatedUser user, ModeledUser targetUser, Collection permissions) throws GuacamoleException { // Delete permissions only if user has permission to do so if (canAlterPermissions(user, targetUser, permissions)) { Collection models = getModelInstances(targetUser, permissions); getPermissionMapper().delete(models); return; } // User lacks permission to delete object permissions throw new GuacamoleSecurityException("Permission denied."); } @Override public ObjectPermission retrievePermission(AuthenticatedUser user, ModeledUser targetUser, ObjectPermission.Type type, String identifier) throws GuacamoleException { // Retrieve permissions only if allowed if (canReadPermissions(user, targetUser)) { // Read permission from database, return null if not found ObjectPermissionModel model = getPermissionMapper().selectOne(targetUser.getModel(), type, identifier); if (model == null) return null; return getPermissionInstance(model); } // User cannot read this user's permissions throw new GuacamoleSecurityException("Permission denied."); } @Override public Collection retrieveAccessibleIdentifiers(AuthenticatedUser user, ModeledUser targetUser, Collection permissions, Collection identifiers) throws GuacamoleException { // Nothing is always accessible if (identifiers.isEmpty()) return identifiers; // Retrieve permissions only if allowed if (canReadPermissions(user, targetUser)) { // If user is an admin, everything is accessible if (user.getUser().isAdministrator()) return identifiers; // Otherwise, return explicitly-retrievable identifiers return getPermissionMapper().selectAccessibleIdentifiers(targetUser.getModel(), permissions, identifiers); } // User cannot read this user's permissions throw new GuacamoleSecurityException("Permission denied."); } } ././@LongLink0100644 0000000 0000000 00000000251 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/UserPermissionSet.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000003340 12612226251 035254 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; import com.google.inject.Inject; /** * A database implementation of ObjectPermissionSet which uses an injected * service to query and manipulate the user permissions associated with a * particular user. * * @author Michael Jumper */ public class UserPermissionSet extends ObjectPermissionSet { /** * Service for querying and manipulating user permissions. */ @Inject private UserPermissionService userPermissionService; @Override protected ObjectPermissionService getObjectPermissionService() { return userPermissionService; } } ././@LongLink0100644 0000000 0000000 00000000260 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/ModeledPermissionService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000013447 12612226251 035265 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Set; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.user.ModeledUser; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.net.auth.permission.Permission; import org.glyptodon.guacamole.net.auth.permission.PermissionSet; /** * Service which provides convenience methods for creating, retrieving, and * deleting permissions within a backend database model, and for obtaining the * permission sets that contain these permissions. This service will * automatically enforce the permissions of the current user. * * @author Michael Jumper * @param * The type of permission sets this service provides access to. * * @param * The type of permission this service provides access to. * * @param * The underlying model object used to represent PermissionType in the * database. */ public abstract class ModeledPermissionService, PermissionType extends Permission, ModelType> extends AbstractPermissionService { /** * Returns an instance of a mapper for the type of permission used by this * service. * * @return * A mapper which provides access to the model objects associated with * the permissions used by this service. */ protected abstract PermissionMapper getPermissionMapper(); /** * Returns an instance of a permission which is based on the given model * object. * * @param model * The model object to use to produce the returned permission. * * @return * A permission which is based on the given model object. */ protected abstract PermissionType getPermissionInstance(ModelType model); /** * Returns a collection of permissions which are based on the models in * the given collection. * * @param models * The model objects to use to produce the permissions within the * returned set. * * @return * A set of permissions which are based on the models in the given * collection. */ protected Set getPermissionInstances(Collection models) { // Create new collection of permissions by manually converting each model Set permissions = new HashSet(models.size()); for (ModelType model : models) permissions.add(getPermissionInstance(model)); return permissions; } /** * Returns an instance of a model object which is based on the given * permission and target user. * * @param targetUser * The user to whom this permission is granted. * * @param permission * The permission to use to produce the returned model object. * * @return * A model object which is based on the given permission and target * user. */ protected abstract ModelType getModelInstance(ModeledUser targetUser, PermissionType permission); /** * Returns a collection of model objects which are based on the given * permissions and target user. * * @param targetUser * The user to whom this permission is granted. * * @param permissions * The permissions to use to produce the returned model objects. * * @return * A collection of model objects which are based on the given * permissions and target user. */ protected Collection getModelInstances(ModeledUser targetUser, Collection permissions) { // Create new collection of models by manually converting each permission Collection models = new ArrayList(permissions.size()); for (PermissionType permission : permissions) models.add(getModelInstance(targetUser, permission)); return models; } @Override public Set retrievePermissions(AuthenticatedUser user, ModeledUser targetUser) throws GuacamoleException { // Retrieve permissions only if allowed if (canReadPermissions(user, targetUser)) return getPermissionInstances(getPermissionMapper().select(targetUser.getModel())); // User cannot read this user's permissions throw new GuacamoleSecurityException("Permission denied."); } } ././@LongLink0100644 0000000 0000000 00000000253 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/SystemPermissionSet.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000010374 12612226251 035261 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; import org.glyptodon.guacamole.auth.jdbc.user.ModeledUser; import com.google.inject.Inject; import java.util.Collections; import java.util.Set; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.auth.jdbc.base.RestrictedObject; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; /** * A database implementation of SystemPermissionSet which uses an injected * service to query and manipulate the system permissions associated with a * particular user. * * @author Michael Jumper */ public class SystemPermissionSet extends RestrictedObject implements org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet { /** * The user associated with this permission set. Each of the permissions in * this permission set is granted to this user. */ private ModeledUser user; /** * Service for reading and manipulating system permissions. */ @Inject private SystemPermissionService systemPermissionService; /** * Creates a new SystemPermissionSet. The resulting permission set * must still be initialized by a call to init(), or the information * necessary to read and modify this set will be missing. */ public SystemPermissionSet() { } /** * Initializes this permission set with the current user and the user * to whom the permissions in this set are granted. * * @param currentUser * The user who queried this permission set, and whose permissions * dictate the access level of all operations performed on this set. * * @param user * The user to whom the permissions in this set are granted. */ public void init(AuthenticatedUser currentUser, ModeledUser user) { super.init(currentUser); this.user = user; } @Override public Set getPermissions() throws GuacamoleException { return systemPermissionService.retrievePermissions(getCurrentUser(), user); } @Override public boolean hasPermission(SystemPermission.Type permission) throws GuacamoleException { return systemPermissionService.retrievePermission(getCurrentUser(), user, permission) != null; } @Override public void addPermission(SystemPermission.Type permission) throws GuacamoleException { addPermissions(Collections.singleton(new SystemPermission(permission))); } @Override public void removePermission(SystemPermission.Type permission) throws GuacamoleException { removePermissions(Collections.singleton(new SystemPermission(permission))); } @Override public void addPermissions(Set permissions) throws GuacamoleException { systemPermissionService.createPermissions(getCurrentUser(), user, permissions); } @Override public void removePermissions(Set permissions) throws GuacamoleException { systemPermissionService.deletePermissions(getCurrentUser(), user, permissions); } } ././@LongLink0100644 0000000 0000000 00000000254 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/UserPermissionMapper.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000002443 12612226251 035257 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; /** * Mapper for user permissions. * * @author Michael Jumper */ public interface UserPermissionMapper extends ObjectPermissionMapper {} ././@LongLink0100644 0000000 0000000 00000000244 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/package-info.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000002331 12612226251 035253 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes related to object- and system-level permissions. */ package org.glyptodon.guacamole.auth.jdbc.permission; ././@LongLink0100644 0000000 0000000 00000000255 12612226251 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/SystemPermissionModel.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000003050 12612226251 035252 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; /** * Object representation of an system-level Guacamole permission, as * represented in the database. * * @author Michael Jumper */ public class SystemPermissionModel extends PermissionModel { /** * Creates a new, empty System permission. */ public SystemPermissionModel() { } } ././@LongLink0100644 0000000 0000000 00000000264 12612226251 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/ConnectionGroupPermissionSet.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000003444 12612226251 035261 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; import com.google.inject.Inject; /** * A database implementation of ObjectPermissionSet which uses an injected * service to query and manipulate the connection group permissions associated * with a particular user. * * @author Michael Jumper */ public class ConnectionGroupPermissionSet extends ObjectPermissionSet { /** * Service for querying and manipulating connection group permissions. */ @Inject private ConnectionGroupPermissionService connectionGroupPermissionService; @Override protected ObjectPermissionService getObjectPermissionService() { return connectionGroupPermissionService; } } ././@LongLink0100644 0000000 0000000 00000000262 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000002457 12612226251 035264 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; /** * Mapper for connection permissions. * * @author Michael Jumper */ public interface ConnectionPermissionMapper extends ObjectPermissionMapper {} ././@LongLink0100644 0000000 0000000 00000000257 12612226251 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/SystemPermissionService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000014262 12612226251 035261 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; import com.google.inject.Inject; import com.google.inject.Provider; import java.util.Collection; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.user.ModeledUser; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.GuacamoleUnsupportedException; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; /** * Service which provides convenience methods for creating, retrieving, and * deleting system permissions. This service will automatically enforce * the permissions of the current user. * * @author Michael Jumper */ public class SystemPermissionService extends ModeledPermissionService { /** * Mapper for system-level permissions. */ @Inject private SystemPermissionMapper systemPermissionMapper; /** * Provider for creating system permission sets. */ @Inject private Provider systemPermissionSetProvider; @Override protected SystemPermissionMapper getPermissionMapper() { return systemPermissionMapper; } @Override protected SystemPermission getPermissionInstance(SystemPermissionModel model) { return new SystemPermission(model.getType()); } @Override protected SystemPermissionModel getModelInstance(final ModeledUser targetUser, final SystemPermission permission) { SystemPermissionModel model = new SystemPermissionModel(); // Populate model object with data from user and permission model.setUserID(targetUser.getModel().getObjectID()); model.setUsername(targetUser.getModel().getIdentifier()); model.setType(permission.getType()); return model; } @Override public SystemPermissionSet getPermissionSet(AuthenticatedUser user, ModeledUser targetUser) throws GuacamoleException { // Create permission set for requested user SystemPermissionSet permissionSet = systemPermissionSetProvider.get(); permissionSet.init(user, targetUser); return permissionSet; } @Override public void createPermissions(AuthenticatedUser user, ModeledUser targetUser, Collection permissions) throws GuacamoleException { // Only an admin can create system permissions if (user.getUser().isAdministrator()) { Collection models = getModelInstances(targetUser, permissions); systemPermissionMapper.insert(models); return; } // User lacks permission to create system permissions throw new GuacamoleSecurityException("Permission denied."); } @Override public void deletePermissions(AuthenticatedUser user, ModeledUser targetUser, Collection permissions) throws GuacamoleException { // Only an admin can delete system permissions if (user.getUser().isAdministrator()) { // Do not allow users to remove their own admin powers if (user.getUser().getIdentifier().equals(targetUser.getIdentifier())) throw new GuacamoleUnsupportedException("Removing your own administrative permissions is not allowed."); Collection models = getModelInstances(targetUser, permissions); systemPermissionMapper.delete(models); return; } // User lacks permission to delete system permissions throw new GuacamoleSecurityException("Permission denied."); } /** * Retrieves the permission of the given type associated with the given * user, if it exists. If no such permission exists, null is returned. * * @param user * The user retrieving the permission. * * @param targetUser * The user associated with the permission to be retrieved. * * @param type * The type of permission to retrieve. * * @return * The permission of the given type associated with the given user, or * null if no such permission exists. * * @throws GuacamoleException * If an error occurs while retrieving the requested permission. */ public SystemPermission retrievePermission(AuthenticatedUser user, ModeledUser targetUser, SystemPermission.Type type) throws GuacamoleException { // Retrieve permissions only if allowed if (canReadPermissions(user, targetUser)) { // Read permission from database, return null if not found SystemPermissionModel model = getPermissionMapper().selectOne(targetUser.getModel(), type); if (model == null) return null; return getPermissionInstance(model); } // User cannot read this user's permissions throw new GuacamoleSecurityException("Permission denied."); } } ././@LongLink0100644 0000000 0000000 00000000257 12612226251 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/ObjectPermissionService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000007616 12612226251 035266 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; import java.util.Collection; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.user.ModeledUser; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; /** * Service which provides convenience methods for creating, retrieving, and * deleting object permissions. This service will automatically enforce the * permissions of the current user. * * @author Michael Jumper */ public interface ObjectPermissionService extends PermissionService { /** * Retrieves the permission of the given type associated with the given * user and object, if it exists. If no such permission exists, null is * * @param user * The user retrieving the permission. * * @param targetUser * The user associated with the permission to be retrieved. * * @param type * The type of permission to retrieve. * * @param identifier * The identifier of the object affected by the permission to return. * * @return * The permission of the given type associated with the given user and * object, or null if no such permission exists. * * @throws GuacamoleException * If an error occurs while retrieving the requested permission. */ ObjectPermission retrievePermission(AuthenticatedUser user, ModeledUser targetUser, ObjectPermission.Type type, String identifier) throws GuacamoleException; /** * Retrieves the subset of the given identifiers for which the given user * has at least one of the given permissions. * * @param user * The user checking the permissions. * * @param targetUser * The user to check permissions of. * * @param permissions * The permissions to check. An identifier will be included in the * resulting collection if at least one of these permissions is granted * for the associated object * * @param identifiers * The identifiers of the objects affected by the permissions being * checked. * * @return * A collection containing the subset of identifiers for which at least * one of the specified permissions is granted. * * @throws GuacamoleException * If an error occurs while retrieving permissions. */ Collection retrieveAccessibleIdentifiers(AuthenticatedUser user, ModeledUser targetUser, Collection permissions, Collection identifiers) throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000247 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/PermissionModel.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000006373 12612226251 035265 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; /** * Generic base permission model which grants a permission of a particular type * to a specific user. * * @author Michael Jumper * @param * The type of permissions allowed within this model. */ public abstract class PermissionModel { /** * The database ID of the user to whom this permission is granted. */ private Integer userID; /** * The username of the user to whom this permission is granted. */ private String username; /** * The type of action granted by this permission. */ private PermissionType type; /** * Returns the database ID of the user to whom this permission is granted. * * @return * The database ID of the user to whom this permission is granted. */ public Integer getUserID() { return userID; } /** * Sets the database ID of the user to whom this permission is granted. * * @param userID * The database ID of the user to whom this permission is granted. */ public void setUserID(Integer userID) { this.userID = userID; } /** * Returns the username of the user to whom this permission is granted. * * @return * The username of the user to whom this permission is granted. */ public String getUsername() { return username; } /** * Sets the username of the user to whom this permission is granted. * * @param username * The username of the user to whom this permission is granted. */ public void setUsername(String username) { this.username = username; } /** * Returns the type of action granted by this permission. * * @return * The type of action granted by this permission. */ public PermissionType getType() { return type; } /** * Sets the type of action granted by this permission. * * @param type * The type of action granted by this permission. */ public void setType(PermissionType type) { this.type = type; } } ././@LongLink0100644 0000000 0000000 00000000251 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/permission/PermissionService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000012300 12612226251 035250 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.permission; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Set; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.user.ModeledUser; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.Permission; import org.glyptodon.guacamole.net.auth.permission.PermissionSet; /** * Service which provides convenience methods for creating, retrieving, and * deleting permissions, and for obtaining the permission sets that contain * these permissions. This service will automatically enforce the permissions * of the current user. * * @author Michael Jumper * @param * The type of permission sets this service provides access to. * * @param * The type of permission this service provides access to. */ public interface PermissionService, PermissionType extends Permission> { /** * Returns a permission set that can be used to retrieve and manipulate the * permissions of the given user. * * @param user * The user who will be retrieving or manipulating permissions through * the returned permission set. * * @param targetUser * The user to whom the permissions in the returned permission set are * granted. * * @return * A permission set that contains all permissions associated with the * given user, and can be used to manipulate that user's permissions. * * @throws GuacamoleException * If an error occurs while retrieving the permissions of the given * user, or if permission to retrieve the permissions of the given * user is denied. */ PermissionSetType getPermissionSet(AuthenticatedUser user, ModeledUser targetUser) throws GuacamoleException; /** * Retrieves all permissions associated with the given user. * * @param user * The user retrieving the permissions. * * @param targetUser * The user associated with the permissions to be retrieved. * * @return * The permissions associated with the given user. * * @throws GuacamoleException * If an error occurs while retrieving the requested permissions. */ Set retrievePermissions(AuthenticatedUser user, ModeledUser targetUser) throws GuacamoleException; /** * Creates the given permissions within the database. If any permissions * already exist, they will be ignored. * * @param user * The user creating the permissions. * * @param targetUser * The user associated with the permissions to be created. * * @param permissions * The permissions to create. * * @throws GuacamoleException * If the user lacks permission to create the permissions, or an error * occurs while creating the permissions. */ void createPermissions(AuthenticatedUser user, ModeledUser targetUser, Collection permissions) throws GuacamoleException; /** * Deletes the given permissions. If any permissions do not exist, they * will be ignored. * * @param user * The user deleting the permissions. * * @param targetUser * The user associated with the permissions to be deleted. * * @param permissions * The permissions to delete. * * @throws GuacamoleException * If the user lacks permission to delete the permissions, or an error * occurs while deleting the permissions. */ void deletePermissions(AuthenticatedUser user, ModeledUser targetUser, Collection permissions) throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000231 12612226251 011626 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/package-info.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000002705 12612226251 035260 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * The base JDBC authentication provider. This authentication provider serves * as a basis for other JDBC authentication provider implementations which are * driven by relatively-common schemas. The only difference between such * implementations are maintained within database-specific MyBatis mappings. */ package org.glyptodon.guacamole.auth.jdbc; ././@LongLink0100644 0000000 0000000 00000000260 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connectiongroup/RootConnectionGroup.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000011413 12612226251 035254 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connectiongroup; import com.google.inject.Inject; import java.util.Collections; import java.util.Map; import java.util.Set; import org.glyptodon.guacamole.auth.jdbc.connection.ConnectionService; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.auth.jdbc.base.RestrictedObject; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; /** * The root connection group, here represented as its own dedicated object as * the database does not contain an actual root group. * * @author Michael Jumper */ public class RootConnectionGroup extends RestrictedObject implements ConnectionGroup { /** * The identifier used to represent the root connection group. There is no * corresponding entry in the database, thus a reserved identifier that * cannot collide with database-generated identifiers is needed. */ public static final String IDENTIFIER = "ROOT"; /** * The human-readable name of this connection group. The name of the root * group is not normally visible, and may even be replaced by the web * interface for the sake of translation. */ public static final String NAME = "ROOT"; /** * Service for managing connection objects. */ @Inject private ConnectionService connectionService; /** * Service for managing connection group objects. */ @Inject private ConnectionGroupService connectionGroupService; /** * Creates a new, empty RootConnectionGroup. */ public RootConnectionGroup() { } @Override public String getName() { return NAME; } @Override public void setName(String name) { throw new UnsupportedOperationException("The root connection group cannot be modified."); } @Override public String getParentIdentifier() { return null; } @Override public void setParentIdentifier(String parentIdentifier) { throw new UnsupportedOperationException("The root connection group cannot be modified."); } @Override public Type getType() { return ConnectionGroup.Type.ORGANIZATIONAL; } @Override public void setType(Type type) { throw new UnsupportedOperationException("The root connection group cannot be modified."); } @Override public Set getConnectionIdentifiers() throws GuacamoleException { return connectionService.getIdentifiersWithin(getCurrentUser(), null); } @Override public Set getConnectionGroupIdentifiers() throws GuacamoleException { return connectionGroupService.getIdentifiersWithin(getCurrentUser(), null); } @Override public String getIdentifier() { return IDENTIFIER; } @Override public void setIdentifier(String identifier) { throw new UnsupportedOperationException("The root connection group cannot be modified."); } @Override public GuacamoleTunnel connect(GuacamoleClientInformation info) throws GuacamoleException { throw new GuacamoleSecurityException("Permission denied."); } @Override public int getActiveConnections() { return 0; } @Override public Map getAttributes() { return Collections.emptyMap(); } @Override public void setAttributes(Map attributes) { throw new UnsupportedOperationException("The root connection group cannot be modified."); } } ././@LongLink0100644 0000000 0000000 00000000261 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connectiongroup/ConnectionGroupModel.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000013376 12612226251 035266 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connectiongroup; import org.glyptodon.guacamole.auth.jdbc.base.GroupedObjectModel; import org.glyptodon.guacamole.net.auth.ConnectionGroup; /** * Object representation of a Guacamole connection group, as represented in the * database. * * @author Michael Jumper */ public class ConnectionGroupModel extends GroupedObjectModel { /** * The human-readable name associated with this connection group. */ private String name; /** * The type of this connection group, such as organizational or balancing. */ private ConnectionGroup.Type type; /** * The maximum number of connections that can be established to this * connection group concurrently, zero if no restriction applies, or * null if the default restrictions should be applied. */ private Integer maxConnections; /** * The maximum number of connections that can be established to this * connection group concurrently by any one user, zero if no restriction * applies, or null if the default restrictions should be applied. */ private Integer maxConnectionsPerUser; /** * Creates a new, empty connection group. */ public ConnectionGroupModel() { } /** * Returns the name associated with this connection group. * * @return * The name associated with this connection group. */ public String getName() { return name; } /** * Sets the name associated with this connection group. * * @param name * The name to associate with this connection group. */ public void setName(String name) { this.name = name; } /** * Returns the type of this connection group, such as organizational or * balancing. * * @return * The type of this connection group. */ public ConnectionGroup.Type getType() { return type; } /** * Sets the type of this connection group, such as organizational or * balancing. * * @param type * The type of this connection group. */ public void setType(ConnectionGroup.Type type) { this.type = type; } /** * Returns the maximum number of connections that can be established to * this connection group concurrently. * * @return * The maximum number of connections that can be established to this * connection group concurrently, zero if no restriction applies, or * null if the default restrictions should be applied. */ public Integer getMaxConnections() { return maxConnections; } /** * Sets the maximum number of connections that can be established to this * connection group concurrently. * * @param maxConnections * The maximum number of connections that can be established to this * connection group concurrently, zero if no restriction applies, or * null if the default restrictions should be applied. */ public void setMaxConnections(Integer maxConnections) { this.maxConnections = maxConnections; } /** * Returns the maximum number of connections that can be established to * this connection group concurrently by any one user. * * @return * The maximum number of connections that can be established to this * connection group concurrently by any one user, zero if no * restriction applies, or null if the default restrictions should be * applied. */ public Integer getMaxConnectionsPerUser() { return maxConnectionsPerUser; } /** * Sets the maximum number of connections that can be established to this * connection group concurrently by any one user. * * @param maxConnectionsPerUser * The maximum number of connections that can be established to this * connection group concurrently by any one user, zero if no * restriction applies, or null if the default restrictions should be * applied. */ public void setMaxConnectionsPerUser(Integer maxConnectionsPerUser) { this.maxConnectionsPerUser = maxConnectionsPerUser; } @Override public String getIdentifier() { // If no associated ID, then no associated identifier Integer id = getObjectID(); if (id == null) return null; // Otherwise, the identifier is the ID as a string return id.toString(); } @Override public void setIdentifier(String identifier) { throw new UnsupportedOperationException("Connection group identifiers are derived from IDs. They cannot be set."); } } ././@LongLink0100644 0000000 0000000 00000000265 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connectiongroup/ConnectionGroupDirectory.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000006426 12612226251 035264 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connectiongroup; import com.google.inject.Inject; import java.util.Collection; import java.util.Collections; import java.util.Set; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.auth.jdbc.base.RestrictedObject; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.Directory; import org.mybatis.guice.transactional.Transactional; /** * Implementation of the ConnectionGroup Directory which is driven by an * underlying, arbitrary database. * * @author James Muehlner * @author Michael Jumper */ public class ConnectionGroupDirectory extends RestrictedObject implements Directory { /** * Service for managing connection group objects. */ @Inject private ConnectionGroupService connectionGroupService; @Override public ConnectionGroup get(String identifier) throws GuacamoleException { return connectionGroupService.retrieveObject(getCurrentUser(), identifier); } @Override @Transactional public Collection getAll(Collection identifiers) throws GuacamoleException { Collection objects = connectionGroupService.retrieveObjects(getCurrentUser(), identifiers); return Collections.unmodifiableCollection(objects); } @Override @Transactional public Set getIdentifiers() throws GuacamoleException { return connectionGroupService.getIdentifiers(getCurrentUser()); } @Override @Transactional public void add(ConnectionGroup object) throws GuacamoleException { connectionGroupService.createObject(getCurrentUser(), object); } @Override @Transactional public void update(ConnectionGroup object) throws GuacamoleException { ModeledConnectionGroup connectionGroup = (ModeledConnectionGroup) object; connectionGroupService.updateObject(getCurrentUser(), connectionGroup); } @Override @Transactional public void remove(String identifier) throws GuacamoleException { connectionGroupService.deleteObject(getCurrentUser(), identifier); } } ././@LongLink0100644 0000000 0000000 00000000263 12645510255 011641 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connectiongroup/ModeledConnectionGroup.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000020134 12645510255 035262 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connectiongroup; import com.google.inject.Inject; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; import org.glyptodon.guacamole.auth.jdbc.connection.ConnectionService; import org.glyptodon.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.auth.jdbc.JDBCEnvironment; import org.glyptodon.guacamole.auth.jdbc.base.ModeledGroupedDirectoryObject; import org.glyptodon.guacamole.form.Field; import org.glyptodon.guacamole.form.Form; import org.glyptodon.guacamole.form.NumericField; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * An implementation of the ConnectionGroup object which is backed by a * database model. * * @author James Muehlner * @author Michael Jumper */ public class ModeledConnectionGroup extends ModeledGroupedDirectoryObject implements ConnectionGroup { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(ModeledConnectionGroup.class); /** * The name of the attribute which controls the maximum number of * concurrent connections. */ public static final String MAX_CONNECTIONS_NAME = "max-connections"; /** * The name of the attribute which controls the maximum number of * concurrent connections per user. */ public static final String MAX_CONNECTIONS_PER_USER_NAME = "max-connections-per-user"; /** * All attributes related to restricting user accounts, within a logical * form. */ public static final Form CONCURRENCY_LIMITS = new Form("concurrency", Arrays.asList( new NumericField(MAX_CONNECTIONS_NAME), new NumericField(MAX_CONNECTIONS_PER_USER_NAME) )); /** * All possible attributes of connection group objects organized as * individual, logical forms. */ public static final Collection ATTRIBUTES = Collections.unmodifiableCollection(Arrays.asList( CONCURRENCY_LIMITS )); /** * The environment of the Guacamole server. */ @Inject private JDBCEnvironment environment; /** * Service for managing connections. */ @Inject private ConnectionService connectionService; /** * Service for managing connection groups. */ @Inject private ConnectionGroupService connectionGroupService; /** * Service for creating and tracking tunnels. */ @Inject private GuacamoleTunnelService tunnelService; /** * Creates a new, empty ModeledConnectionGroup. */ public ModeledConnectionGroup() { } @Override public String getName() { return getModel().getName(); } @Override public void setName(String name) { getModel().setName(name); } @Override public GuacamoleTunnel connect(GuacamoleClientInformation info) throws GuacamoleException { return connectionGroupService.connect(getCurrentUser(), this, info); } @Override public int getActiveConnections() { return tunnelService.getActiveConnections(this).size(); } @Override public void setType(Type type) { getModel().setType(type); } @Override public Type getType() { return getModel().getType(); } @Override public Set getConnectionIdentifiers() throws GuacamoleException { return connectionService.getIdentifiersWithin(getCurrentUser(), getIdentifier()); } @Override public Set getConnectionGroupIdentifiers() throws GuacamoleException { return connectionGroupService.getIdentifiersWithin(getCurrentUser(), getIdentifier()); } @Override public Map getAttributes() { Map attributes = new HashMap(); // Set connection limit attribute attributes.put(MAX_CONNECTIONS_NAME, NumericField.format(getModel().getMaxConnections())); // Set per-user connection limit attribute attributes.put(MAX_CONNECTIONS_PER_USER_NAME, NumericField.format(getModel().getMaxConnectionsPerUser())); return attributes; } @Override public void setAttributes(Map attributes) { // Translate connection limit attribute try { getModel().setMaxConnections(NumericField.parse(attributes.get(MAX_CONNECTIONS_NAME))); } catch (NumberFormatException e) { logger.warn("Not setting maximum connections: {}", e.getMessage()); logger.debug("Unable to parse numeric attribute.", e); } // Translate per-user connection limit attribute try { getModel().setMaxConnectionsPerUser(NumericField.parse(attributes.get(MAX_CONNECTIONS_PER_USER_NAME))); } catch (NumberFormatException e) { logger.warn("Not setting maximum connections per user: {}", e.getMessage()); logger.debug("Unable to parse numeric attribute.", e); } } /** * Returns the maximum number of connections that should be allowed to this * connection group overall. If no limit applies, zero is returned. * * @return * The maximum number of connections that should be allowed to this * connection group overall, or zero if no limit applies. * * @throws GuacamoleException * If an error occurs while parsing the concurrency limit properties * specified within guacamole.properties. */ public int getMaxConnections() throws GuacamoleException { // Pull default from environment if connection limit is unset Integer value = getModel().getMaxConnections(); if (value == null) return environment.getDefaultMaxGroupConnections(); // Otherwise use defined value return value; } /** * Returns the maximum number of connections that should be allowed to this * connection group for any individual user. If no limit applies, zero is * returned. * * @return * The maximum number of connections that should be allowed to this * connection group for any individual user, or zero if no limit * applies. * * @throws GuacamoleException * If an error occurs while parsing the concurrency limit properties * specified within guacamole.properties. */ public int getMaxConnectionsPerUser() throws GuacamoleException { // Pull default from environment if per-user connection limit is unset Integer value = getModel().getMaxConnectionsPerUser(); if (value == null) return environment.getDefaultMaxGroupConnectionsPerUser(); // Otherwise use defined value return value; } } ././@LongLink0100644 0000000 0000000 00000000262 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000007364 12612226251 035266 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connectiongroup; import java.util.Set; import org.glyptodon.guacamole.auth.jdbc.base.ModeledDirectoryObjectMapper; import org.glyptodon.guacamole.auth.jdbc.user.UserModel; import org.apache.ibatis.annotations.Param; /** * Mapper for connection group objects. * * @author Michael Jumper */ public interface ConnectionGroupMapper extends ModeledDirectoryObjectMapper { /** * Selects the identifiers of all connection groups within the given parent * connection group, regardless of whether they are readable by any * particular user. This should only be called on behalf of a system * administrator. If identifiers are needed by a non-administrative user * who must have explicit read rights, use * selectReadableIdentifiersWithin() instead. * * @param parentIdentifier * The identifier of the parent connection group, or null if the root * connection group is to be queried. * * @return * A Set containing all identifiers of all objects. */ Set selectIdentifiersWithin(@Param("parentIdentifier") String parentIdentifier); /** * Selects the identifiers of all connection groups within the given parent * connection group that are explicitly readable by the given user. If * identifiers are needed by a system administrator (who, by definition, * does not need explicit read rights), use selectIdentifiersWithin() * instead. * * @param user * The user whose permissions should determine whether an identifier * is returned. * * @param parentIdentifier * The identifier of the parent connection group, or null if the root * connection group is to be queried. * * @return * A Set containing all identifiers of all readable objects. */ Set selectReadableIdentifiersWithin(@Param("user") UserModel user, @Param("parentIdentifier") String parentIdentifier); /** * Selects the connection group within the given parent group and having * the given name. If no such connection group exists, null is returned. * * @param parentIdentifier * The identifier of the parent group to search within. * * @param name * The name of the connection group to find. * * @return * The connection group having the given name within the given parent * group, or null if no such connection group exists. */ ConnectionGroupModel selectOneByName(@Param("parentIdentifier") String parentIdentifier, @Param("name") String name); }././@LongLink0100644 0000000 0000000 00000000251 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connectiongroup/package-info.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000002313 12612226251 035253 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes related to connection groups. */ package org.glyptodon.guacamole.auth.jdbc.connectiongroup; ././@LongLink0100644 0000000 0000000 00000000263 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/connectiongroup/ConnectionGroupService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000024060 12612226251 035256 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.connectiongroup; import com.google.inject.Inject; import com.google.inject.Provider; import java.util.Set; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.base.ModeledDirectoryObjectMapper; import org.glyptodon.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.GuacamoleUnsupportedException; import org.glyptodon.guacamole.auth.jdbc.base.ModeledGroupedDirectoryObjectService; import org.glyptodon.guacamole.auth.jdbc.permission.ConnectionGroupPermissionMapper; import org.glyptodon.guacamole.auth.jdbc.permission.ObjectPermissionMapper; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; /** * Service which provides convenience methods for creating, retrieving, and * manipulating connection groups. * * @author Michael Jumper, James Muehlner */ public class ConnectionGroupService extends ModeledGroupedDirectoryObjectService { /** * Mapper for accessing connection groups. */ @Inject private ConnectionGroupMapper connectionGroupMapper; /** * Mapper for manipulating connection group permissions. */ @Inject private ConnectionGroupPermissionMapper connectionGroupPermissionMapper; /** * Provider for creating connection groups. */ @Inject private Provider connectionGroupProvider; /** * Service for creating and tracking tunnels. */ @Inject private GuacamoleTunnelService tunnelService; @Override protected ModeledDirectoryObjectMapper getObjectMapper() { return connectionGroupMapper; } @Override protected ObjectPermissionMapper getPermissionMapper() { return connectionGroupPermissionMapper; } @Override protected ModeledConnectionGroup getObjectInstance(AuthenticatedUser currentUser, ConnectionGroupModel model) { ModeledConnectionGroup connectionGroup = connectionGroupProvider.get(); connectionGroup.init(currentUser, model); return connectionGroup; } @Override protected ConnectionGroupModel getModelInstance(AuthenticatedUser currentUser, final ConnectionGroup object) { // Create new ModeledConnectionGroup backed by blank model ConnectionGroupModel model = new ConnectionGroupModel(); ModeledConnectionGroup connectionGroup = getObjectInstance(currentUser, model); // Set model contents through ModeledConnectionGroup, copying the provided connection group connectionGroup.setParentIdentifier(object.getParentIdentifier()); connectionGroup.setName(object.getName()); connectionGroup.setType(object.getType()); connectionGroup.setAttributes(object.getAttributes()); return model; } @Override protected boolean hasCreatePermission(AuthenticatedUser user) throws GuacamoleException { // Return whether user has explicit connection group creation permission SystemPermissionSet permissionSet = user.getUser().getSystemPermissions(); return permissionSet.hasPermission(SystemPermission.Type.CREATE_CONNECTION_GROUP); } @Override protected ObjectPermissionSet getPermissionSet(AuthenticatedUser user) throws GuacamoleException { // Return permissions related to connection groups return user.getUser().getConnectionGroupPermissions(); } @Override protected void beforeCreate(AuthenticatedUser user, ConnectionGroupModel model) throws GuacamoleException { super.beforeCreate(user, model); // Name must not be blank if (model.getName() == null || model.getName().trim().isEmpty()) throw new GuacamoleClientException("Connection group names must not be blank."); // Do not attempt to create duplicate connection groups ConnectionGroupModel existing = connectionGroupMapper.selectOneByName(model.getParentIdentifier(), model.getName()); if (existing != null) throw new GuacamoleClientException("The connection group \"" + model.getName() + "\" already exists."); } @Override protected void beforeUpdate(AuthenticatedUser user, ConnectionGroupModel model) throws GuacamoleException { super.beforeUpdate(user, model); // Name must not be blank if (model.getName() == null || model.getName().trim().isEmpty()) throw new GuacamoleClientException("Connection group names must not be blank."); // Check whether such a connection group is already present ConnectionGroupModel existing = connectionGroupMapper.selectOneByName(model.getParentIdentifier(), model.getName()); if (existing != null) { // If the specified name matches a DIFFERENT existing connection group, the update cannot continue if (!existing.getObjectID().equals(model.getObjectID())) throw new GuacamoleClientException("The connection group \"" + model.getName() + "\" already exists."); } // Verify that this connection group's location does not create a cycle String relativeParentIdentifier = model.getParentIdentifier(); while (relativeParentIdentifier != null) { // Abort if cycle is detected if (relativeParentIdentifier.equals(model.getIdentifier())) throw new GuacamoleUnsupportedException("A connection group may not contain itself."); // Advance to next parent ModeledConnectionGroup relativeParentGroup = retrieveObject(user, relativeParentIdentifier); relativeParentIdentifier = relativeParentGroup.getModel().getParentIdentifier(); } } /** * Returns the set of all identifiers for all connection groups within the * connection group having the given identifier. Only connection groups * that the user has read access to will be returned. * * Permission to read the connection group having the given identifier is * NOT checked. * * @param user * The user retrieving the identifiers. * * @param identifier * The identifier of the parent connection group, or null to check the * root connection group. * * @return * The set of all identifiers for all connection groups in the * connection group having the given identifier that the user has read * access to. * * @throws GuacamoleException * If an error occurs while reading identifiers. */ public Set getIdentifiersWithin(AuthenticatedUser user, String identifier) throws GuacamoleException { // Bypass permission checks if the user is a system admin if (user.getUser().isAdministrator()) return connectionGroupMapper.selectIdentifiersWithin(identifier); // Otherwise only return explicitly readable identifiers else return connectionGroupMapper.selectReadableIdentifiersWithin(user.getUser().getModel(), identifier); } /** * Connects to the given connection group as the given user, using the * given client information. If the user does not have permission to read * the connection group, permission will be denied. * * @param user * The user connecting to the connection group. * * @param connectionGroup * The connectionGroup being connected to. * * @param info * Information associated with the connecting client. * * @return * A connected GuacamoleTunnel associated with a newly-established * connection. * * @throws GuacamoleException * If permission to connect to this connection is denied. */ public GuacamoleTunnel connect(AuthenticatedUser user, ModeledConnectionGroup connectionGroup, GuacamoleClientInformation info) throws GuacamoleException { // Connect only if READ permission is granted if (hasObjectPermission(user, connectionGroup.getIdentifier(), ObjectPermission.Type.READ)) return tunnelService.getGuacamoleTunnel(user, connectionGroup, info); // The user does not have permission to connect throw new GuacamoleSecurityException("Permission denied."); } } ././@LongLink0100644 0000000 0000000 00000000234 12645510255 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/JDBCEnvironment.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000010075 12645510255 035265 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.environment.LocalEnvironment; /** * A JDBC-specific implementation of Environment that defines generic properties * intended for use within JDBC based authentication providers. * * @author James Muehlner */ public abstract class JDBCEnvironment extends LocalEnvironment { /** * Constructs a new JDBCEnvironment using an underlying LocalEnviroment to * read properties from the file system. * * @throws GuacamoleException * If an error occurs while setting up the underlying LocalEnvironment. */ public JDBCEnvironment() throws GuacamoleException { super(); } /** * Returns the default maximum number of concurrent connections to allow to * any one connection, unless specified differently on an individual * connection. Zero denotes unlimited. * * @return * The default maximum allowable number of concurrent connections * to any connection. * * @throws GuacamoleException * If an error occurs while retrieving the property. */ public abstract int getDefaultMaxConnections() throws GuacamoleException; /** * Returns the default maximum number of concurrent connections to allow to * any one connection group, unless specified differently on an individual * connection group. Zero denotes unlimited. * * @return * The default maximum allowable number of concurrent connections * to any connection group. * * @throws GuacamoleException * If an error occurs while retrieving the property. */ public abstract int getDefaultMaxGroupConnections() throws GuacamoleException; /** * Returns the default maximum number of concurrent connections to allow to * any one connection by an individual user, unless specified differently on * an individual connection. Zero denotes unlimited. * * @return * The default maximum allowable number of concurrent connections to * any connection by an individual user. * * @throws GuacamoleException * If an error occurs while retrieving the property. */ public abstract int getDefaultMaxConnectionsPerUser() throws GuacamoleException; /** * Returns the default maximum number of concurrent connections to allow to * any one connection group by an individual user, unless specified * differently on an individual connection group. Zero denotes unlimited. * * @return * The default maximum allowable number of concurrent connections to * any connection group by an individual user. * * @throws GuacamoleException * If an error occurs while retrieving the property. */ public abstract int getDefaultMaxGroupConnectionsPerUser() throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000265 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000013730 12612226251 035260 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.activeconnection; import com.google.inject.Inject; import com.google.inject.Provider; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Set; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.auth.jdbc.base.DirectoryObjectService; import org.glyptodon.guacamole.auth.jdbc.tunnel.ActiveConnectionRecord; import org.glyptodon.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.ActiveConnection; /** * Service which provides convenience methods for creating, retrieving, and * manipulating active connections. * * @author Michael Jumper */ public class ActiveConnectionService implements DirectoryObjectService { /** * Service for creating and tracking tunnels. */ @Inject private GuacamoleTunnelService tunnelService; /** * Provider for active connections. */ @Inject private Provider trackedActiveConnectionProvider; @Override public TrackedActiveConnection retrieveObject(AuthenticatedUser user, String identifier) throws GuacamoleException { // Pull objects having given identifier Collection objects = retrieveObjects(user, Collections.singleton(identifier)); // If no such object, return null if (objects.isEmpty()) return null; // The object collection will have exactly one element unless the // database has seriously lost integrity assert(objects.size() == 1); // Return first and only object return objects.iterator().next(); } @Override public Collection retrieveObjects(AuthenticatedUser user, Collection identifiers) throws GuacamoleException { boolean isAdmin = user.getUser().isAdministrator(); Set identifierSet = new HashSet(identifiers); // Retrieve all visible connections (permissions enforced by tunnel service) Collection records = tunnelService.getActiveConnections(user); // Restrict to subset of records which match given identifiers Collection activeConnections = new ArrayList(identifiers.size()); for (ActiveConnectionRecord record : records) { // Add connection if within requested identifiers if (identifierSet.contains(record.getUUID().toString())) { TrackedActiveConnection activeConnection = trackedActiveConnectionProvider.get(); activeConnection.init(user, record, isAdmin); activeConnections.add(activeConnection); } } return activeConnections; } @Override public void deleteObject(AuthenticatedUser user, String identifier) throws GuacamoleException { // Only administrators may delete active connections if (!user.getUser().isAdministrator()) throw new GuacamoleSecurityException("Permission denied."); // Close connection, if it exists (and we have permission) ActiveConnection activeConnection = retrieveObject(user, identifier); if (activeConnection != null) { // Close connection if not already closed GuacamoleTunnel tunnel = activeConnection.getTunnel(); if (tunnel != null && tunnel.isOpen()) tunnel.close(); } } @Override public Set getIdentifiers(AuthenticatedUser user) throws GuacamoleException { // Retrieve all visible connections (permissions enforced by tunnel service) Collection records = tunnelService.getActiveConnections(user); // Build list of identifiers Set identifiers = new HashSet(records.size()); for (ActiveConnectionRecord record : records) identifiers.add(record.getUUID().toString()); return identifiers; } @Override public TrackedActiveConnection createObject(AuthenticatedUser user, ActiveConnection object) throws GuacamoleException { // Updating active connections is not implemented throw new GuacamoleSecurityException("Permission denied."); } @Override public void updateObject(AuthenticatedUser user, TrackedActiveConnection object) throws GuacamoleException { // Updating active connections is not implemented throw new GuacamoleSecurityException("Permission denied."); } } ././@LongLink0100644 0000000 0000000 00000000273 12612226251 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/activeconnection/ActiveConnectionPermissionSet.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000003652 12612226251 035262 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.activeconnection; import com.google.inject.Inject; import org.glyptodon.guacamole.auth.jdbc.permission.ObjectPermissionService; import org.glyptodon.guacamole.auth.jdbc.permission.ObjectPermissionSet; /** * An implementation of ObjectPermissionSet which uses an injected service to * query and manipulate the permissions associated with active connections. * * @author Michael Jumper */ public class ActiveConnectionPermissionSet extends ObjectPermissionSet { /** * Service for querying and manipulating active connection permissions. */ @Inject private ActiveConnectionPermissionService activeConnectionPermissionService; @Override protected ObjectPermissionService getObjectPermissionService() { return activeConnectionPermissionService; } } ././@LongLink0100644 0000000 0000000 00000000252 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/activeconnection/package-info.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000002327 12612226251 035260 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes related to currently-active connections. */ package org.glyptodon.guacamole.auth.jdbc.activeconnection; ././@LongLink0100644 0000000 0000000 00000000267 12612226251 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/activeconnection/ActiveConnectionDirectory.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000006163 12612226251 035262 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.activeconnection; import com.google.inject.Inject; import java.util.Collection; import java.util.Collections; import java.util.Set; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.auth.jdbc.base.RestrictedObject; import org.glyptodon.guacamole.net.auth.ActiveConnection; import org.glyptodon.guacamole.net.auth.Directory; /** * Implementation of a Directory which contains all currently-active * connections. * * @author Michael Jumper */ public class ActiveConnectionDirectory extends RestrictedObject implements Directory { /** * Service for retrieving and manipulating active connections. */ @Inject private ActiveConnectionService activeConnectionService; @Override public ActiveConnection get(String identifier) throws GuacamoleException { return activeConnectionService.retrieveObject(getCurrentUser(), identifier); } @Override public Collection getAll(Collection identifiers) throws GuacamoleException { Collection objects = activeConnectionService.retrieveObjects(getCurrentUser(), identifiers); return Collections.unmodifiableCollection(objects); } @Override public Set getIdentifiers() throws GuacamoleException { return activeConnectionService.getIdentifiers(getCurrentUser()); } @Override public void add(ActiveConnection object) throws GuacamoleException { activeConnectionService.createObject(getCurrentUser(), object); } @Override public void update(ActiveConnection object) throws GuacamoleException { TrackedActiveConnection connection = (TrackedActiveConnection) object; activeConnectionService.updateObject(getCurrentUser(), connection); } @Override public void remove(String identifier) throws GuacamoleException { activeConnectionService.deleteObject(getCurrentUser(), identifier); } } ././@LongLink0100644 0000000 0000000 00000000265 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/activeconnection/TrackedActiveConnection.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000012124 12612226251 035254 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.activeconnection; import java.util.Date; import org.glyptodon.guacamole.auth.jdbc.base.RestrictedObject; import org.glyptodon.guacamole.auth.jdbc.tunnel.ActiveConnectionRecord; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.ActiveConnection; /** * An implementation of the ActiveConnection object which has an associated * ActiveConnectionRecord. * * @author Michael Jumper */ public class TrackedActiveConnection extends RestrictedObject implements ActiveConnection { /** * The identifier of this active connection. */ private String identifier; /** * The identifier of the associated connection. */ private String connectionIdentifier; /** * The date and time this active connection began. */ private Date startDate; /** * The remote host that initiated this connection. */ private String remoteHost; /** * The username of the user that initiated this connection. */ private String username; /** * The underlying GuacamoleTunnel. */ private GuacamoleTunnel tunnel; /** * Initializes this TrackedActiveConnection, copying the data associated * with the given active connection record. At a minimum, the identifier * of this active connection will be set, the start date, and the * identifier of the associated connection will be copied. If requested, * sensitive information like the associated username will be copied, as * well. * * @param currentUser * The user that created or retrieved this object. * * @param activeConnectionRecord * The active connection record to copy. * * @param includeSensitiveInformation * Whether sensitive data should be copied from the connection record * as well. This includes the remote host, associated tunnel, and * username. */ public void init(AuthenticatedUser currentUser, ActiveConnectionRecord activeConnectionRecord, boolean includeSensitiveInformation) { super.init(currentUser); // Copy all non-sensitive data from given record this.connectionIdentifier = activeConnectionRecord.getConnection().getIdentifier(); this.identifier = activeConnectionRecord.getUUID().toString(); this.startDate = activeConnectionRecord.getStartDate(); // Include sensitive data, too, if requested if (includeSensitiveInformation) { this.remoteHost = activeConnectionRecord.getRemoteHost(); this.tunnel = activeConnectionRecord.getTunnel(); this.username = activeConnectionRecord.getUsername(); } } @Override public String getIdentifier() { return identifier; } @Override public void setIdentifier(String identifier) { this.identifier = identifier; } @Override public String getConnectionIdentifier() { return connectionIdentifier; } @Override public void setConnectionIdentifier(String connnectionIdentifier) { this.connectionIdentifier = connnectionIdentifier; } @Override public Date getStartDate() { return startDate; } @Override public void setStartDate(Date startDate) { this.startDate = startDate; } @Override public String getRemoteHost() { return remoteHost; } @Override public void setRemoteHost(String remoteHost) { this.remoteHost = remoteHost; } @Override public String getUsername() { return username; } @Override public void setUsername(String username) { this.username = username; } @Override public GuacamoleTunnel getTunnel() { return tunnel; } @Override public void setTunnel(GuacamoleTunnel tunnel) { this.tunnel = tunnel; } } ././@LongLink0100644 0000000 0000000 00000000277 12612226251 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/activeconnection/ActiveConnectionPermissionService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000015030 12612226251 035253 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.activeconnection; import com.google.inject.Inject; import com.google.inject.Provider; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Set; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.auth.jdbc.permission.AbstractPermissionService; import org.glyptodon.guacamole.auth.jdbc.permission.ObjectPermissionService; import org.glyptodon.guacamole.auth.jdbc.tunnel.ActiveConnectionRecord; import org.glyptodon.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.user.ModeledUser; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; /** * Service which provides convenience methods for creating, retrieving, and * manipulating active connections. * * @author Michael Jumper */ public class ActiveConnectionPermissionService extends AbstractPermissionService implements ObjectPermissionService { /** * Service for creating and tracking tunnels. */ @Inject private GuacamoleTunnelService tunnelService; /** * Provider for active connection permission sets. */ @Inject private Provider activeConnectionPermissionSetProvider; @Override public ObjectPermission retrievePermission(AuthenticatedUser user, ModeledUser targetUser, ObjectPermission.Type type, String identifier) throws GuacamoleException { // Retrieve permissions Set permissions = retrievePermissions(user, targetUser); // If retrieved permissions contains the requested permission, return it ObjectPermission permission = new ObjectPermission(type, identifier); if (permissions.contains(permission)) return permission; // Otherwise, no such permission return null; } @Override public Set retrievePermissions(AuthenticatedUser user, ModeledUser targetUser) throws GuacamoleException { // Retrieve permissions only if allowed if (canReadPermissions(user, targetUser)) { // Only administrators may access active connections boolean isAdmin = targetUser.isAdministrator(); // Get all active connections Collection records = tunnelService.getActiveConnections(user); // We have READ, and possibly DELETE, on all active connections Set permissions = new HashSet(); for (ActiveConnectionRecord record : records) { // Add implicit READ String identifier = record.getUUID().toString(); permissions.add(new ObjectPermission(ObjectPermission.Type.READ, identifier)); // If we're and admin, then we also have DELETE if (isAdmin) permissions.add(new ObjectPermission(ObjectPermission.Type.DELETE, identifier)); } return permissions; } throw new GuacamoleSecurityException("Permission denied."); } @Override public Collection retrieveAccessibleIdentifiers(AuthenticatedUser user, ModeledUser targetUser, Collection permissionTypes, Collection identifiers) throws GuacamoleException { Set permissions = retrievePermissions(user, targetUser); Collection accessibleObjects = new ArrayList(permissions.size()); // For each identifier/permission combination for (String identifier : identifiers) { for (ObjectPermission.Type permissionType : permissionTypes) { // Add identifier if at least one requested permission is granted ObjectPermission permission = new ObjectPermission(permissionType, identifier); if (permissions.contains(permission)) { accessibleObjects.add(identifier); break; } } } return accessibleObjects; } @Override public ObjectPermissionSet getPermissionSet(AuthenticatedUser user, ModeledUser targetUser) throws GuacamoleException { // Create permission set for requested user ActiveConnectionPermissionSet permissionSet = activeConnectionPermissionSetProvider.get(); permissionSet.init(user, targetUser); return permissionSet; } @Override public void createPermissions(AuthenticatedUser user, ModeledUser targetUser, Collection permissions) throws GuacamoleException { // Creating active connection permissions is not implemented throw new GuacamoleSecurityException("Permission denied."); } @Override public void deletePermissions(AuthenticatedUser user, ModeledUser targetUser, Collection permissions) throws GuacamoleException { // Deleting active connection permissions is not implemented throw new GuacamoleSecurityException("Permission denied."); } } ././@LongLink0100644 0000000 0000000 00000000255 12645510255 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000017616 12645510255 035275 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc; import org.glyptodon.guacamole.auth.jdbc.user.UserContext; import org.glyptodon.guacamole.auth.jdbc.connectiongroup.RootConnectionGroup; import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ConnectionGroupDirectory; import org.glyptodon.guacamole.auth.jdbc.connection.ConnectionDirectory; import org.glyptodon.guacamole.auth.jdbc.connection.ModeledGuacamoleConfiguration; import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; import org.glyptodon.guacamole.auth.jdbc.permission.SystemPermissionSet; import org.glyptodon.guacamole.auth.jdbc.user.ModeledUser; import org.glyptodon.guacamole.auth.jdbc.user.UserDirectory; import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ConnectionGroupMapper; import org.glyptodon.guacamole.auth.jdbc.connection.ConnectionMapper; import org.glyptodon.guacamole.auth.jdbc.connection.ConnectionRecordMapper; import org.glyptodon.guacamole.auth.jdbc.connection.ParameterMapper; import org.glyptodon.guacamole.auth.jdbc.permission.SystemPermissionMapper; import org.glyptodon.guacamole.auth.jdbc.user.UserMapper; import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ConnectionGroupService; import org.glyptodon.guacamole.auth.jdbc.connection.ConnectionService; import org.glyptodon.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService; import org.glyptodon.guacamole.auth.jdbc.security.PasswordEncryptionService; import org.glyptodon.guacamole.auth.jdbc.security.SHA256PasswordEncryptionService; import org.glyptodon.guacamole.auth.jdbc.security.SaltService; import org.glyptodon.guacamole.auth.jdbc.security.SecureRandomSaltService; import org.glyptodon.guacamole.auth.jdbc.permission.SystemPermissionService; import org.glyptodon.guacamole.auth.jdbc.user.UserService; import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; import org.glyptodon.guacamole.auth.jdbc.permission.ConnectionGroupPermissionMapper; import org.glyptodon.guacamole.auth.jdbc.permission.ConnectionGroupPermissionService; import org.glyptodon.guacamole.auth.jdbc.permission.ConnectionGroupPermissionSet; import org.glyptodon.guacamole.auth.jdbc.permission.ConnectionPermissionMapper; import org.glyptodon.guacamole.auth.jdbc.permission.ConnectionPermissionService; import org.glyptodon.guacamole.auth.jdbc.permission.ConnectionPermissionSet; import org.glyptodon.guacamole.auth.jdbc.permission.UserPermissionMapper; import org.glyptodon.guacamole.auth.jdbc.permission.UserPermissionService; import org.glyptodon.guacamole.auth.jdbc.permission.UserPermissionSet; import org.glyptodon.guacamole.auth.jdbc.activeconnection.ActiveConnectionDirectory; import org.glyptodon.guacamole.auth.jdbc.activeconnection.ActiveConnectionPermissionService; import org.glyptodon.guacamole.auth.jdbc.activeconnection.ActiveConnectionPermissionSet; import org.glyptodon.guacamole.auth.jdbc.activeconnection.ActiveConnectionService; import org.glyptodon.guacamole.auth.jdbc.activeconnection.TrackedActiveConnection; import org.glyptodon.guacamole.auth.jdbc.tunnel.RestrictedGuacamoleTunnelService; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.mybatis.guice.MyBatisModule; import org.mybatis.guice.datasource.builtin.PooledDataSourceProvider; /** * Guice module which configures the injections used by the JDBC authentication * provider base. This module MUST be included in the Guice injector, or * authentication providers based on JDBC will not function. * * @author Michael Jumper * @author James Muehlner */ public class JDBCAuthenticationProviderModule extends MyBatisModule { /** * The environment of the Guacamole server. */ private final JDBCEnvironment environment; /** * The AuthenticationProvider which is using this module to configure * injection. */ private final AuthenticationProvider authProvider; /** * Creates a new JDBC authentication provider module that configures the * various injected base classes using the given environment, and provides * connections using the given socket service. * * @param authProvider * The AuthenticationProvider which is using this module to configure * injection. * * @param environment * The environment to use to configure injected classes. */ public JDBCAuthenticationProviderModule(AuthenticationProvider authProvider, JDBCEnvironment environment) { this.authProvider = authProvider; this.environment = environment; } @Override protected void initialize() { // Datasource bindDataSourceProviderType(PooledDataSourceProvider.class); // Transaction factory bindTransactionFactoryType(JdbcTransactionFactory.class); // Add MyBatis mappers addMapperClass(ConnectionMapper.class); addMapperClass(ConnectionGroupMapper.class); addMapperClass(ConnectionGroupPermissionMapper.class); addMapperClass(ConnectionPermissionMapper.class); addMapperClass(ConnectionRecordMapper.class); addMapperClass(ParameterMapper.class); addMapperClass(SystemPermissionMapper.class); addMapperClass(UserMapper.class); addMapperClass(UserPermissionMapper.class); // Bind core implementations of guacamole-ext classes bind(ActiveConnectionDirectory.class); bind(ActiveConnectionPermissionSet.class); bind(AuthenticationProvider.class).toInstance(authProvider); bind(JDBCEnvironment.class).toInstance(environment); bind(ConnectionDirectory.class); bind(ConnectionGroupDirectory.class); bind(ConnectionGroupPermissionSet.class); bind(ConnectionPermissionSet.class); bind(ModeledConnection.class); bind(ModeledConnectionGroup.class); bind(ModeledGuacamoleConfiguration.class); bind(ModeledUser.class); bind(RootConnectionGroup.class); bind(SystemPermissionSet.class); bind(TrackedActiveConnection.class); bind(UserContext.class); bind(UserDirectory.class); bind(UserPermissionSet.class); // Bind services bind(ActiveConnectionService.class); bind(ActiveConnectionPermissionService.class); bind(ConnectionGroupPermissionService.class); bind(ConnectionGroupService.class); bind(ConnectionPermissionService.class); bind(ConnectionService.class); bind(GuacamoleTunnelService.class).to(RestrictedGuacamoleTunnelService.class); bind(PasswordEncryptionService.class).to(SHA256PasswordEncryptionService.class); bind(SaltService.class).to(SecureRandomSaltService.class); bind(SystemPermissionService.class); bind(UserPermissionService.class); bind(UserService.class); } } ././@LongLink0100644 0000000 0000000 00000000233 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/UserModel.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000026043 12612226251 035261 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.user; import java.sql.Date; import java.sql.Time; import org.glyptodon.guacamole.auth.jdbc.base.ObjectModel; /** * Object representation of a Guacamole user, as represented in the database. * * @author Michael Jumper */ public class UserModel extends ObjectModel { /** * The SHA-256 hash of the password and salt. */ private byte[] passwordHash; /** * The 32-byte random binary password salt that was appended to the * password prior to hashing. */ private byte[] passwordSalt; /** * Whether the user account is disabled. Disabled accounts exist and can * be modified, but cannot be used. */ private boolean disabled; /** * Whether the user's password is expired. If a user's password is expired, * it must be changed immediately upon login, and the account cannot be * used until this occurs. */ private boolean expired; /** * The time each day after which this user account may be used, stored in * local time according to the value of timeZone. */ private Time accessWindowStart; /** * The time each day after which this user account may NOT be used, stored * in local time according to the value of timeZone. */ private Time accessWindowEnd; /** * The day after which this account becomes valid and usable. Account * validity begins at midnight of this day. Time information within the * Date object is ignored. */ private Date validFrom; /** * The day after which this account can no longer be used. Account validity * ends at midnight of the day following this day. Time information within * the Date object is ignored. */ private Date validUntil; /** * The ID of the time zone used for all time comparisons for this user. * Both accessWindowStart and accessWindowEnd values will use this time * zone, as will checks for whether account validity dates have passed. If * unset, the server's local time zone is used. */ private String timeZone; /** * Creates a new, empty user. */ public UserModel() { } /** * Returns the hash of this user's password and password salt. This may be * null if the user was not retrieved from the database, and setPassword() * has not yet been called. * * @return * The hash of this user's password and password salt. */ public byte[] getPasswordHash() { return passwordHash; } /** * Sets the hash of this user's password and password salt. This is * normally only set upon retrieval from the database, or through a call * to the higher-level setPassword() function. * * @param passwordHash * The hash of this user's password and password salt. */ public void setPasswordHash(byte[] passwordHash) { this.passwordHash = passwordHash; } /** * Returns the random salt that was used when generating this user's * password hash. This may be null if the user was not retrieved from the * database, and setPassword() has not yet been called. * * @return * The random salt that was used when generating this user's password * hash. */ public byte[] getPasswordSalt() { return passwordSalt; } /** * Sets the random salt that was used when generating this user's password * hash. This is normally only set upon retrieval from the database, or * through a call to the higher-level setPassword() function. * * @param passwordSalt * The random salt used when generating this user's password hash. */ public void setPasswordSalt(byte[] passwordSalt) { this.passwordSalt = passwordSalt; } /** * Returns whether the user has been disabled. Disabled users are not * allowed to login. Although their account data exists, all login attempts * will fail as if the account does not exist. * * @return * true if the account is disabled, false otherwise. */ public boolean isDisabled() { return disabled; } /** * Sets whether the user is disabled. Disabled users are not allowed to * login. Although their account data exists, all login attempts will fail * as if the account does not exist. * * @param disabled * true if the account should be disabled, false otherwise. */ public void setDisabled(boolean disabled) { this.disabled = disabled; } /** * Returns whether the user's password has expired. If a user's password is * expired, it must be immediately changed upon login. A user account with * an expired password cannot be used until the password has been changed. * * @return * true if the user's password has expired, false otherwise. */ public boolean isExpired() { return expired; } /** * Sets whether the user's password is expired. If a user's password is * expired, it must be immediately changed upon login. A user account with * an expired password cannot be used until the password has been changed. * * @param expired * true to expire the user's password, false otherwise. */ public void setExpired(boolean expired) { this.expired = expired; } /** * Returns the time each day after which this user account may be used. The * time returned will be local time according to the time zone set with * setTimeZone(). * * @return * The time each day after which this user account may be used, or null * if this restriction does not apply. */ public Time getAccessWindowStart() { return accessWindowStart; } /** * Sets the time each day after which this user account may be used. The * time given must be in local time according to the time zone set with * setTimeZone(). * * @param accessWindowStart * The time each day after which this user account may be used, or null * if this restriction does not apply. */ public void setAccessWindowStart(Time accessWindowStart) { this.accessWindowStart = accessWindowStart; } /** * Returns the time each day after which this user account may NOT be used. * The time returned will be local time according to the time zone set with * setTimeZone(). * * @return * The time each day after which this user account may NOT be used, or * null if this restriction does not apply. */ public Time getAccessWindowEnd() { return accessWindowEnd; } /** * Sets the time each day after which this user account may NOT be used. * The time given must be in local time according to the time zone set with * setTimeZone(). * * @param accessWindowEnd * The time each day after which this user account may NOT be used, or * null if this restriction does not apply. */ public void setAccessWindowEnd(Time accessWindowEnd) { this.accessWindowEnd = accessWindowEnd; } /** * Returns the day after which this account becomes valid and usable. * Account validity begins at midnight of this day. Any time information * within the returned Date object must be ignored. * * @return * The day after which this account becomes valid and usable, or null * if this restriction does not apply. */ public Date getValidFrom() { return validFrom; } /** * Sets the day after which this account becomes valid and usable. Account * validity begins at midnight of this day. Any time information within * the provided Date object will be ignored. * * @param validFrom * The day after which this account becomes valid and usable, or null * if this restriction does not apply. */ public void setValidFrom(Date validFrom) { this.validFrom = validFrom; } /** * Returns the day after which this account can no longer be used. Account * validity ends at midnight of the day following this day. Any time * information within the returned Date object must be ignored. * * @return * The day after which this account can no longer be used, or null if * this restriction does not apply. */ public Date getValidUntil() { return validUntil; } /** * Sets the day after which this account can no longer be used. Account * validity ends at midnight of the day following this day. Any time * information within the provided Date object will be ignored. * * @param validUntil * The day after which this account can no longer be used, or null if * this restriction does not apply. */ public void setValidUntil(Date validUntil) { this.validUntil = validUntil; } /** * Returns the Java ID of the time zone to be used for all time comparisons * for this user. This ID should correspond to a value returned by * TimeZone.getAvailableIDs(). If unset or invalid, the server's local time * zone must be used. * * @return * The ID of the time zone to be used for all time comparisons, which * should correspond to a value returned by TimeZone.getAvailableIDs(). */ public String getTimeZone() { return timeZone; } /** * Sets the Java ID of the time zone to be used for all time comparisons * for this user. This ID should correspond to a value returned by * TimeZone.getAvailableIDs(). If unset or invalid, the server's local time * zone will be used. * * @param timeZone * The ID of the time zone to be used for all time comparisons, which * should correspond to a value returned by TimeZone.getAvailableIDs(). */ public void setTimeZone(String timeZone) { this.timeZone = timeZone; } } ././@LongLink0100644 0000000 0000000 00000000235 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/UserService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000035101 12612226251 035254 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.user; import com.google.inject.Inject; import com.google.inject.Provider; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import javax.servlet.http.HttpServletRequest; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.auth.jdbc.base.ModeledDirectoryObjectMapper; import org.glyptodon.guacamole.auth.jdbc.base.ModeledDirectoryObjectService; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleUnsupportedException; import org.glyptodon.guacamole.auth.jdbc.permission.ObjectPermissionMapper; import org.glyptodon.guacamole.auth.jdbc.permission.ObjectPermissionModel; import org.glyptodon.guacamole.auth.jdbc.permission.UserPermissionMapper; import org.glyptodon.guacamole.auth.jdbc.security.PasswordEncryptionService; import org.glyptodon.guacamole.form.Field; import org.glyptodon.guacamole.form.PasswordField; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.User; import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo; import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Service which provides convenience methods for creating, retrieving, and * manipulating users. * * @author Michael Jumper, James Muehlner */ public class UserService extends ModeledDirectoryObjectService { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(UserService.class); /** * All user permissions which are implicitly granted to the new user upon * creation. */ private static final ObjectPermission.Type[] IMPLICIT_USER_PERMISSIONS = { ObjectPermission.Type.READ }; /** * The name of the HTTP password parameter to expect if the user is * changing their expired password upon login. */ private static final String NEW_PASSWORD_PARAMETER = "new-password"; /** * The password field to provide the user when their password is expired * and must be changed. */ private static final Field NEW_PASSWORD = new PasswordField(NEW_PASSWORD_PARAMETER); /** * The name of the HTTP password confirmation parameter to expect if the * user is changing their expired password upon login. */ private static final String CONFIRM_NEW_PASSWORD_PARAMETER = "confirm-new-password"; /** * The password confirmation field to provide the user when their password * is expired and must be changed. */ private static final Field CONFIRM_NEW_PASSWORD = new PasswordField(CONFIRM_NEW_PASSWORD_PARAMETER); /** * Information describing the expected credentials if a user's password is * expired. If a user's password is expired, it must be changed during the * login process. */ private static final CredentialsInfo EXPIRED_PASSWORD = new CredentialsInfo(Arrays.asList( CredentialsInfo.USERNAME, CredentialsInfo.PASSWORD, NEW_PASSWORD, CONFIRM_NEW_PASSWORD )); /** * Mapper for accessing users. */ @Inject private UserMapper userMapper; /** * Mapper for manipulating user permissions. */ @Inject private UserPermissionMapper userPermissionMapper; /** * Provider for creating users. */ @Inject private Provider userProvider; /** * Service for hashing passwords. */ @Inject private PasswordEncryptionService encryptionService; @Override protected ModeledDirectoryObjectMapper getObjectMapper() { return userMapper; } @Override protected ObjectPermissionMapper getPermissionMapper() { return userPermissionMapper; } @Override protected ModeledUser getObjectInstance(AuthenticatedUser currentUser, UserModel model) { ModeledUser user = userProvider.get(); user.init(currentUser, model); return user; } @Override protected UserModel getModelInstance(AuthenticatedUser currentUser, final User object) { // Create new ModeledUser backed by blank model UserModel model = new UserModel(); ModeledUser user = getObjectInstance(currentUser, model); // Set model contents through ModeledUser, copying the provided user user.setIdentifier(object.getIdentifier()); user.setPassword(object.getPassword()); user.setAttributes(object.getAttributes()); return model; } @Override protected boolean hasCreatePermission(AuthenticatedUser user) throws GuacamoleException { // Return whether user has explicit user creation permission SystemPermissionSet permissionSet = user.getUser().getSystemPermissions(); return permissionSet.hasPermission(SystemPermission.Type.CREATE_USER); } @Override protected ObjectPermissionSet getPermissionSet(AuthenticatedUser user) throws GuacamoleException { // Return permissions related to users return user.getUser().getUserPermissions(); } @Override protected void beforeCreate(AuthenticatedUser user, UserModel model) throws GuacamoleException { super.beforeCreate(user, model); // Username must not be blank if (model.getIdentifier() == null || model.getIdentifier().trim().isEmpty()) throw new GuacamoleClientException("The username must not be blank."); // Do not create duplicate users Collection existing = userMapper.select(Collections.singleton(model.getIdentifier())); if (!existing.isEmpty()) throw new GuacamoleClientException("User \"" + model.getIdentifier() + "\" already exists."); } @Override protected void beforeUpdate(AuthenticatedUser user, UserModel model) throws GuacamoleException { super.beforeUpdate(user, model); // Username must not be blank if (model.getIdentifier() == null || model.getIdentifier().trim().isEmpty()) throw new GuacamoleClientException("The username must not be blank."); // Check whether such a user is already present UserModel existing = userMapper.selectOne(model.getIdentifier()); if (existing != null) { // Do not rename to existing user if (!existing.getObjectID().equals(model.getObjectID())) throw new GuacamoleClientException("User \"" + model.getIdentifier() + "\" already exists."); } } @Override protected Collection getImplicitPermissions(AuthenticatedUser user, UserModel model) { // Get original set of implicit permissions Collection implicitPermissions = super.getImplicitPermissions(user, model); // Grant implicit permissions to the new user for (ObjectPermission.Type permissionType : IMPLICIT_USER_PERMISSIONS) { ObjectPermissionModel permissionModel = new ObjectPermissionModel(); permissionModel.setUserID(model.getObjectID()); permissionModel.setUsername(model.getIdentifier()); permissionModel.setType(permissionType); permissionModel.setObjectIdentifier(model.getIdentifier()); // Add new permission to implicit permission set implicitPermissions.add(permissionModel); } return implicitPermissions; } @Override protected void beforeDelete(AuthenticatedUser user, String identifier) throws GuacamoleException { super.beforeDelete(user, identifier); // Do not allow users to delete themselves if (identifier.equals(user.getUser().getIdentifier())) throw new GuacamoleUnsupportedException("Deleting your own user is not allowed."); } /** * Retrieves the user corresponding to the given credentials from the * database. If the user account is expired, and the credentials contain * the necessary additional parameters to reset the user's password, the * password is reset. * * @param authenticationProvider * The AuthenticationProvider on behalf of which the user is being * retrieved. * * @param credentials * The credentials to use when locating the user. * * @return * An AuthenticatedUser containing the existing ModeledUser object if * the credentials given are valid, null otherwise. * * @throws GuacamoleException * If the provided credentials to not conform to expectations. */ public AuthenticatedUser retrieveAuthenticatedUser(AuthenticationProvider authenticationProvider, Credentials credentials) throws GuacamoleException { // Get username and password String username = credentials.getUsername(); String password = credentials.getPassword(); // Retrieve corresponding user model, if such a user exists UserModel userModel = userMapper.selectOne(username); if (userModel == null) return null; // If user is disabled, pretend user does not exist if (userModel.isDisabled()) return null; // Verify provided password is correct byte[] hash = encryptionService.createPasswordHash(password, userModel.getPasswordSalt()); if (!Arrays.equals(hash, userModel.getPasswordHash())) return null; // Create corresponding user object, set up cyclic reference ModeledUser user = getObjectInstance(null, userModel); user.setCurrentUser(new AuthenticatedUser(authenticationProvider, user, credentials)); // Verify user account is still valid as of today if (!user.isAccountValid()) throw new GuacamoleClientException("LOGIN.ERROR_NOT_VALID"); // Verify user account is allowed to be used at the current time if (!user.isAccountAccessible()) throw new GuacamoleClientException("LOGIN.ERROR_NOT_ACCESSIBLE"); // Update password if password is expired if (userModel.isExpired()) { // Pull new password from HTTP request HttpServletRequest request = credentials.getRequest(); String newPassword = request.getParameter(NEW_PASSWORD_PARAMETER); String confirmNewPassword = request.getParameter(CONFIRM_NEW_PASSWORD_PARAMETER); // Require new password if account is expired if (newPassword == null || confirmNewPassword == null) { logger.info("The password of user \"{}\" has expired and must be reset.", username); throw new GuacamoleInsufficientCredentialsException("LOGIN.INFO_PASSWORD_EXPIRED", EXPIRED_PASSWORD); } // New password must be different from old password if (newPassword.equals(credentials.getPassword())) throw new GuacamoleClientException("LOGIN.ERROR_PASSWORD_SAME"); // New password must not be blank if (newPassword.isEmpty()) throw new GuacamoleClientException("LOGIN.ERROR_PASSWORD_BLANK"); // Confirm that the password was entered correctly twice if (!newPassword.equals(confirmNewPassword)) throw new GuacamoleClientException("LOGIN.ERROR_PASSWORD_MISMATCH"); // Change password and reset expiration flag userModel.setExpired(false); user.setPassword(newPassword); userMapper.update(userModel); logger.info("Expired password of user \"{}\" has been reset.", username); } // Return now-authenticated user return user.getCurrentUser(); } /** * Retrieves the user corresponding to the given AuthenticatedUser from the * database. * * @param authenticatedUser * The AuthenticatedUser to retrieve the corresponding ModeledUser of. * * @return * The ModeledUser which corresponds to the given AuthenticatedUser, or * null if no such user exists. */ public ModeledUser retrieveUser(org.glyptodon.guacamole.net.auth.AuthenticatedUser authenticatedUser) { // If we already queried this user, return that rather than querying again if (authenticatedUser instanceof AuthenticatedUser) return ((AuthenticatedUser) authenticatedUser).getUser(); // Get username String username = authenticatedUser.getIdentifier(); // Retrieve corresponding user model, if such a user exists UserModel userModel = userMapper.selectOne(username); if (userModel == null) return null; // Create corresponding user object, set up cyclic reference ModeledUser user = getObjectInstance(null, userModel); user.setCurrentUser(new AuthenticatedUser(authenticatedUser.getAuthenticationProvider(), user, authenticatedUser.getCredentials())); // Return already-authenticated user return user; } } ././@LongLink0100644 0000000 0000000 00000000257 12612226251 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/AuthenticationProviderService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000010547 12612226251 035263 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.user; import com.google.inject.Inject; import com.google.inject.Provider; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo; import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; /** * Service which authenticates users based on credentials and provides for * the creation of corresponding, new UserContext objects for authenticated * users. * * @author Michael Jumper */ public class AuthenticationProviderService { /** * Service for accessing users. */ @Inject private UserService userService; /** * Provider for retrieving UserContext instances. */ @Inject private Provider userContextProvider; /** * Authenticates the user having the given credentials, returning a new * AuthenticatedUser instance only if the credentials are valid. If the * credentials are invalid or expired, an appropriate GuacamoleException * will be thrown. * * @param authenticationProvider * The AuthenticationProvider on behalf of which the user is being * authenticated. * * @param credentials * The credentials to use to produce the AuthenticatedUser. * * @return * A new AuthenticatedUser instance for the user identified by the * given credentials. * * @throws GuacamoleException * If an error occurs during authentication, or if the given * credentials are invalid or expired. */ public AuthenticatedUser authenticateUser(AuthenticationProvider authenticationProvider, Credentials credentials) throws GuacamoleException { // Authenticate user AuthenticatedUser user = userService.retrieveAuthenticatedUser(authenticationProvider, credentials); if (user != null) return user; // Otherwise, unauthorized throw new GuacamoleInvalidCredentialsException("Invalid login", CredentialsInfo.USERNAME_PASSWORD); } /** * Returning a new UserContext instance for the given already-authenticated * user. A new placeholder account will be created for any user that does * not already exist within the database. * * @param authenticatedUser * The credentials to use to produce the UserContext. * * @return * A new UserContext instance for the user identified by the given * credentials. * * @throws GuacamoleException * If an error occurs during authentication, or if the given * credentials are invalid or expired. */ public UserContext getUserContext(org.glyptodon.guacamole.net.auth.AuthenticatedUser authenticatedUser) throws GuacamoleException { // Retrieve user account for already-authenticated user ModeledUser user = userService.retrieveUser(authenticatedUser); if (user == null) return null; // Link to user context UserContext context = userContextProvider.get(); context.init(user.getCurrentUser()); return context; } } ././@LongLink0100644 0000000 0000000 00000000243 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/AuthenticatedUser.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000013245 12612226251 035261 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.user; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Credentials; /** * Associates a user with the credentials they used to authenticate. * * @author Michael Jumper */ public class AuthenticatedUser implements org.glyptodon.guacamole.net.auth.AuthenticatedUser { /** * The user that authenticated. */ private final ModeledUser user; /** * The credentials given when this user authenticated. */ private final Credentials credentials; /** * The AuthenticationProvider that authenticated this user. */ private final AuthenticationProvider authenticationProvider; /** * The host from which this user authenticated. */ private final String remoteHost; /** * Regular expression which matches any IPv4 address. */ private static final String IPV4_ADDRESS_REGEX = "([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})"; /** * Regular expression which matches any IPv6 address. */ private static final String IPV6_ADDRESS_REGEX = "([0-9a-fA-F]*(:[0-9a-fA-F]*){0,7})"; /** * Regular expression which matches any IP address, regardless of version. */ private static final String IP_ADDRESS_REGEX = "(" + IPV4_ADDRESS_REGEX + "|" + IPV6_ADDRESS_REGEX + ")"; /** * Pattern which matches valid values of the de-facto standard * "X-Forwarded-For" header. */ private static final Pattern X_FORWARDED_FOR = Pattern.compile("^" + IP_ADDRESS_REGEX + "(, " + IP_ADDRESS_REGEX + ")*$"); /** * Derives the remote host of the authenticating user from the given * credentials object. The remote host is derived from X-Forwarded-For * in addition to the actual source IP of the request, and thus is not * trusted. The derived remote host is really only useful for logging, * unless the server is configured such that X-Forwarded-For is guaranteed * to be trustworthy. * * @param credentials * The credentials to derive the remote host from. * * @return * The remote host from which the user with the given credentials is * authenticating. */ private static String getRemoteHost(Credentials credentials) { HttpServletRequest request = credentials.getRequest(); // Use X-Forwarded-For, if present and valid String header = request.getHeader("X-Forwarded-For"); if (header != null) { Matcher matcher = X_FORWARDED_FOR.matcher(header); if (matcher.matches()) return matcher.group(1); } // If header absent or invalid, just use source IP return request.getRemoteAddr(); } /** * Creates a new AuthenticatedUser associating the given user with their * corresponding credentials. * * @param authenticationProvider * The AuthenticationProvider that has authenticated the given user. * * @param user * The user this object should represent. * * @param credentials * The credentials given by the user when they authenticated. */ public AuthenticatedUser(AuthenticationProvider authenticationProvider, ModeledUser user, Credentials credentials) { this.authenticationProvider = authenticationProvider; this.user = user; this.credentials = credentials; this.remoteHost = getRemoteHost(credentials); } /** * Returns the user that authenticated. * * @return * The user that authenticated. */ public ModeledUser getUser() { return user; } /** * Returns the credentials given during authentication by this user. * * @return * The credentials given during authentication by this user. */ @Override public Credentials getCredentials() { return credentials; } /** * Returns the host from which this user authenticated. * * @return * The host from which this user authenticated. */ public String getRemoteHost() { return remoteHost; } @Override public AuthenticationProvider getAuthenticationProvider() { return authenticationProvider; } @Override public String getIdentifier() { return user.getIdentifier(); } @Override public void setIdentifier(String identifier) { user.setIdentifier(identifier); } } ././@LongLink0100644 0000000 0000000 00000000237 12612226251 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/UserDirectory.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000006015 12612226251 035256 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.user; import com.google.inject.Inject; import java.util.Collection; import java.util.Collections; import java.util.Set; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.auth.jdbc.base.RestrictedObject; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.User; import org.mybatis.guice.transactional.Transactional; /** * Implementation of the User Directory which is driven by an underlying, * arbitrary database. * * @author James Muehlner * @author Michael Jumper */ public class UserDirectory extends RestrictedObject implements Directory { /** * Service for managing user objects. */ @Inject private UserService userService; @Override public User get(String identifier) throws GuacamoleException { return userService.retrieveObject(getCurrentUser(), identifier); } @Override @Transactional public Collection getAll(Collection identifiers) throws GuacamoleException { Collection objects = userService.retrieveObjects(getCurrentUser(), identifiers); return Collections.unmodifiableCollection(objects); } @Override @Transactional public Set getIdentifiers() throws GuacamoleException { return userService.getIdentifiers(getCurrentUser()); } @Override @Transactional public void add(User object) throws GuacamoleException { userService.createObject(getCurrentUser(), object); } @Override @Transactional public void update(User object) throws GuacamoleException { ModeledUser user = (ModeledUser) object; userService.updateObject(getCurrentUser(), user); } @Override @Transactional public void remove(String identifier) throws GuacamoleException { userService.deleteObject(getCurrentUser(), identifier); } } ././@LongLink0100644 0000000 0000000 00000000236 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/package-info.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000002276 12612226251 035263 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes related to Guacamole users. */ package org.glyptodon.guacamole.auth.jdbc.user; ././@LongLink0100644 0000000 0000000 00000000235 12645510255 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/UserContext.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000013345 12645510255 035270 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.user; import org.glyptodon.guacamole.auth.jdbc.connectiongroup.RootConnectionGroup; import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ConnectionGroupDirectory; import org.glyptodon.guacamole.auth.jdbc.connection.ConnectionDirectory; import com.google.inject.Inject; import com.google.inject.Provider; import java.util.Collection; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.auth.jdbc.base.RestrictedObject; import org.glyptodon.guacamole.auth.jdbc.activeconnection.ActiveConnectionDirectory; import org.glyptodon.guacamole.auth.jdbc.connection.ConnectionRecordSet; import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; import org.glyptodon.guacamole.form.Form; import org.glyptodon.guacamole.net.auth.ActiveConnection; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.User; /** * UserContext implementation which is driven by an arbitrary, underlying * database. * * @author James Muehlner * @author Michael Jumper */ public class UserContext extends RestrictedObject implements org.glyptodon.guacamole.net.auth.UserContext { /** * The AuthenticationProvider that created this UserContext. */ @Inject private AuthenticationProvider authProvider; /** * User directory restricted by the permissions of the user associated * with this context. */ @Inject private UserDirectory userDirectory; /** * Connection directory restricted by the permissions of the user * associated with this context. */ @Inject private ConnectionDirectory connectionDirectory; /** * Connection group directory restricted by the permissions of the user * associated with this context. */ @Inject private ConnectionGroupDirectory connectionGroupDirectory; /** * ActiveConnection directory restricted by the permissions of the user * associated with this context. */ @Inject private ActiveConnectionDirectory activeConnectionDirectory; /** * Provider for creating the root group. */ @Inject private Provider rootGroupProvider; /** * Provider for creating connection record sets. */ @Inject private Provider connectionRecordSetProvider; @Override public void init(AuthenticatedUser currentUser) { super.init(currentUser); // Init directories userDirectory.init(currentUser); connectionDirectory.init(currentUser); connectionGroupDirectory.init(currentUser); activeConnectionDirectory.init(currentUser); } @Override public User self() { return getCurrentUser().getUser(); } @Override public AuthenticationProvider getAuthenticationProvider() { return authProvider; } @Override public Directory getUserDirectory() throws GuacamoleException { return userDirectory; } @Override public Directory getConnectionDirectory() throws GuacamoleException { return connectionDirectory; } @Override public Directory getConnectionGroupDirectory() throws GuacamoleException { return connectionGroupDirectory; } @Override public Directory getActiveConnectionDirectory() throws GuacamoleException { return activeConnectionDirectory; } @Override public ConnectionRecordSet getConnectionHistory() throws GuacamoleException { ConnectionRecordSet connectionRecordSet = connectionRecordSetProvider.get(); connectionRecordSet.init(getCurrentUser()); return connectionRecordSet; } @Override public ConnectionGroup getRootConnectionGroup() throws GuacamoleException { // Build and return a root group for the current user RootConnectionGroup rootGroup = rootGroupProvider.get(); rootGroup.init(getCurrentUser()); return rootGroup; } @Override public Collection getUserAttributes() { return ModeledUser.ATTRIBUTES; } @Override public Collection getConnectionAttributes() { return ModeledConnection.ATTRIBUTES; } @Override public Collection getConnectionGroupAttributes() { return ModeledConnectionGroup.ATTRIBUTES; } } ././@LongLink0100644 0000000 0000000 00000000235 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/ModeledUser.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000051164 12612226251 035263 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.user; import com.google.inject.Inject; import java.sql.Date; import java.sql.Time; import java.text.ParseException; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.TimeZone; import org.glyptodon.guacamole.auth.jdbc.base.ModeledDirectoryObject; import org.glyptodon.guacamole.auth.jdbc.security.PasswordEncryptionService; import org.glyptodon.guacamole.auth.jdbc.security.SaltService; import org.glyptodon.guacamole.auth.jdbc.permission.SystemPermissionService; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.auth.jdbc.activeconnection.ActiveConnectionPermissionService; import org.glyptodon.guacamole.auth.jdbc.permission.ConnectionGroupPermissionService; import org.glyptodon.guacamole.auth.jdbc.permission.ConnectionPermissionService; import org.glyptodon.guacamole.auth.jdbc.permission.UserPermissionService; import org.glyptodon.guacamole.form.BooleanField; import org.glyptodon.guacamole.form.DateField; import org.glyptodon.guacamole.form.Field; import org.glyptodon.guacamole.form.Form; import org.glyptodon.guacamole.form.TimeField; import org.glyptodon.guacamole.form.TimeZoneField; import org.glyptodon.guacamole.net.auth.User; import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * An implementation of the User object which is backed by a database model. * * @author James Muehlner * @author Michael Jumper */ public class ModeledUser extends ModeledDirectoryObject implements User { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(ModeledUser.class); /** * The name of the attribute which controls whether a user account is * disabled. */ public static final String DISABLED_ATTRIBUTE_NAME = "disabled"; /** * The name of the attribute which controls whether a user's password is * expired and must be reset upon login. */ public static final String EXPIRED_ATTRIBUTE_NAME = "expired"; /** * The name of the attribute which controls the time of day after which a * user may login. */ public static final String ACCESS_WINDOW_START_ATTRIBUTE_NAME = "access-window-start"; /** * The name of the attribute which controls the time of day after which a * user may NOT login. */ public static final String ACCESS_WINDOW_END_ATTRIBUTE_NAME = "access-window-end"; /** * The name of the attribute which controls the date after which a user's * account is valid. */ public static final String VALID_FROM_ATTRIBUTE_NAME = "valid-from"; /** * The name of the attribute which controls the date after which a user's * account is no longer valid. */ public static final String VALID_UNTIL_ATTRIBUTE_NAME = "valid-until"; /** * The name of the attribute which defines the time zone used for all * time and date attributes related to this user. */ public static final String TIMEZONE_ATTRIBUTE_NAME = "timezone"; /** * All attributes related to restricting user accounts, within a logical * form. */ public static final Form ACCOUNT_RESTRICTIONS = new Form("restrictions", Arrays.asList( new BooleanField(DISABLED_ATTRIBUTE_NAME, "true"), new BooleanField(EXPIRED_ATTRIBUTE_NAME, "true"), new TimeField(ACCESS_WINDOW_START_ATTRIBUTE_NAME), new TimeField(ACCESS_WINDOW_END_ATTRIBUTE_NAME), new DateField(VALID_FROM_ATTRIBUTE_NAME), new DateField(VALID_UNTIL_ATTRIBUTE_NAME), new TimeZoneField(TIMEZONE_ATTRIBUTE_NAME) )); /** * All possible attributes of user objects organized as individual, * logical forms. */ public static final Collection ATTRIBUTES = Collections.unmodifiableCollection(Arrays.asList( ACCOUNT_RESTRICTIONS )); /** * Service for hashing passwords. */ @Inject private PasswordEncryptionService encryptionService; /** * Service for providing secure, random salts. */ @Inject private SaltService saltService; /** * Service for retrieving system permissions. */ @Inject private SystemPermissionService systemPermissionService; /** * Service for retrieving connection permissions. */ @Inject private ConnectionPermissionService connectionPermissionService; /** * Service for retrieving connection group permissions. */ @Inject private ConnectionGroupPermissionService connectionGroupPermissionService; /** * Service for retrieving active connection permissions. */ @Inject private ActiveConnectionPermissionService activeConnectionPermissionService; /** * Service for retrieving user permissions. */ @Inject private UserPermissionService userPermissionService; /** * The plaintext password previously set by a call to setPassword(), if * any. The password of a user cannot be retrieved once saved into the * database, so this serves to ensure getPassword() returns a reasonable * value if setPassword() is called. If no password has been set, or the * user was retrieved from the database, this will be null. */ private String password = null; /** * Creates a new, empty ModeledUser. */ public ModeledUser() { } @Override public String getPassword() { return password; } @Override public void setPassword(String password) { UserModel userModel = getModel(); // Store plaintext password internally this.password = password; // If no password provided, clear password salt and hash if (password == null) { userModel.setPasswordSalt(null); userModel.setPasswordHash(null); } // Otherwise generate new salt and hash given password using newly-generated salt else { byte[] salt = saltService.generateSalt(); byte[] hash = encryptionService.createPasswordHash(password, salt); // Set stored salt and hash userModel.setPasswordSalt(salt); userModel.setPasswordHash(hash); } } /** * Returns whether this user is a system administrator, and thus is not * restricted by permissions. * * @return * true if this user is a system administrator, false otherwise. * * @throws GuacamoleException * If an error occurs while determining the user's system administrator * status. */ public boolean isAdministrator() throws GuacamoleException { SystemPermissionSet systemPermissionSet = getSystemPermissions(); return systemPermissionSet.hasPermission(SystemPermission.Type.ADMINISTER); } @Override public SystemPermissionSet getSystemPermissions() throws GuacamoleException { return systemPermissionService.getPermissionSet(getCurrentUser(), this); } @Override public ObjectPermissionSet getConnectionPermissions() throws GuacamoleException { return connectionPermissionService.getPermissionSet(getCurrentUser(), this); } @Override public ObjectPermissionSet getConnectionGroupPermissions() throws GuacamoleException { return connectionGroupPermissionService.getPermissionSet(getCurrentUser(), this); } @Override public ObjectPermissionSet getActiveConnectionPermissions() throws GuacamoleException { return activeConnectionPermissionService.getPermissionSet(getCurrentUser(), this); } @Override public ObjectPermissionSet getUserPermissions() throws GuacamoleException { return userPermissionService.getPermissionSet(getCurrentUser(), this); } @Override public Map getAttributes() { Map attributes = new HashMap(); // Set disabled attribute attributes.put(DISABLED_ATTRIBUTE_NAME, getModel().isDisabled() ? "true" : null); // Set password expired attribute attributes.put(EXPIRED_ATTRIBUTE_NAME, getModel().isExpired() ? "true" : null); // Set access window start time attributes.put(ACCESS_WINDOW_START_ATTRIBUTE_NAME, TimeField.format(getModel().getAccessWindowStart())); // Set access window end time attributes.put(ACCESS_WINDOW_END_ATTRIBUTE_NAME, TimeField.format(getModel().getAccessWindowEnd())); // Set account validity start date attributes.put(VALID_FROM_ATTRIBUTE_NAME, DateField.format(getModel().getValidFrom())); // Set account validity end date attributes.put(VALID_UNTIL_ATTRIBUTE_NAME, DateField.format(getModel().getValidUntil())); // Set timezone attribute attributes.put(TIMEZONE_ATTRIBUTE_NAME, getModel().getTimeZone()); return attributes; } /** * Parses the given string into a corresponding date. The string must * follow the standard format used by date attributes, as defined by * DateField.FORMAT and as would be produced by DateField.format(). * * @param dateString * The date string to parse, which may be null. * * @return * The date corresponding to the given date string, or null if the * provided date string was null or blank. * * @throws ParseException * If the given date string does not conform to the standard format * used by date attributes. */ private Date parseDate(String dateString) throws ParseException { // Return null if no date provided java.util.Date parsedDate = DateField.parse(dateString); if (parsedDate == null) return null; // Convert to SQL Date return new Date(parsedDate.getTime()); } /** * Parses the given string into a corresponding time. The string must * follow the standard format used by time attributes, as defined by * TimeField.FORMAT and as would be produced by TimeField.format(). * * @param timeString * The time string to parse, which may be null. * * @return * The time corresponding to the given time string, or null if the * provided time string was null or blank. * * @throws ParseException * If the given time string does not conform to the standard format * used by time attributes. */ private Time parseTime(String timeString) throws ParseException { // Return null if no time provided java.util.Date parsedDate = TimeField.parse(timeString); if (parsedDate == null) return null; // Convert to SQL Time return new Time(parsedDate.getTime()); } @Override public void setAttributes(Map attributes) { // Translate disabled attribute getModel().setDisabled("true".equals(attributes.get(DISABLED_ATTRIBUTE_NAME))); // Translate password expired attribute getModel().setExpired("true".equals(attributes.get(EXPIRED_ATTRIBUTE_NAME))); // Translate access window start time try { getModel().setAccessWindowStart(parseTime(attributes.get(ACCESS_WINDOW_START_ATTRIBUTE_NAME))); } catch (ParseException e) { logger.warn("Not setting start time of user access window: {}", e.getMessage()); logger.debug("Unable to parse time attribute.", e); } // Translate access window end time try { getModel().setAccessWindowEnd(parseTime(attributes.get(ACCESS_WINDOW_END_ATTRIBUTE_NAME))); } catch (ParseException e) { logger.warn("Not setting end time of user access window: {}", e.getMessage()); logger.debug("Unable to parse time attribute.", e); } // Translate account validity start date try { getModel().setValidFrom(parseDate(attributes.get(VALID_FROM_ATTRIBUTE_NAME))); } catch (ParseException e) { logger.warn("Not setting user validity start date: {}", e.getMessage()); logger.debug("Unable to parse date attribute.", e); } // Translate account validity end date try { getModel().setValidUntil(parseDate(attributes.get(VALID_UNTIL_ATTRIBUTE_NAME))); } catch (ParseException e) { logger.warn("Not setting user validity end date: {}", e.getMessage()); logger.debug("Unable to parse date attribute.", e); } // Translate timezone attribute getModel().setTimeZone(TimeZoneField.parse(attributes.get(TIMEZONE_ATTRIBUTE_NAME))); } /** * Returns the time zone associated with this user. This time zone must be * used when interpreting all date/time restrictions related to this user. * * @return * The time zone associated with this user. */ private TimeZone getTimeZone() { // If no time zone is set, use the default String timeZone = getModel().getTimeZone(); if (timeZone == null) return TimeZone.getDefault(); // Otherwise parse and return time zone return TimeZone.getTimeZone(timeZone); } /** * Converts a SQL Time to a Calendar, independently of time zone, using the * given Calendar as a base. The time components will be copied to the * given Calendar verbatim, leaving the date and time zone components of * the given Calendar otherwise intact. * * @param base * The Calendar object to use as a base for the conversion. * * @param time * The SQL Time object containing the time components to be applied to * the given Calendar. * * @return * The given Calendar, now modified to represent the given time. */ private Calendar asCalendar(Calendar base, Time time) { // Get calendar from given SQL time Calendar timeCalendar = Calendar.getInstance(); timeCalendar.setTime(time); // Apply given time to base calendar base.set(Calendar.HOUR_OF_DAY, timeCalendar.get(Calendar.HOUR_OF_DAY)); base.set(Calendar.MINUTE, timeCalendar.get(Calendar.MINUTE)); base.set(Calendar.SECOND, timeCalendar.get(Calendar.SECOND)); base.set(Calendar.MILLISECOND, timeCalendar.get(Calendar.MILLISECOND)); return base; } /** * Returns the time during the current day when this user account can start * being used. * * @return * The time during the current day when this user account can start * being used. */ private Calendar getAccessWindowStart() { // Get window start time Time start = getModel().getAccessWindowStart(); if (start == null) return null; // Return within defined time zone, current day return asCalendar(Calendar.getInstance(getTimeZone()), start); } /** * Returns the time during the current day when this user account can no * longer be used. * * @return * The time during the current day when this user account can no longer * be used. */ private Calendar getAccessWindowEnd() { // Get window end time Time end = getModel().getAccessWindowEnd(); if (end == null) return null; // Return within defined time zone, current day return asCalendar(Calendar.getInstance(getTimeZone()), end); } /** * Returns the date after which this account becomes valid. The time * components of the resulting Calendar object will be set to midnight of * the date in question. * * @return * The date after which this account becomes valid. */ private Calendar getValidFrom() { // Get valid from date Date validFrom = getModel().getValidFrom(); if (validFrom == null) return null; // Convert to midnight within defined time zone Calendar validFromCalendar = Calendar.getInstance(getTimeZone()); validFromCalendar.setTime(validFrom); validFromCalendar.set(Calendar.HOUR_OF_DAY, 0); validFromCalendar.set(Calendar.MINUTE, 0); validFromCalendar.set(Calendar.SECOND, 0); validFromCalendar.set(Calendar.MILLISECOND, 0); return validFromCalendar; } /** * Returns the date after which this account becomes invalid. The time * components of the resulting Calendar object will be set to the last * millisecond of the day in question (23:59:59.999). * * @return * The date after which this account becomes invalid. */ private Calendar getValidUntil() { // Get valid until date Date validUntil = getModel().getValidUntil(); if (validUntil == null) return null; // Convert to end-of-day within defined time zone Calendar validUntilCalendar = Calendar.getInstance(getTimeZone()); validUntilCalendar.setTime(validUntil); validUntilCalendar.set(Calendar.HOUR_OF_DAY, 23); validUntilCalendar.set(Calendar.MINUTE, 59); validUntilCalendar.set(Calendar.SECOND, 59); validUntilCalendar.set(Calendar.MILLISECOND, 999); return validUntilCalendar; } /** * Given a time when a particular state changes from inactive to active, * and a time when a particular state changes from active to inactive, * determines whether that state is currently active. * * @param activeStart * The time at which the state changes from inactive to active. * * @param inactiveStart * The time at which the state changes from active to inactive. * * @return * true if the state is currently active, false otherwise. */ private boolean isActive(Calendar activeStart, Calendar inactiveStart) { // If end occurs before start, convert to equivalent case where start // start is before end if (inactiveStart != null && activeStart != null && inactiveStart.before(activeStart)) return !isActive(inactiveStart, activeStart); // Get current time Calendar current = Calendar.getInstance(); // State is active iff the current time is between the start and end return !(activeStart != null && current.before(activeStart)) && !(inactiveStart != null && current.after(inactiveStart)); } /** * Returns whether this user account is currently valid as of today. * Account validity depends on optional date-driven restrictions which * define when an account becomes valid, and when an account ceases being * valid. * * @return * true if the account is valid as of today, false otherwise. */ public boolean isAccountValid() { return isActive(getValidFrom(), getValidUntil()); } /** * Returns whether the current time is within this user's allowed access * window. If the login times for this user are not limited, this will * return true. * * @return * true if the current time is within this user's allowed access * window, or if this user has no restrictions on login time, false * otherwise. */ public boolean isAccountAccessible() { return isActive(getAccessWindowStart(), getAccessWindowEnd()); } } ././@LongLink0100644 0000000 0000000 00000000234 12625262336 011641 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/user/UserMapper.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000003412 12625262336 035264 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.user; import org.glyptodon.guacamole.auth.jdbc.base.ModeledDirectoryObjectMapper; import org.apache.ibatis.annotations.Param; /** * Mapper for user objects. * * @author Michael Jumper */ public interface UserMapper extends ModeledDirectoryObjectMapper { /** * Returns the user having the given username, if any. If no such user * exists, null is returned. * * @param username * The username of the user to return. * * @return * The user having the given username, or null if no such user exists. */ UserModel selectOne(@Param("username") String username); }././@LongLink0100644 0000000 0000000 00000000242 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/security/package-info.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000002310 12612226251 035250 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes related to hashing or encryption. */ package org.glyptodon.guacamole.auth.jdbc.security; ././@LongLink0100644 0000000 0000000 00000000255 12612226251 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/security/SecureRandomSaltService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000003147 12612226251 035261 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.security; import java.security.SecureRandom; /** * Generates password salts via SecureRandom. * @author James Muehlner */ public class SecureRandomSaltService implements SaltService { /** * Instance of SecureRandom for generating the salt. */ private SecureRandom secureRandom = new SecureRandom(); @Override public byte[] generateSalt() { byte[] salt = new byte[32]; secureRandom.nextBytes(salt); return salt; } } ././@LongLink0100644 0000000 0000000 00000000241 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/security/SaltService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000002650 12612226251 035257 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.security; /** * A service to generate password salts. * @author James Muehlner */ public interface SaltService { /** * Generates a new String that can be used as a password salt. * @return a new salt for password encryption. */ public byte[] generateSalt(); } ././@LongLink0100644 0000000 0000000 00000000257 12612226251 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/security/PasswordEncryptionService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000003346 12612226251 035262 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.security; /** * A service to perform password encryption and checking. * @author James Muehlner */ public interface PasswordEncryptionService { /** * Creates a password hash based on the provided username, password, and * salt. If the provided salt is null, only the password itself is hashed. * * @param password * The password to hash. * * @param salt * The salt to use when hashing the password, if any. * * @return * The generated password hash. */ public byte[] createPasswordHash(String password, byte[] salt); } ././@LongLink0100644 0000000 0000000 00000000265 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/security/SHA256PasswordEncryptionService.javaguacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java0100664 0001751 0001751 00000004727 12612226251 035266 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.jdbc.security; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import javax.xml.bind.DatatypeConverter; /** * Provides a SHA-256 based implementation of the password encryption functionality. * @author James Muehlner */ public class SHA256PasswordEncryptionService implements PasswordEncryptionService { @Override public byte[] createPasswordHash(String password, byte[] salt) { try { // Build salted password, if a salt was provided StringBuilder builder = new StringBuilder(); builder.append(password); if (salt != null) builder.append(DatatypeConverter.printHexBinary(salt)); // Hash UTF-8 bytes of possibly-salted password MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update(builder.toString().getBytes("UTF-8")); return md.digest(); } // Throw hard errors if standard pieces of Java are missing catch (UnsupportedEncodingException e) { throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e); } catch (NoSuchAlgorithmException e) { throw new UnsupportedOperationException("Unexpected lack of SHA-256 support.", e); } } } guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml0100664 0001751 0001751 00000006130 12645510255 034220 0ustar00mjumpermjumper0000000 0000000 4.0.0 org.glyptodon.guacamole guacamole-auth-jdbc-base jar guacamole-auth-jdbc-base http://guac-dev.org/ UTF-8 org.glyptodon.guacamole guacamole-auth-jdbc 0.9.9 ../../ org.apache.maven.plugins maven-compiler-plugin 3.3 1.6 1.6 -Xlint:all -Werror true javax.servlet servlet-api 2.5 provided org.glyptodon.guacamole guacamole-ext provided org.slf4j slf4j-api 1.7.7 org.mybatis mybatis 3.2.8 org.mybatis mybatis-guice 3.6 com.google.inject guice 3.0 com.google.inject.extensions guice-multibindings 3.0 com.google.guava guava 18.0 guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/pom.xml0100664 0001751 0001751 00000004524 12645510255 026051 0ustar00mjumpermjumper0000000 0000000 4.0.0 org.glyptodon.guacamole guacamole-auth-jdbc pom 0.9.9 guacamole-auth-jdbc http://guac-dev.org/ UTF-8 modules/guacamole-auth-jdbc-base modules/guacamole-auth-jdbc-mysql modules/guacamole-auth-jdbc-postgresql maven-assembly-plugin 2.5.3 false make-dist-archive package single ${project.artifactId}-${project.version} false src/main/assembly/dist.xml org.glyptodon.guacamole guacamole-ext 0.9.9 provided guacamole-client-0.9.9/extensions/guacamole-auth-jdbc/LICENSE0100664 0001751 0001751 00000002041 12612226251 025523 0ustar00mjumpermjumper0000000 0000000 Copyright (C) 2013 Glyptodon LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. guacamole-client-0.9.9/extensions/guacamole-auth-ldap/README0100664 0001751 0001751 00000007063 12471273272 025435 0ustar00mjumpermjumper0000000 0000000 ------------------------------------------------------------ About this README ------------------------------------------------------------ This README is intended to provide quick and to-the-point documentation for technical users intending to compile parts of Guacamole themselves. Distribution-specific packages are available from the files section of the main project page: http://sourceforge.net/projects/guacamole/files/ Distribution-specific documentation is provided on the Guacamole wiki: http://guac-dev.org/ ------------------------------------------------------------ What is guacamole-auth-ldap? ------------------------------------------------------------ guacamole-auth-ldap is a Java library for use with the Guacamole web application to provide LDAP based authentication. guacamole-auth-ldap provides an authentication provider which can be set in guacamole.properties to allow LDAP authentication of Guacamole users. Additional properties are required to configure the LDAP connection and search parameters. Schema files are provided to create the required object classes in your LDAP directory. ------------------------------------------------------------ Compiling and installing guacamole-auth-ldap ------------------------------------------------------------ guacamole-auth-ldap is built using Maven. Building guacamole-auth-ldap compiles all classes and packages them into a redistributable .jar file. This .jar file can be installed in the library directory configured in guacamole.properties such that the authentication provider is available. 1) Run mvn package $ mvn package Maven will download any needed dependencies for building the .jar file. Once all dependencies have been downloaded, the .jar file will be created in the target/ subdirectory of the current directory. 2) Copy the .jar file into the library directory specified in your guacamole.properties You will likely need to do this as root. If you do not have a library directory configured in your guacamole.properties, you will need to specify one. The directory is specified using the "lib-directory" property. 3) Set up your LDAP server to authenticate Guacamole users Schema files are provided in the doc directory for creating the "guacConfig" object class required. You must add guacConfig objects to your LDAP directory. Each guacConfig represents a connection configuration, which is made up of a protocol and any number of protocol-specific parameters. 4) Configure guacamole.properties for LDAP There are additional properties required by the LDAP authentication provider which must be added/changed in your guacamole.properties: # Use LDAP authentication auth-provider: net.sourceforge.guacamole.net.auth.ldap.LDAPAuthenticationProvider # Configuration for LDAP connection ldap-hostname: LDAP_SERVER_HOSTNAME ldap-port: 389 # The attribute which uniquely identifies users ldap-username-attribute: uid # The base DN which, when appended to the user identifier attribute, # produces the full DN of the user being authenticated. ldap-user-base-dn: ou=people,dc=example,dc=net # The base DN within which all guacConfig objects can be found. ldap-config-base-dn: dc=example,dc=net ------------------------------------------------------------ Reporting problems ------------------------------------------------------------ Please report any bugs encountered by opening a new ticket at the Trac system hosted at: http://guac-dev.org/trac/ guacamole-client-0.9.9/extensions/guacamole-auth-ldap/doc/examples/exampleConfigGroup.ldif0100664 0001751 0001751 00000000476 12471273272 033577 0ustar00mjumpermjumper0000000 0000000 dn: cn=Example Config,dc=guac-dev,dc=org objectClass: guacConfigGroup objectClass: groupOfNames cn: Example Config guacConfigProtocol: vnc guacConfigParameter: hostname=localhost guacConfigParameter: port=5900 guacConfigParameter: password=secret member: cn=user1,dc=example,dc=com member: cn=user2,dc=example,dc=com guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/resources/translations/en.json0100664 0001751 0001751 00000000077 12612226251 034306 0ustar00mjumpermjumper0000000 0000000 { "DATA_SOURCE_LDAP" : { "NAME" : "LDAP" } } guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/resources/guac-manifest.json0100664 0001751 0001751 00000000433 12645510255 033710 0ustar00mjumpermjumper0000000 0000000 { "guacamoleVersion" : "0.9.9", "name" : "LDAP Authentication", "namespace" : "guac-ldap", "authProviders" : [ "net.sourceforge.guacamole.net.auth.ldap.LDAPAuthenticationProvider" ], "translations" : [ "translations/en.json" ] } guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/assembly/dist.xml0100664 0001751 0001751 00000002124 12612226251 031555 0ustar00mjumpermjumper0000000 0000000 dist ${project.artifactId}-${project.version} tar.gz doc schema schema target *.jar ././@LongLink0100644 0000000 0000000 00000000200 12645510255 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/ConfigurationService.javaguacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/lda0100664 0001751 0001751 00000015277 12645510255 035404 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.ldap; import com.google.inject.Inject; import java.util.Collections; import java.util.List; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.environment.Environment; /** * Service for retrieving configuration information regarding the LDAP server. * * @author Michael Jumper */ public class ConfigurationService { /** * The Guacamole server environment. */ @Inject private Environment environment; /** * Returns the hostname of the LDAP server as configured with * guacamole.properties. By default, this will be "localhost". * * @return * The hostname of the LDAP server, as configured with * guacamole.properties. * * @throws GuacamoleException * If guacamole.properties cannot be parsed. */ public String getServerHostname() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_HOSTNAME, "localhost" ); } /** * Returns the port of the LDAP server configured with * guacamole.properties. The default value depends on which encryption * method is being used. For unencrypted LDAP and STARTTLS, this will be * 389. For LDAPS (LDAP over SSL) this will be 636. * * @return * The port of the LDAP server, as configured with * guacamole.properties. * * @throws GuacamoleException * If guacamole.properties cannot be parsed. */ public int getServerPort() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_PORT, getEncryptionMethod().DEFAULT_PORT ); } /** * Returns all username attributes which should be used to query and bind * users using the LDAP directory. By default, this will be "uid" - a * common attribute used for this purpose. * * @return * The username attributes which should be used to query and bind users * using the LDAP directory. * * @throws GuacamoleException * If guacamole.properties cannot be parsed. */ public List getUsernameAttributes() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_USERNAME_ATTRIBUTE, Collections.singletonList("uid") ); } /** * Returns the base DN under which all Guacamole users will be stored * within the LDAP directory. * * @return * The base DN under which all Guacamole users will be stored within * the LDAP directory. * * @throws GuacamoleException * If guacamole.properties cannot be parsed, or if the user base DN * property is not specified. */ public String getUserBaseDN() throws GuacamoleException { return environment.getRequiredProperty( LDAPGuacamoleProperties.LDAP_USER_BASE_DN ); } /** * Returns the base DN under which all Guacamole configurations * (connections) will be stored within the LDAP directory. If Guacamole * configurations will not be stored within LDAP, null is returned. * * @return * The base DN under which all Guacamole configurations will be stored * within the LDAP directory, or null if no Guacamole configurations * will be stored within the LDAP directory. * * @throws GuacamoleException * If guacamole.properties cannot be parsed. */ public String getConfigurationBaseDN() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN ); } /** * Returns the DN that should be used when searching for the DNs of users * attempting to authenticate. If no such search should be performed, null * is returned. * * @return * The DN that should be used when searching for the DNs of users * attempting to authenticate, or null if no such search should be * performed. * * @throws GuacamoleException * If guacamole.properties cannot be parsed. */ public String getSearchBindDN() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_SEARCH_BIND_DN ); } /** * Returns the password that should be used when binding to the LDAP server * using the DN returned by getSearchBindDN(). If no password should be * used, null is returned. * * @return * The password that should be used when binding to the LDAP server * using the DN returned by getSearchBindDN(), or null if no password * should be used. * * @throws GuacamoleException * If guacamole.properties cannot be parsed. */ public String getSearchBindPassword() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_SEARCH_BIND_PASSWORD ); } /** * Returns the encryption method that should be used when connecting to the * LDAP server. By default, no encryption is used. * * @return * The encryption method that should be used when connecting to the * LDAP server. * * @throws GuacamoleException * If guacamole.properties cannot be parsed. */ public EncryptionMethod getEncryptionMethod() throws GuacamoleException { return environment.getProperty( LDAPGuacamoleProperties.LDAP_ENCRYPTION_METHOD, EncryptionMethod.NONE ); } } ././@LongLink0100644 0000000 0000000 00000000173 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/EscapingService.javaguacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/lda0100664 0001751 0001751 00000010440 12612226251 035361 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.ldap; /** * Service for escaping LDAP filters, distinguished names (DN's), etc. * * @author Michael Jumper */ public class EscapingService { /** * Escapes the given string for use within an LDAP search filter. This * implementation is provided courtesy of OWASP: * * https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java * * @param filter * The string to escape such that it has no special meaning within an * LDAP search filter. * * @return * The escaped string, safe for use within an LDAP search filter. */ public String escapeLDAPSearchFilter(String filter) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < filter.length(); i++) { char curChar = filter.charAt(i); switch (curChar) { case '\\': sb.append("\\5c"); break; case '*': sb.append("\\2a"); break; case '(': sb.append("\\28"); break; case ')': sb.append("\\29"); break; case '\u0000': sb.append("\\00"); break; default: sb.append(curChar); } } return sb.toString(); } /** * Escapes the given string such that it is safe for use within an LDAP * distinguished name (DN). This implementation is provided courtesy of * OWASP: * * https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java * * @param name * The string to escape such that it has no special meaning within an * LDAP DN. * * @return * The escaped string, safe for use within an LDAP DN. */ public String escapeDN(String name) { StringBuilder sb = new StringBuilder(); if ((name.length() > 0) && ((name.charAt(0) == ' ') || (name.charAt(0) == '#'))) { sb.append('\\'); // add the leading backslash if needed } for (int i = 0; i < name.length(); i++) { char curChar = name.charAt(i); switch (curChar) { case '\\': sb.append("\\\\"); break; case ',': sb.append("\\,"); break; case '+': sb.append("\\+"); break; case '"': sb.append("\\\""); break; case '<': sb.append("\\<"); break; case '>': sb.append("\\>"); break; case ';': sb.append("\\;"); break; default: sb.append(curChar); } } if ((name.length() > 1) && (name.charAt(name.length() - 1) == ' ')) { sb.insert(sb.length() - 1, '\\'); // add the trailing backslash if needed } return sb.toString(); } } ././@LongLink0100644 0000000 0000000 00000000211 12645510255 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/AuthenticationProviderService.javaguacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/lda0100664 0001751 0001751 00000022336 12645510255 035376 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.ldap; import com.google.inject.Inject; import com.google.inject.Provider; import com.novell.ldap.LDAPConnection; import java.util.List; import org.glyptodon.guacamole.auth.ldap.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.ldap.user.UserContext; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.auth.ldap.user.UserService; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo; import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Service providing convenience functions for the LDAP AuthenticationProvider * implementation. * * @author Michael Jumper */ public class AuthenticationProviderService { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(AuthenticationProviderService.class); /** * Service for creating and managing connections to LDAP servers. */ @Inject private LDAPConnectionService ldapService; /** * Service for retrieving LDAP server configuration information. */ @Inject private ConfigurationService confService; /** * Service for retrieving users and their corresponding LDAP DNs. */ @Inject private UserService userService; /** * Provider for AuthenticatedUser objects. */ @Inject private Provider authenticatedUserProvider; /** * Provider for UserContext objects. */ @Inject private Provider userContextProvider; /** * Determines the DN which corresponds to the user having the given * username. The DN will either be derived directly from the user base DN, * or queried from the LDAP server, depending on how LDAP authentication * has been configured. * * @param username * The username of the user whose corresponding DN should be returned. * * @return * The DN which corresponds to the user having the given username. * * @throws GuacamoleException * If required properties are missing, and thus the user DN cannot be * determined. */ private String getUserBindDN(String username) throws GuacamoleException { // If a search DN is provided, search the LDAP directory for the DN // corresponding to the given username String searchBindDN = confService.getSearchBindDN(); if (searchBindDN != null) { // Create an LDAP connection using the search account LDAPConnection searchConnection = ldapService.bindAs( searchBindDN, confService.getSearchBindPassword() ); // Warn of failure to find if (searchConnection == null) { logger.error("Unable to bind using search DN \"{}\"", searchBindDN); return null; } try { // Retrieve all DNs associated with the given username List userDNs = userService.getUserDNs(searchConnection, username); if (userDNs.isEmpty()) return null; // Warn if multiple DNs exist for the same user if (userDNs.size() != 1) { logger.warn("Multiple DNs possible for user \"{}\": {}", username, userDNs); return null; } // Return the single possible DN return userDNs.get(0); } // Always disconnect finally { ldapService.disconnect(searchConnection); } } // Otherwise, derive user DN from base DN return userService.deriveUserDN(username); } /** * Binds to the LDAP server using the provided Guacamole credentials. The * DN of the user is derived using the LDAP configuration properties * provided in guacamole.properties, as is the server hostname and port * information. * * @param credentials * The credentials to use to bind to the LDAP server. * * @return * A bound LDAP connection, or null if the connection could not be * bound. * * @throws GuacamoleException * If an error occurs while binding to the LDAP server. */ private LDAPConnection bindAs(Credentials credentials) throws GuacamoleException { // Get username and password from credentials String username = credentials.getUsername(); String password = credentials.getPassword(); // Require username if (username == null || username.isEmpty()) { logger.debug("Anonymous bind is not currently allowed by the LDAP authentication provider."); return null; } // Require password, and do not allow anonymous binding if (password == null || password.isEmpty()) { logger.debug("Anonymous bind is not currently allowed by the LDAP authentication provider."); return null; } // Determine user DN String userDN = getUserBindDN(username); if (userDN == null) { logger.debug("Unable to determine DN for user \"{}\".", username); return null; } // Bind using user's DN return ldapService.bindAs(userDN, password); } /** * Returns an AuthenticatedUser representing the user authenticated by the * given credentials. * * @param credentials * The credentials to use for authentication. * * @return * An AuthenticatedUser representing the user authenticated by the * given credentials. * * @throws GuacamoleException * If an error occurs while authenticating the user, or if access is * denied. */ public AuthenticatedUser authenticateUser(Credentials credentials) throws GuacamoleException { // Attempt bind LDAPConnection ldapConnection; try { ldapConnection = bindAs(credentials); } catch (GuacamoleException e) { logger.error("Cannot bind with LDAP server: {}", e.getMessage()); logger.debug("Error binding with LDAP server.", e); ldapConnection = null; } // If bind fails, permission to login is denied if (ldapConnection == null) throw new GuacamoleInvalidCredentialsException("Permission denied.", CredentialsInfo.USERNAME_PASSWORD); try { // Return AuthenticatedUser if bind succeeds AuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); authenticatedUser.init(credentials); return authenticatedUser; } // Always disconnect finally { ldapService.disconnect(ldapConnection); } } /** * Returns a UserContext object initialized with data accessible to the * given AuthenticatedUser. * * @param authenticatedUser * The AuthenticatedUser to retrieve data for. * * @return * A UserContext object initialized with data accessible to the given * AuthenticatedUser. * * @throws GuacamoleException * If the UserContext cannot be created due to an error. */ public UserContext getUserContext(org.glyptodon.guacamole.net.auth.AuthenticatedUser authenticatedUser) throws GuacamoleException { // Bind using credentials associated with AuthenticatedUser Credentials credentials = authenticatedUser.getCredentials(); LDAPConnection ldapConnection = bindAs(credentials); if (ldapConnection == null) return null; try { // Build user context by querying LDAP UserContext userContext = userContextProvider.get(); userContext.init(authenticatedUser, ldapConnection); return userContext; } // Always disconnect finally { ldapService.disconnect(ldapConnection); } } } ././@LongLink0100644 0000000 0000000 00000000174 12645510255 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/EncryptionMethod.javaguacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/lda0100664 0001751 0001751 00000004570 12645510255 035376 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.ldap; /** * All possible encryption methods which may be used when connecting to an LDAP * server. * * @author Michael Jumper */ public enum EncryptionMethod { /** * No encryption will be used. All data will be sent to the LDAP server in * plaintext. Unencrypted LDAP connections use port 389 by default. */ NONE(389), /** * The connection to the LDAP server will be encrypted with SSL. LDAP over * SSL (LDAPS) will use port 636 by default. */ SSL(636), /** * The connection to the LDAP server will be encrypted using STARTTLS. TLS * connections are negotiated over the standard LDAP port of 389 - the same * port used for unencrypted traffic. */ STARTTLS(389); /** * The default port of this specific encryption method. As with most * protocols, the default port for LDAP varies by whether SSL is used. */ public final int DEFAULT_PORT; /** * Initializes this encryption method such that it is associated with the * given default port. * * @param defaultPort * The default port to associate with this encryption method. */ private EncryptionMethod(int defaultPort) { this.DEFAULT_PORT = defaultPort; } } ././@LongLink0100644 0000000 0000000 00000000210 12645510255 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/connection/ConnectionService.javaguacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/lda0100664 0001751 0001751 00000016744 12645510255 035404 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.ldap.connection; import com.google.inject.Inject; import com.novell.ldap.LDAPAttribute; import com.novell.ldap.LDAPConnection; import com.novell.ldap.LDAPEntry; import com.novell.ldap.LDAPException; import com.novell.ldap.LDAPSearchResults; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import net.sourceforge.guacamole.net.auth.ldap.LDAPAuthenticationProvider; import org.glyptodon.guacamole.auth.ldap.ConfigurationService; import org.glyptodon.guacamole.auth.ldap.EscapingService; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleServerException; import org.glyptodon.guacamole.net.auth.AuthenticatedUser; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.simple.SimpleConnection; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; import org.glyptodon.guacamole.token.StandardTokens; import org.glyptodon.guacamole.token.TokenFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Service for querying the connections available to a particular Guacamole * user according to an LDAP directory. * * @author Michael Jumper */ public class ConnectionService { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(ConnectionService.class); /** * Service for escaping parts of LDAP queries. */ @Inject private EscapingService escapingService; /** * Service for retrieving LDAP server configuration information. */ @Inject private ConfigurationService confService; /** * Returns all Guacamole connections accessible to the user currently bound * under the given LDAP connection. * * @param user * The AuthenticatedUser object associated with the user who is * currently authenticated with Guacamole. * * @param ldapConnection * The current connection to the LDAP server, associated with the * current user. * * @return * All connections accessible to the user currently bound under the * given LDAP connection, as a map of connection identifier to * corresponding connection object. * * @throws GuacamoleException * If an error occurs preventing retrieval of connections. */ public Map getConnections(AuthenticatedUser user, LDAPConnection ldapConnection) throws GuacamoleException { // Do not return any connections if base DN is not specified String configurationBaseDN = confService.getConfigurationBaseDN(); if (configurationBaseDN == null) return Collections.emptyMap(); try { // Pull the current user DN from the LDAP connection String userDN = ldapConnection.getAuthenticationDN(); // getConnections() will only be called after a connection has been // authenticated (via non-anonymous bind), thus userDN cannot // possibly be null assert(userDN != null); // Find all Guacamole connections for the given user LDAPSearchResults results = ldapConnection.search( configurationBaseDN, LDAPConnection.SCOPE_SUB, "(&(objectClass=guacConfigGroup)(member=" + escapingService.escapeLDAPSearchFilter(userDN) + "))", null, false ); // Build token filter containing credential tokens TokenFilter tokenFilter = new TokenFilter(); StandardTokens.addStandardTokens(tokenFilter, user.getCredentials()); // Produce connections for each readable configuration Map connections = new HashMap(); while (results.hasMore()) { LDAPEntry entry = results.next(); // Get common name (CN) LDAPAttribute cn = entry.getAttribute("cn"); if (cn == null) { logger.warn("guacConfigGroup is missing a cn."); continue; } // Get associated protocol LDAPAttribute protocol = entry.getAttribute("guacConfigProtocol"); if (protocol == null) { logger.warn("guacConfigGroup \"{}\" is missing the " + "required \"guacConfigProtocol\" attribute.", cn.getStringValue()); continue; } // Set protocol GuacamoleConfiguration config = new GuacamoleConfiguration(); config.setProtocol(protocol.getStringValue()); // Get parameters, if any LDAPAttribute parameterAttribute = entry.getAttribute("guacConfigParameter"); if (parameterAttribute != null) { // For each parameter Enumeration parameters = parameterAttribute.getStringValues(); while (parameters.hasMoreElements()) { String parameter = (String) parameters.nextElement(); // Parse parameter int equals = parameter.indexOf('='); if (equals != -1) { // Parse name String name = parameter.substring(0, equals); String value = parameter.substring(equals+1); config.setParameter(name, value); } } } // Filter the configuration, substituting all defined tokens tokenFilter.filterValues(config.getParameters()); // Store connection using cn for both identifier and name String name = cn.getStringValue(); Connection connection = new SimpleConnection(name, name, config); connection.setParentIdentifier(LDAPAuthenticationProvider.ROOT_CONNECTION_GROUP); connections.put(name, connection); } // Return map of all connections return connections; } catch (LDAPException e) { throw new GuacamoleServerException("Error while querying for connections.", e); } } } ././@LongLink0100644 0000000 0000000 00000000204 12645510255 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/EncryptionMethodProperty.javaguacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/lda0100664 0001751 0001751 00000004520 12645510255 035371 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.ldap; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleServerException; import org.glyptodon.guacamole.properties.GuacamoleProperty; /** * A GuacamoleProperty whose value is an EncryptionMethod. The string values * "none", "ssl", and "starttls" are each parsed to their corresponding values * within the EncryptionMethod enum. All other string values result in parse * errors. * * @author Michael Jumper */ public abstract class EncryptionMethodProperty implements GuacamoleProperty { @Override public EncryptionMethod parseValue(String value) throws GuacamoleException { // If no value provided, return null. if (value == null) return null; // Plaintext (no encryption) if (value.equals("none")) return EncryptionMethod.NONE; // SSL if (value.equals("ssl")) return EncryptionMethod.SSL; // STARTTLS if (value.equals("starttls")) return EncryptionMethod.STARTTLS; // The provided value is not legal throw new GuacamoleServerException("Encryption method must be one of \"none\", \"ssl\", or \"starttls\"."); } } ././@LongLink0100644 0000000 0000000 00000000214 12645510255 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/LDAPAuthenticationProviderModule.javaguacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/lda0100664 0001751 0001751 00000006165 12645510255 035400 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.ldap; import com.google.inject.AbstractModule; import org.glyptodon.guacamole.auth.ldap.connection.ConnectionService; import org.glyptodon.guacamole.auth.ldap.user.UserService; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.environment.LocalEnvironment; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; /** * Guice module which configures LDAP-specific injections. * * @author Michael Jumper */ public class LDAPAuthenticationProviderModule extends AbstractModule { /** * Guacamole server environment. */ private final Environment environment; /** * A reference to the LDAPAuthenticationProvider on behalf of which this * module has configured injection. */ private final AuthenticationProvider authProvider; /** * Creates a new LDAP authentication provider module which configures * injection for the LDAPAuthenticationProvider. * * @param authProvider * The AuthenticationProvider for which injection is being configured. * * @throws GuacamoleException * If an error occurs while retrieving the Guacamole server * environment. */ public LDAPAuthenticationProviderModule(AuthenticationProvider authProvider) throws GuacamoleException { // Get local environment this.environment = new LocalEnvironment(); // Store associated auth provider this.authProvider = authProvider; } @Override protected void configure() { // Bind core implementations of guacamole-ext classes bind(AuthenticationProvider.class).toInstance(authProvider); bind(Environment.class).toInstance(environment); // Bind LDAP-specific services bind(ConfigurationService.class); bind(ConnectionService.class); bind(EscapingService.class); bind(LDAPConnectionService.class); bind(UserService.class); } } ././@LongLink0100644 0000000 0000000 00000000176 12645510255 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/StringListProperty.javaguacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/lda0100664 0001751 0001751 00000005144 12645510255 035374 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.ldap; import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.properties.GuacamoleProperty; /** * A GuacamoleProperty whose value is a List of Strings. The string value * parsed to produce this list is a comma-delimited list. Duplicate values are * ignored, as is any whitespace following delimiters. To maintain * compatibility with the behavior of Java properties in general, only * whitespace at the beginning of each value is ignored; trailing whitespace * becomes part of the value. * * @author Michael Jumper */ public abstract class StringListProperty implements GuacamoleProperty> { /** * A pattern which matches against the delimiters between values. This is * currently simply a comma and any following whitespace. Parts of the * input string which match this pattern will not be included in the parsed * result. */ private static final Pattern DELIMITER_PATTERN = Pattern.compile(",\\s*"); @Override public List parseValue(String values) throws GuacamoleException { // If no property provided, return null. if (values == null) return null; // Split string into a list of individual values List stringValues = Arrays.asList(DELIMITER_PATTERN.split(values)); if (stringValues.isEmpty()) return null; return stringValues; } } ././@LongLink0100644 0000000 0000000 00000000201 12645510255 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/LDAPConnectionService.javaguacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/lda0100664 0001751 0001751 00000015154 12645510255 035376 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.ldap; import com.google.inject.Inject; import com.novell.ldap.LDAPConnection; import com.novell.ldap.LDAPException; import com.novell.ldap.LDAPJSSESecureSocketFactory; import com.novell.ldap.LDAPJSSEStartTLSFactory; import java.io.UnsupportedEncodingException; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleUnsupportedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Service for creating and managing connections to LDAP servers. * * @author Michael Jumper */ public class LDAPConnectionService { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(LDAPConnectionService.class); /** * Service for retrieving LDAP server configuration information. */ @Inject private ConfigurationService confService; /** * Creates a new instance of LDAPConnection, configured as required to use * whichever encryption method is requested within guacamole.properties. * * @return * A new LDAPConnection instance which has already been configured to * use the encryption method requested within guacamole.properties. * * @throws GuacamoleException * If an error occurs while parsing guacamole.properties, or if the * requested encryption method is actually not implemented (a bug). */ private LDAPConnection createLDAPConnection() throws GuacamoleException { // Map encryption method to proper connection and socket factory EncryptionMethod encryptionMethod = confService.getEncryptionMethod(); switch (encryptionMethod) { // Unencrypted LDAP connection case NONE: logger.debug("Connection to LDAP server without encryption."); return new LDAPConnection(); // LDAP over SSL (LDAPS) case SSL: logger.debug("Connecting to LDAP server using SSL/TLS."); return new LDAPConnection(new LDAPJSSESecureSocketFactory()); // LDAP + STARTTLS case STARTTLS: logger.debug("Connecting to LDAP server using STARTTLS."); return new LDAPConnection(new LDAPJSSEStartTLSFactory()); // The encryption method, though known, is not actually // implemented. If encountered, this would be a bug. default: throw new GuacamoleUnsupportedException("Unimplemented encryption method: " + encryptionMethod); } } /** * Binds to the LDAP server using the provided user DN and password. * * @param userDN * The DN of the user to bind as, or null to bind anonymously. * * @param password * The password to use when binding as the specified user, or null to * attempt to bind without a password. * * @return * A bound LDAP connection, or null if the connection could not be * bound. * * @throws GuacamoleException * If an error occurs while binding to the LDAP server. */ public LDAPConnection bindAs(String userDN, String password) throws GuacamoleException { // Obtain appropriately-configured LDAPConnection instance LDAPConnection ldapConnection = createLDAPConnection(); try { // Connect to LDAP server ldapConnection.connect( confService.getServerHostname(), confService.getServerPort() ); // Explicitly start TLS if requested if (confService.getEncryptionMethod() == EncryptionMethod.STARTTLS) ldapConnection.startTLS(); } catch (LDAPException e) { logger.error("Unable to connect to LDAP server: {}", e.getMessage()); logger.debug("Failed to connect to LDAP server.", e); return null; } // Bind using provided credentials try { byte[] passwordBytes; try { // Convert password into corresponding byte array if (password != null) passwordBytes = password.getBytes("UTF-8"); else passwordBytes = null; } catch (UnsupportedEncodingException e) { logger.error("Unexpected lack of support for UTF-8: {}", e.getMessage()); logger.debug("Support for UTF-8 (as required by Java spec) not found.", e); disconnect(ldapConnection); return null; } // Bind as user ldapConnection.bind(LDAPConnection.LDAP_V3, userDN, passwordBytes); } // Disconnect if an error occurs during bind catch (LDAPException e) { logger.debug("LDAP bind failed.", e); disconnect(ldapConnection); return null; } return ldapConnection; } /** * Disconnects the given LDAP connection, logging any failure to do so * appropriately. * * @param ldapConnection * The LDAP connection to disconnect. */ public void disconnect(LDAPConnection ldapConnection) { // Attempt disconnect try { ldapConnection.disconnect(); } // Warn if disconnect unexpectedly fails catch (LDAPException e) { logger.warn("Unable to disconnect from LDAP server: {}", e.getMessage()); logger.debug("LDAP disconnect failed.", e); } } } ././@LongLink0100644 0000000 0000000 00000000203 12645510255 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/LDAPGuacamoleProperties.javaguacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/lda0100664 0001751 0001751 00000011735 12645510255 035377 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.ldap; import org.glyptodon.guacamole.properties.IntegerGuacamoleProperty; import org.glyptodon.guacamole.properties.StringGuacamoleProperty; /** * Provides properties required for use of the LDAP authentication provider. * These properties will be read from guacamole.properties when the LDAP * authentication provider is used. * * @author Michael Jumper */ public class LDAPGuacamoleProperties { /** * This class should not be instantiated. */ private LDAPGuacamoleProperties() {} /** * The base DN to search for Guacamole configurations. */ public static final StringGuacamoleProperty LDAP_CONFIG_BASE_DN = new StringGuacamoleProperty() { @Override public String getName() { return "ldap-config-base-dn"; } }; /** * The base DN of users. All users must be contained somewhere within the * subtree of this DN. If the LDAP authentication will not be given its own * credentials for querying other LDAP users, all users must be direct * children of this base DN, varying only by LDAP_USERNAME_ATTRIBUTE. */ public static final StringGuacamoleProperty LDAP_USER_BASE_DN = new StringGuacamoleProperty() { @Override public String getName() { return "ldap-user-base-dn"; } }; /** * The attribute or attributes which identify users. One of these * attributes must be present within each Guacamole user's record in the * LDAP directory. If the LDAP authentication will not be given its own * credentials for querying other LDAP users, this list may contain only * one attribute, and the concatenation of that attribute and the value of * LDAP_USER_BASE_DN must equal the user's full DN. */ public static final StringListProperty LDAP_USERNAME_ATTRIBUTE = new StringListProperty() { @Override public String getName() { return "ldap-username-attribute"; } }; /** * The port on the LDAP server to connect to when authenticating users. */ public static final IntegerGuacamoleProperty LDAP_PORT = new IntegerGuacamoleProperty() { @Override public String getName() { return "ldap-port"; } }; /** * The hostname of the LDAP server to connect to when authenticating users. */ public static final StringGuacamoleProperty LDAP_HOSTNAME = new StringGuacamoleProperty() { @Override public String getName() { return "ldap-hostname"; } }; /** * The DN of the user that the LDAP authentication should bind as when * searching for the user accounts of users attempting to log in. If not * specified, the DNs of users attempting to log in will be derived from * the LDAP_BASE_DN and LDAP_USERNAME_ATTRIBUTE directly. */ public static final StringGuacamoleProperty LDAP_SEARCH_BIND_DN = new StringGuacamoleProperty() { @Override public String getName() { return "ldap-search-bind-dn"; } }; /** * The password to provide to the LDAP server when binding as * LDAP_SEARCH_BIND_DN. If LDAP_SEARCH_BIND_DN is not specified, this * property has no effect. If this property is not specified, no password * will be provided when attempting to bind as LDAP_SEARCH_BIND_DN. */ public static final StringGuacamoleProperty LDAP_SEARCH_BIND_PASSWORD = new StringGuacamoleProperty() { @Override public String getName() { return "ldap-search-bind-password"; } }; /** * The encryption method to use when connecting to the LDAP server, if any. * The chosen method will also dictate the default port if not already * explicitly specified via LDAP_PORT. */ public static final EncryptionMethodProperty LDAP_ENCRYPTION_METHOD = new EncryptionMethodProperty() { @Override public String getName() { return "ldap-encryption-method"; } }; } ././@LongLink0100644 0000000 0000000 00000000174 12645510255 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/user/UserService.javaguacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/lda0100664 0001751 0001751 00000026552 12645510255 035402 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.ldap.user; import com.google.inject.Inject; import com.novell.ldap.LDAPAttribute; import com.novell.ldap.LDAPConnection; import com.novell.ldap.LDAPEntry; import com.novell.ldap.LDAPException; import com.novell.ldap.LDAPSearchResults; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.glyptodon.guacamole.auth.ldap.ConfigurationService; import org.glyptodon.guacamole.auth.ldap.EscapingService; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleServerException; import org.glyptodon.guacamole.auth.ldap.LDAPGuacamoleProperties; import org.glyptodon.guacamole.net.auth.User; import org.glyptodon.guacamole.net.auth.simple.SimpleUser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Service for queries the users visible to a particular Guacamole user * according to an LDAP directory. * * @author Michael Jumper */ public class UserService { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(UserService.class); /** * Service for escaping parts of LDAP queries. */ @Inject private EscapingService escapingService; /** * Service for retrieving LDAP server configuration information. */ @Inject private ConfigurationService confService; /** * Adds all Guacamole users accessible to the user currently bound under * the given LDAP connection to the provided map. Only users with the * specified attribute are added. If the same username is encountered * multiple times, warnings about possible ambiguity will be logged. * * @param ldapConnection * The current connection to the LDAP server, associated with the * current user. * * @return * All users accessible to the user currently bound under the given * LDAP connection, as a map of connection identifier to corresponding * user object. * * @throws GuacamoleException * If an error occurs preventing retrieval of users. */ private void putAllUsers(Map users, LDAPConnection ldapConnection, String usernameAttribute) throws GuacamoleException { try { // Find all Guacamole users underneath base DN LDAPSearchResults results = ldapConnection.search( confService.getUserBaseDN(), LDAPConnection.SCOPE_SUB, "(&(objectClass=*)(" + escapingService.escapeLDAPSearchFilter(usernameAttribute) + "=*))", null, false ); // Read all visible users while (results.hasMore()) { LDAPEntry entry = results.next(); // Get username from record LDAPAttribute username = entry.getAttribute(usernameAttribute); if (username == null) { logger.warn("Queried user is missing the username attribute \"{}\".", usernameAttribute); continue; } // Store user using their username as the identifier String identifier = username.getStringValue(); if (users.put(identifier, new SimpleUser(identifier)) != null) logger.warn("Possibly ambiguous user account: \"{}\".", identifier); } } catch (LDAPException e) { throw new GuacamoleServerException("Error while querying users.", e); } } /** * Returns all Guacamole users accessible to the user currently bound under * the given LDAP connection. * * @param ldapConnection * The current connection to the LDAP server, associated with the * current user. * * @return * All users accessible to the user currently bound under the given * LDAP connection, as a map of connection identifier to corresponding * user object. * * @throws GuacamoleException * If an error occurs preventing retrieval of users. */ public Map getUsers(LDAPConnection ldapConnection) throws GuacamoleException { // Build map of users by querying each username attribute separately Map users = new HashMap(); for (String usernameAttribute : confService.getUsernameAttributes()) { // Attempt to pull all users with given attribute try { putAllUsers(users, ldapConnection, usernameAttribute); } // Log any errors non-fatally catch (GuacamoleException e) { logger.warn("Could not query list of all users for attribute \"{}\": {}", usernameAttribute, e.getMessage()); logger.debug("Error querying list of all users.", e); } } // Return map of all users return users; } /** * Generates a properly-escaped LDAP query which finds all objects having * at least one username attribute set to the specified username, where * the possible username attributes are defined within * guacamole.properties. * * @param username * The username that the resulting LDAP query should search for within * objects within the LDAP directory. * * @return * An LDAP query which will search for arbitrary LDAP objects * containing at least one username attribute set to the specified * username. * * @throws GuacamoleException * If the LDAP query cannot be generated because the list of username * attributes cannot be parsed from guacamole.properties. */ private String generateLDAPQuery(String username) throws GuacamoleException { List usernameAttributes = confService.getUsernameAttributes(); // Build LDAP query for users having at least one username attribute // with the specified username as its value StringBuilder ldapQuery = new StringBuilder("(&(objectClass=*)"); // Include all attributes within OR clause if there are more than one if (usernameAttributes.size() > 1) ldapQuery.append("(|"); // Add equality comparison for each possible username attribute for (String usernameAttribute : usernameAttributes) { ldapQuery.append("("); ldapQuery.append(escapingService.escapeLDAPSearchFilter(usernameAttribute)); ldapQuery.append("="); ldapQuery.append(escapingService.escapeLDAPSearchFilter(username)); ldapQuery.append(")"); } // Close OR clause, if any if (usernameAttributes.size() > 1) ldapQuery.append(")"); // Close overall query (AND clause) ldapQuery.append(")"); return ldapQuery.toString(); } /** * Returns a list of all DNs corresponding to the users having the given * username. If multiple username attributes are defined, or if uniqueness * is not enforced across the username attribute, it is possible that this * will return multiple DNs. * * @param ldapConnection * The connection to the LDAP server to use when querying user DNs. * * @param username * The username of the user whose corresponding user account DNs are * to be retrieved. * * @return * A list of all DNs corresponding to the users having the given * username. If no such DNs exist, this list will be empty. * * @throws GuacamoleException * If an error occurs while querying the user DNs, or if the username * attribute property cannot be parsed within guacamole.properties. */ public List getUserDNs(LDAPConnection ldapConnection, String username) throws GuacamoleException { try { List userDNs = new ArrayList(); // Find all Guacamole users underneath base DN and matching the // specified username LDAPSearchResults results = ldapConnection.search( confService.getUserBaseDN(), LDAPConnection.SCOPE_SUB, generateLDAPQuery(username), null, false ); // Add all DNs for found users while (results.hasMore()) { LDAPEntry entry = results.next(); userDNs.add(entry.getDN()); } // Return all discovered DNs (if any) return userDNs; } catch (LDAPException e) { throw new GuacamoleServerException("Error while query user DNs.", e); } } /** * Determines the DN which corresponds to the user having the given * username. The DN will either be derived directly from the user base DN, * or queried from the LDAP server, depending on how LDAP authentication * has been configured. * * @param username * The username of the user whose corresponding DN should be returned. * * @return * The DN which corresponds to the user having the given username. * * @throws GuacamoleException * If required properties are missing, and thus the user DN cannot be * determined. */ public String deriveUserDN(String username) throws GuacamoleException { // Pull username attributes from properties List usernameAttributes = confService.getUsernameAttributes(); // We need exactly one base DN to derive the user DN if (usernameAttributes.size() != 1) { logger.warn(String.format("Cannot directly derive user DN when " + "multiple username attributes are specified. Please " + "define an LDAP search DN using the \"%s\" property " + "in your \"guacamole.properties\".", LDAPGuacamoleProperties.LDAP_SEARCH_BIND_DN.getName())); return null; } // Derive user DN from base DN return escapingService.escapeDN(usernameAttributes.get(0)) + "=" + escapingService.escapeDN(username) + "," + confService.getUserBaseDN(); } } ././@LongLink0100644 0000000 0000000 00000000202 12612265775 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/user/AuthenticatedUser.javaguacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/lda0100664 0001751 0001751 00000004646 12612265775 035412 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.ldap.user; import com.google.inject.Inject; import org.glyptodon.guacamole.net.auth.AbstractAuthenticatedUser; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Credentials; /** * An LDAP-specific implementation of AuthenticatedUser, associating a * particular set of credentials with the LDAP authentication provider. * * @author Michael Jumper */ public class AuthenticatedUser extends AbstractAuthenticatedUser { /** * Reference to the authentication provider associated with this * authenticated user. */ @Inject private AuthenticationProvider authProvider; /** * The credentials provided when this user was authenticated. */ private Credentials credentials; /** * Initializes this AuthenticatedUser using the given credentials. * * @param credentials * The credentials provided when this user was authenticated. */ public void init(Credentials credentials) { this.credentials = credentials; setIdentifier(credentials.getUsername()); } @Override public AuthenticationProvider getAuthenticationProvider() { return authProvider; } @Override public Credentials getCredentials() { return credentials; } } ././@LongLink0100644 0000000 0000000 00000000174 12645510255 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/ldap/user/UserContext.javaguacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/org/glyptodon/guacamole/auth/lda0100664 0001751 0001751 00000016365 12645510255 035403 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.auth.ldap.user; import com.google.inject.Inject; import com.novell.ldap.LDAPConnection; import java.util.Collection; import java.util.Collections; import net.sourceforge.guacamole.net.auth.ldap.LDAPAuthenticationProvider; import org.glyptodon.guacamole.auth.ldap.connection.ConnectionService; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.form.Form; import org.glyptodon.guacamole.net.auth.ActiveConnection; import org.glyptodon.guacamole.net.auth.AuthenticatedUser; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.ConnectionRecordSet; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.User; import org.glyptodon.guacamole.net.auth.simple.SimpleConnectionGroup; import org.glyptodon.guacamole.net.auth.simple.SimpleConnectionGroupDirectory; import org.glyptodon.guacamole.net.auth.simple.SimpleConnectionRecordSet; import org.glyptodon.guacamole.net.auth.simple.SimpleDirectory; import org.glyptodon.guacamole.net.auth.simple.SimpleUser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * An LDAP-specific implementation of UserContext which queries all Guacamole * connections and users from the LDAP directory. * * @author Michael Jumper */ public class UserContext implements org.glyptodon.guacamole.net.auth.UserContext { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(UserContext.class); /** * Service for retrieving Guacamole connections from the LDAP server. */ @Inject private ConnectionService connectionService; /** * Service for retrieving Guacamole users from the LDAP server. */ @Inject private UserService userService; /** * Reference to the AuthenticationProvider associated with this * UserContext. */ @Inject private AuthenticationProvider authProvider; /** * Reference to a User object representing the user whose access level * dictates the users and connections visible through this UserContext. */ private User self; /** * Directory containing all User objects accessible to the user associated * with this UserContext. */ private Directory userDirectory; /** * Directory containing all Connection objects accessible to the user * associated with this UserContext. */ private Directory connectionDirectory; /** * Directory containing all ConnectionGroup objects accessible to the user * associated with this UserContext. */ private Directory connectionGroupDirectory; /** * Reference to the root connection group. */ private ConnectionGroup rootGroup; /** * Initializes this UserContext using the provided AuthenticatedUser and * LDAPConnection. * * @param user * The AuthenticatedUser representing the user that authenticated. This * user may have been authenticated by a different authentication * provider (not LDAP). * * @param ldapConnection * The connection to the LDAP server to use when querying accessible * Guacamole users and connections. * * @throws GuacamoleException * If associated data stored within the LDAP directory cannot be * queried due to an error. */ public void init(AuthenticatedUser user, LDAPConnection ldapConnection) throws GuacamoleException { // Query all accessible users userDirectory = new SimpleDirectory( userService.getUsers(ldapConnection) ); // Query all accessible connections connectionDirectory = new SimpleDirectory( connectionService.getConnections(user, ldapConnection) ); // Root group contains only connections rootGroup = new SimpleConnectionGroup( LDAPAuthenticationProvider.ROOT_CONNECTION_GROUP, LDAPAuthenticationProvider.ROOT_CONNECTION_GROUP, connectionDirectory.getIdentifiers(), Collections.emptyList() ); // Expose only the root group in the connection group directory connectionGroupDirectory = new SimpleConnectionGroupDirectory(Collections.singleton(rootGroup)); // Init self with basic permissions self = new SimpleUser( user.getIdentifier(), userDirectory.getIdentifiers(), connectionDirectory.getIdentifiers(), connectionGroupDirectory.getIdentifiers() ); } @Override public User self() { return self; } @Override public AuthenticationProvider getAuthenticationProvider() { return authProvider; } @Override public Directory getUserDirectory() throws GuacamoleException { return userDirectory; } @Override public Directory getConnectionDirectory() throws GuacamoleException { return connectionDirectory; } @Override public Directory getConnectionGroupDirectory() throws GuacamoleException { return connectionGroupDirectory; } @Override public ConnectionGroup getRootConnectionGroup() throws GuacamoleException { return rootGroup; } @Override public Directory getActiveConnectionDirectory() throws GuacamoleException { return new SimpleDirectory(); } @Override public ConnectionRecordSet getConnectionHistory() throws GuacamoleException { return new SimpleConnectionRecordSet(); } @Override public Collection getUserAttributes() { return Collections.emptyList(); } @Override public Collection getConnectionAttributes() { return Collections.emptyList(); } @Override public Collection getConnectionGroupAttributes() { return Collections.emptyList(); } } ././@LongLink0100644 0000000 0000000 00000000214 12612226251 011627 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/net/sourceforge/guacamole/net/auth/ldap/LDAPAuthenticationProvider.javaguacamole-client-0.9.9/extensions/guacamole-auth-ldap/src/main/java/net/sourceforge/guacamole/net/au0100664 0001751 0001751 00000007364 12612226251 035371 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package net.sourceforge.guacamole.net.auth.ldap; import org.glyptodon.guacamole.auth.ldap.AuthenticationProviderService; import org.glyptodon.guacamole.auth.ldap.LDAPAuthenticationProviderModule; import com.google.inject.Guice; import com.google.inject.Injector; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.AuthenticatedUser; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.UserContext; /** * Allows users to be authenticated against an LDAP server. Each user may have * any number of authorized configurations. Authorized configurations may be * shared. * * @author Michael Jumper */ public class LDAPAuthenticationProvider implements AuthenticationProvider { /** * The identifier reserved for the root connection group. */ public static final String ROOT_CONNECTION_GROUP = "ROOT"; /** * Injector which will manage the object graph of this authentication * provider. */ private final Injector injector; /** * Creates a new LDAPAuthenticationProvider that authenticates users * against an LDAP directory. * * @throws GuacamoleException * If a required property is missing, or an error occurs while parsing * a property. */ public LDAPAuthenticationProvider() throws GuacamoleException { // Set up Guice injector. injector = Guice.createInjector( new LDAPAuthenticationProviderModule(this) ); } @Override public String getIdentifier() { return "ldap"; } @Override public AuthenticatedUser authenticateUser(Credentials credentials) throws GuacamoleException { AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class); return authProviderService.authenticateUser(credentials); } @Override public AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser, Credentials credentials) throws GuacamoleException { return authenticatedUser; } @Override public UserContext getUserContext(AuthenticatedUser authenticatedUser) throws GuacamoleException { AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class); return authProviderService.getUserContext(authenticatedUser); } @Override public UserContext updateUserContext(UserContext context, AuthenticatedUser authenticatedUser) throws GuacamoleException { return context; } } guacamole-client-0.9.9/extensions/guacamole-auth-ldap/schema/guacConfigGroup.ldif0100664 0001751 0001751 00000000763 12471273272 031737 0ustar00mjumpermjumper0000000 0000000 dn: cn=guacConfigGroup,cn=schema,cn=config objectClass: olcSchemaConfig cn: guacConfigGroup olcAttributeTypes: {0}( 1.3.6.1.4.1.38971.1.1.1 NAME 'guacConfigProtocol' SYNTAX 1.3.6.1.4.1.1466 .115.121.1.15 ) olcAttributeTypes: {1}( 1.3.6.1.4.1.38971.1.1.2 NAME 'guacConfigParameter' SYNTAX 1.3.6.1.4.1.146 6.115.121.1.15 ) olcObjectClasses: {0}( 1.3.6.1.4.1.38971.1.2.1 NAME 'guacConfigGroup' DESC 'Guacamole config uration group' SUP groupOfNames MUST guacConfigProtocol MAY guacConfigParameter ) guacamole-client-0.9.9/extensions/guacamole-auth-ldap/schema/guacConfigGroup.schema0100664 0001751 0001751 00000000572 12471273272 032257 0ustar00mjumpermjumper0000000 0000000 attributetype ( 1.3.6.1.4.1.38971.1.1.1 NAME 'guacConfigProtocol' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) attributetype ( 1.3.6.1.4.1.38971.1.1.2 NAME 'guacConfigParameter' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) objectClass ( 1.3.6.1.4.1.38971.1.2.1 NAME 'guacConfigGroup' DESC 'Guacamole configuration group' SUP groupOfNames MUST protocol MAY parameter ) guacamole-client-0.9.9/extensions/guacamole-auth-ldap/pom.xml0100664 0001751 0001751 00000010110 12645510255 026053 0ustar00mjumpermjumper0000000 0000000 4.0.0 org.glyptodon.guacamole guacamole-auth-ldap jar 0.9.9 guacamole-auth-ldap http://guac-dev.org/ UTF-8 org.apache.maven.plugins maven-compiler-plugin 3.3 1.6 1.6 -Xlint:all -Werror true org.apache.maven.plugins maven-dependency-plugin 2.10 unpack-dependencies prepare-package unpack-dependencies runtime ${project.build.directory}/classes maven-assembly-plugin 2.5.3 ${project.artifactId}-${project.version} false src/main/assembly/dist.xml make-dist-archive package single org.glyptodon.guacamole guacamole-common 0.9.9 provided org.glyptodon.guacamole guacamole-ext 0.9.9 provided com.novell.ldap jldap 4.3 com.google.inject guice 3.0 com.google.inject.extensions guice-multibindings 3.0 guacamole-client-0.9.9/extensions/guacamole-auth-ldap/LICENSE0100664 0001751 0001751 00000002041 12471273272 025551 0ustar00mjumpermjumper0000000 0000000 Copyright (C) 2013 Glyptodon LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. guacamole-client-0.9.9/extensions/guacamole-auth-noauth/README0100664 0001751 0001751 00000010466 12471273272 026014 0ustar00mjumpermjumper0000000 0000000 ------------------------------------------------------------ About this README ------------------------------------------------------------ This README is intended to provide quick and to-the-point documentation for technical users intending to compile parts of Guacamole themselves. Distribution-specific packages are available from the files section of the main project page: http://sourceforge.net/projects/guacamole/files/ Distribution-specific documentation is provided on the Guacamole wiki: http://guac-dev.org/ ------------------------------------------------------------ What is guacamole-auth-noauth? ------------------------------------------------------------ guacamole-auth-noauth is a Java library for use with the Guacamole web application which removes all authentication. It provides a static set of connections to anyone using the web application. Beware that with authentication disabled, any person with access to your Guacamole server will have access to any connections it provides. The login screen will not appear unless guacamole-auth-noauth was set up incorrectly or it is not compatible with your version of Guacamole. ------------------------------------------------------------ Compiling and installing guacamole-auth-noauth ------------------------------------------------------------ guacamole-auth-noauth is built using Maven. Building guacamole-auth-noauth compiles all classes and packages them into a redistributable .jar file. This .jar file can be installed in the library directory configured in guacamole.properties such that the authentication provider is available. 1) Run mvn package $ mvn package Maven will download any needed dependencies for building the .jar file. Once all dependencies have been downloaded, the .jar file will be created in the target/ subdirectory of the current directory. If this process fails, check the build errors, and verify that the contents of your settings.xml file is correct. 2) Extract the .tar.gz file now present in the target/ directory, and place the .jar files in the extracted lib/ subdirectory in the library directory specified in guacamole.properties. You will likely need to do this as root. If you do not have a library directory configured in your guacamole.properties, you will need to specify one. The directory is specified using the "lib-directory" property. 3) Write a noauth-config.xml file containing all desired connections The guacamole-auth-noauth authentication provided is configured by a static XML file which lists all available connections. The file consists of a single tag which contains any number of tags, each having a name and protocol. These tags correspond to connections. Each can contain any number of tags which correspond to the parameters of the protocol chosen. Each tag has a name and value, where the name is defined by the documentation of the protocol being used. For example, to define a single RDP connection to your RDP server with the hostname "my-rdp-server-hostname", you could write: When completed, this file should be placed in a common location such that it can be referenced from guacamole.properties, like /etc/guacamole/noauth-config.xml. 4) Configure guacamole.properties An additional property is required by guacamole-auth-noauth which defines where the configuration file is found. It must be added to your guacamole.properties: # Configuration for NoAuth noauth-config: /etc/guacamole/noauth-config.xml 5) Restart Tomcat The guacamole.properties file as well as any authentication provider .jar files are only read when Tomcat starts. This goes for any other servlet container as well. You must restart Tomcat before any of the above changes can take effect. ------------------------------------------------------------ Reporting problems ------------------------------------------------------------ Please report any bugs encountered by opening a new ticket at the Trac system hosted at: http://guac-dev.org/trac/ guacamole-client-0.9.9/extensions/guacamole-auth-noauth/doc/example/noauth-config.xml0100664 0001751 0001751 00000000257 12471273272 032614 0ustar00mjumpermjumper0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/resources/translations/en.json0100664 0001751 0001751 00000000103 12612226251 034652 0ustar00mjumpermjumper0000000 0000000 { "DATA_SOURCE_NOAUTH" : { "NAME" : "NoAuth" } } guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/resources/guac-manifest.json0100664 0001751 0001751 00000000441 12645510255 034265 0ustar00mjumpermjumper0000000 0000000 { "guacamoleVersion" : "0.9.9", "name" : "Disabled Authentication", "namespace" : "guac-noauth", "authProviders" : [ "net.sourceforge.guacamole.net.auth.noauth.NoAuthenticationProvider" ], "translations" : [ "translations/en.json" ] } guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/assembly/dist.xml0100664 0001751 0001751 00000001645 12612226251 032142 0ustar00mjumpermjumper0000000 0000000 dist ${project.artifactId}-${project.version} tar.gz doc target *.jar ././@LongLink0100644 0000000 0000000 00000000216 12612226251 011631 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/java/net/sourceforge/guacamole/net/auth/noauth/NoAuthenticationProvider.javaguacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/java/net/sourceforge/guacamole/net/0100664 0001751 0001751 00000015514 12612226251 035415 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package net.sourceforge.guacamole.net.auth.noauth; import java.util.Map; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.Reader; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleServerException; import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.environment.LocalEnvironment; import org.glyptodon.guacamole.net.auth.simple.SimpleAuthenticationProvider; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.properties.FileGuacamoleProperty; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; import org.slf4j.LoggerFactory; import org.slf4j.Logger; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLReaderFactory; /** * Disable authentication in Guacamole. All users accessing Guacamole are * automatically authenticated as "Anonymous" user and are able to use all * available GuacamoleConfigurations. * * GuacamoleConfiguration are read from the XML file defined by `noauth-config` * in the Guacamole configuration file (`guacamole.properties`). * * * Example `guacamole.properties`: * * noauth-config: /etc/guacamole/noauth-config.xml * * * Example `noauth-config.xml`: * * * * * * * * * @author Laurent Meunier */ public class NoAuthenticationProvider extends SimpleAuthenticationProvider { /** * Logger for this class. */ private Logger logger = LoggerFactory.getLogger(NoAuthenticationProvider.class); /** * Map of all known configurations, indexed by identifier. */ private Map configs; /** * The last time the configuration XML was modified, as milliseconds since * UNIX epoch. */ private long configTime; /** * Guacamole server environment. */ private final Environment environment; /** * The XML file to read the configuration from. */ public static final FileGuacamoleProperty NOAUTH_CONFIG = new FileGuacamoleProperty() { @Override public String getName() { return "noauth-config"; } }; /** * The default filename to use for the configuration, if not defined within * guacamole.properties. */ public static final String DEFAULT_NOAUTH_CONFIG = "noauth-config.xml"; /** * Creates a new NoAuthenticationProvider that does not perform any * authentication at all. All attempts to access the Guacamole system are * presumed to be authorized. * * @throws GuacamoleException * If a required property is missing, or an error occurs while parsing * a property. */ public NoAuthenticationProvider() throws GuacamoleException { environment = new LocalEnvironment(); } @Override public String getIdentifier() { return "noauth"; } /** * Retrieves the configuration file, as defined within guacamole.properties. * * @return The configuration file, as defined within guacamole.properties. * @throws GuacamoleException If an error occurs while reading the * property. */ private File getConfigurationFile() throws GuacamoleException { // Get config file, defaulting to GUACAMOLE_HOME/noauth-config.xml File configFile = environment.getProperty(NOAUTH_CONFIG); if (configFile == null) configFile = new File(environment.getGuacamoleHome(), DEFAULT_NOAUTH_CONFIG); return configFile; } public synchronized void init() throws GuacamoleException { // Get configuration file File configFile = getConfigurationFile(); logger.debug("Reading configuration file: \"{}\"", configFile); // Parse document try { // Set up parser NoAuthConfigContentHandler contentHandler = new NoAuthConfigContentHandler(); XMLReader parser = XMLReaderFactory.createXMLReader(); parser.setContentHandler(contentHandler); // Read and parse file Reader reader = new BufferedReader(new FileReader(configFile)); parser.parse(new InputSource(reader)); reader.close(); // Init configs configTime = configFile.lastModified(); configs = contentHandler.getConfigs(); } catch (IOException e) { throw new GuacamoleServerException("Error reading configuration file.", e); } catch (SAXException e) { throw new GuacamoleServerException("Error parsing XML file.", e); } } @Override public Map getAuthorizedConfigurations(Credentials credentials) throws GuacamoleException { // Check mapping file mod time File configFile = getConfigurationFile(); if (configFile.exists() && configTime < configFile.lastModified()) { // If modified recently, gain exclusive access and recheck synchronized (this) { if (configFile.exists() && configTime < configFile.lastModified()) { logger.debug("Configuration file \"{}\" has been modified.", configFile); init(); // If still not up to date, re-init } } } // If no mapping available, report as such if (configs == null) throw new GuacamoleServerException("Configuration could not be read."); return configs; } } ././@LongLink0100644 0000000 0000000 00000000220 12471273272 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/java/net/sourceforge/guacamole/net/auth/noauth/NoAuthConfigContentHandler.javaguacamole-client-0.9.9/extensions/guacamole-auth-noauth/src/main/java/net/sourceforge/guacamole/net/0100664 0001751 0001751 00000010021 12471273272 035411 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package net.sourceforge.guacamole.net.auth.noauth; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; /** * XML parser for the configuration file used by the NoAuth auth provider. * * @author Laurent Meunier */ public class NoAuthConfigContentHandler extends DefaultHandler { /** * Map of all configurations, indexed by name. */ private Map configs = new HashMap(); /** * The name of the current configuration, if any. */ private String current = null; /** * The current configuration being parsed, if any. */ private GuacamoleConfiguration currentConfig = null; /** * Returns the a map of all available configurations as parsed from the * XML file. This map is unmodifiable. * * @return A map of all available configurations. */ public Map getConfigs() { return Collections.unmodifiableMap(configs); } @Override public void endElement(String uri, String localName, String qName) throws SAXException { // If end of config element, add to map if (localName.equals("config")) { // Add to map configs.put(current, currentConfig); // Reset state for next configuration currentConfig = null; current = null; } } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { // Begin configuration parsing if config element if (localName.equals("config")) { // Ensure this config is on the top level if (current != null) throw new SAXException("Configurations cannot be nested."); // Read name String name = attributes.getValue("name"); if (name == null) throw new SAXException("Each configuration must have a name."); // Read protocol String protocol = attributes.getValue("protocol"); if (protocol == null) throw new SAXException("Each configuration must have a protocol."); // Create config stub current = name; currentConfig = new GuacamoleConfiguration(); currentConfig.setProtocol(protocol); } // Add parameters to existing configuration else if (localName.equals("param")) { // Ensure a corresponding config exists if (currentConfig == null) throw new SAXException("Parameter without corresponding configuration."); currentConfig.setParameter(attributes.getValue("name"), attributes.getValue("value")); } } } guacamole-client-0.9.9/extensions/guacamole-auth-noauth/pom.xml0100664 0001751 0001751 00000007024 12645510255 026443 0ustar00mjumpermjumper0000000 0000000 4.0.0 org.glyptodon.guacamole guacamole-auth-noauth jar 0.9.9 guacamole-auth-noauth http://guacamole.sourceforge.net/ UTF-8 org.apache.maven.plugins maven-compiler-plugin 3.3 1.6 1.6 -Xlint:all -Werror true org.apache.maven.plugins maven-dependency-plugin 2.10 unpack-dependencies prepare-package unpack-dependencies runtime ${project.build.directory}/classes maven-assembly-plugin 2.5.3 ${project.artifactId}-${project.version} false src/main/assembly/dist.xml make-dist-archive package single org.glyptodon.guacamole guacamole-common 0.9.9 provided org.glyptodon.guacamole guacamole-ext 0.9.9 provided guacamole-client-0.9.9/extensions/guacamole-auth-noauth/LICENSE0100664 0001751 0001751 00000002041 12471273272 026127 0ustar00mjumpermjumper0000000 0000000 Copyright (C) 2013 Glyptodon LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. guacamole-client-0.9.9/guacamole-common/README0100664 0001751 0001751 00000005227 12424302752 022641 0ustar00mjumpermjumper0000000 0000000 ------------------------------------------------------------ About this README ------------------------------------------------------------ This README is intended to provide quick and to-the-point documentation for technical users intending to compile parts of Guacamole themselves. Distribution-specific packages are available from the files section of the main project page: http://sourceforge.net/projects/guacamole/files/ Distribution-specific documentation is provided on the Guacamole wiki: http://guac-dev.org/ ------------------------------------------------------------ What is guacamole-common? ------------------------------------------------------------ guacamole-common is the core Java library used by the Guacamole web application. guacamole-common provides abstract means of connecting to guacd, interfacing with the JavaScript client and tunnel provided by guacamole-common-js, and reading configuration from a standard location (guacamole.properties). ------------------------------------------------------------ Compiling and installing guacamole-common ------------------------------------------------------------ guacamole-common is built using Maven. Building guacamole-common compiles all classes and packages them into a redistributable .jar file. This .jar file can be easily included in other Maven-based projects (like Guacamole). Note that prebuilt versions of guacamole-common are available from the main guac-dev.org Maven repository which is referenced in all Maven projects in Guacamole. Unless you want to make changes to guacamole-common or you want to use a newer, unreleased version (such as the unstable branch), you do not need to build this manually. You can let Maven download it for you when you build Guacamole. 1) Run mvn package $ mvn package Maven will download any needed dependencies for building the .jar file. Once all dependencies have been downloaded, the .jar file will be created in the target/ subdirectory of the current directory. 2) Run mvn install $ mvn install DO NOT RUN THIS AS ROOT! Maven will install guacamole-common to your user's local Maven repository where it can be used in future builds. It will not install into a system-wide repository and does not require root privileges. Once installed, building other Maven projects that depend on guacamole-common (such as Guacamole) will be possible. ------------------------------------------------------------ Reporting problems ------------------------------------------------------------ Please report any bugs encountered by opening a new ticket at the Trac system hosted at: http://guac-dev.org/trac/ guacamole-client-0.9.9/guacamole-common/doc/Doxyfile0100664 0001751 0001751 00000177663 12424302752 024252 0ustar00mjumpermjumper0000000 0000000 # Doxyfile 1.6.3 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project # # All text after a hash (#) is considered a comment and will be ignored # The format is: # TAG = value [value, ...] # For lists items can also be appended using: # TAG += value [value, ...] # Values that contain spaces should be placed between quotes (" ") #--------------------------------------------------------------------------- # Project related configuration options #--------------------------------------------------------------------------- # This tag specifies the encoding used for all characters in the config file # that follow. The default is UTF-8 which is also the encoding used for all # text before the first occurrence of this tag. Doxygen uses libiconv (or the # iconv built into libc) for the transcoding. See # http://www.gnu.org/software/libiconv for the list of possible encodings. DOXYFILE_ENCODING = UTF-8 # The PROJECT_NAME tag is a single word (or a sequence of words surrounded # by quotes) that should identify the project. PROJECT_NAME = guacamole-common # The PROJECT_NUMBER tag can be used to enter a project or revision number. # This could be handy for archiving the generated documentation or # if some version control system is used. PROJECT_NUMBER = 0.5.0 # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) # base path where the generated documentation will be put. # If a relative path is entered, it will be relative to the location # where doxygen was started. If left blank the current directory will be used. OUTPUT_DIRECTORY = doxygen-output # If the CREATE_SUBDIRS tag is set to YES, then doxygen will create # 4096 sub-directories (in 2 levels) under the output directory of each output # format and will distribute the generated files over these directories. # Enabling this option can be useful when feeding doxygen a huge amount of # source files, where putting all generated files in the same directory would # otherwise cause performance problems for the file system. CREATE_SUBDIRS = NO # The OUTPUT_LANGUAGE tag is used to specify the language in which all # documentation generated by doxygen is written. Doxygen will use this # information to generate all constant output in the proper language. # The default language is English, other supported languages are: # Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional, # Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German, # Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English # messages), Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian, # Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrilic, Slovak, # Slovene, Spanish, Swedish, Ukrainian, and Vietnamese. OUTPUT_LANGUAGE = English # If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will # include brief member descriptions after the members that are listed in # the file and class documentation (similar to JavaDoc). # Set to NO to disable this. BRIEF_MEMBER_DESC = YES # If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend # the brief description of a member or function before the detailed description. # Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the # brief descriptions will be completely suppressed. REPEAT_BRIEF = YES # This tag implements a quasi-intelligent brief description abbreviator # that is used to form the text in various listings. Each string # in this list, if found as the leading text of the brief description, will be # stripped from the text and the result after processing the whole list, is # used as the annotated text. Otherwise, the brief description is used as-is. # If left blank, the following values are used ("$name" is automatically # replaced with the name of the entity): "The $name class" "The $name widget" # "The $name file" "is" "provides" "specifies" "contains" # "represents" "a" "an" "the" ABBREVIATE_BRIEF = # If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then # Doxygen will generate a detailed section even if there is only a brief # description. ALWAYS_DETAILED_SEC = NO # If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all # inherited members of a class in the documentation of that class as if those # members were ordinary class members. Constructors, destructors and assignment # operators of the base classes will not be shown. INLINE_INHERITED_MEMB = NO # If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full # path before files name in the file list and in the header files. If set # to NO the shortest path that makes the file name unique will be used. FULL_PATH_NAMES = YES # If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag # can be used to strip a user-defined part of the path. Stripping is # only done if one of the specified strings matches the left-hand part of # the path. The tag can be used to show relative paths in the file list. # If left blank the directory from which doxygen is run is used as the # path to strip. STRIP_FROM_PATH = ../src # The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of # the path mentioned in the documentation of a class, which tells # the reader which header file to include in order to use a class. # If left blank only the name of the header file containing the class # definition is used. Otherwise one should specify the include paths that # are normally passed to the compiler using the -I flag. STRIP_FROM_INC_PATH = # If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter # (but less readable) file names. This can be useful is your file systems # doesn't support long names like on DOS, Mac, or CD-ROM. SHORT_NAMES = NO # If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen # will interpret the first line (until the first dot) of a JavaDoc-style # comment as the brief description. If set to NO, the JavaDoc # comments will behave just like regular Qt-style comments # (thus requiring an explicit @brief command for a brief description.) JAVADOC_AUTOBRIEF = YES # If the QT_AUTOBRIEF tag is set to YES then Doxygen will # interpret the first line (until the first dot) of a Qt-style # comment as the brief description. If set to NO, the comments # will behave just like regular Qt-style comments (thus requiring # an explicit \brief command for a brief description.) QT_AUTOBRIEF = NO # The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen # treat a multi-line C++ special comment block (i.e. a block of //! or /// # comments) as a brief description. This used to be the default behaviour. # The new default is to treat a multi-line C++ comment block as a detailed # description. Set this tag to YES if you prefer the old behaviour instead. MULTILINE_CPP_IS_BRIEF = NO # If the INHERIT_DOCS tag is set to YES (the default) then an undocumented # member inherits the documentation from any documented member that it # re-implements. INHERIT_DOCS = YES # If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce # a new page for each member. If set to NO, the documentation of a member will # be part of the file/class/namespace that contains it. SEPARATE_MEMBER_PAGES = NO # The TAB_SIZE tag can be used to set the number of spaces in a tab. # Doxygen uses this value to replace tabs by spaces in code fragments. TAB_SIZE = 8 # This tag can be used to specify a number of aliases that acts # as commands in the documentation. An alias has the form "name=value". # For example adding "sideeffect=\par Side Effects:\n" will allow you to # put the command \sideeffect (or @sideeffect) in the documentation, which # will result in a user-defined paragraph with heading "Side Effects:". # You can put \n's in the value part of an alias to insert newlines. ALIASES = # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C # sources only. Doxygen will then generate output that is more tailored for C. # For instance, some of the names that are used will be different. The list # of all members will be omitted, etc. OPTIMIZE_OUTPUT_FOR_C = NO # Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java # sources only. Doxygen will then generate output that is more tailored for # Java. For instance, namespaces will be presented as packages, qualified # scopes will look different, etc. OPTIMIZE_OUTPUT_JAVA = YES # Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran # sources only. Doxygen will then generate output that is more tailored for # Fortran. OPTIMIZE_FOR_FORTRAN = NO # Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL # sources. Doxygen will then generate output that is tailored for # VHDL. OPTIMIZE_OUTPUT_VHDL = NO # Doxygen selects the parser to use depending on the extension of the files it parses. # With this tag you can assign which parser to use for a given extension. # Doxygen has a built-in mapping, but you can override or extend it using this tag. # The format is ext=language, where ext is a file extension, and language is one of # the parsers supported by doxygen: IDL, Java, Javascript, C#, C, C++, D, PHP, # Objective-C, Python, Fortran, VHDL, C, C++. For instance to make doxygen treat # .inc files as Fortran files (default is PHP), and .f files as C (default is Fortran), # use: inc=Fortran f=C. Note that for custom extensions you also need to set FILE_PATTERNS otherwise the files are not read by doxygen. EXTENSION_MAPPING = # If you use STL classes (i.e. std::string, std::vector, etc.) but do not want # to include (a tag file for) the STL sources as input, then you should # set this tag to YES in order to let doxygen match functions declarations and # definitions whose arguments contain STL classes (e.g. func(std::string); v.s. # func(std::string) {}). This also make the inheritance and collaboration # diagrams that involve STL classes more complete and accurate. BUILTIN_STL_SUPPORT = NO # If you use Microsoft's C++/CLI language, you should set this option to YES to # enable parsing support. CPP_CLI_SUPPORT = NO # Set the SIP_SUPPORT tag to YES if your project consists of sip sources only. # Doxygen will parse them like normal C++ but will assume all classes use public # instead of private inheritance when no explicit protection keyword is present. SIP_SUPPORT = NO # For Microsoft's IDL there are propget and propput attributes to indicate getter # and setter methods for a property. Setting this option to YES (the default) # will make doxygen to replace the get and set methods by a property in the # documentation. This will only work if the methods are indeed getting or # setting a simple type. If this is not the case, or you want to show the # methods anyway, you should set this option to NO. IDL_PROPERTY_SUPPORT = NO # If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC # tag is set to YES, then doxygen will reuse the documentation of the first # member in the group (if any) for the other members of the group. By default # all members of a group must be documented explicitly. DISTRIBUTE_GROUP_DOC = NO # Set the SUBGROUPING tag to YES (the default) to allow class member groups of # the same type (for instance a group of public functions) to be put as a # subgroup of that type (e.g. under the Public Functions section). Set it to # NO to prevent subgrouping. Alternatively, this can be done per class using # the \nosubgrouping command. SUBGROUPING = YES # When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum # is documented as struct, union, or enum with the name of the typedef. So # typedef struct TypeS {} TypeT, will appear in the documentation as a struct # with name TypeT. When disabled the typedef will appear as a member of a file, # namespace, or class. And the struct will be named TypeS. This can typically # be useful for C code in case the coding convention dictates that all compound # types are typedef'ed and only the typedef is referenced, never the tag name. TYPEDEF_HIDES_STRUCT = YES # The SYMBOL_CACHE_SIZE determines the size of the internal cache use to # determine which symbols to keep in memory and which to flush to disk. # When the cache is full, less often used symbols will be written to disk. # For small to medium size projects (<1000 input files) the default value is # probably good enough. For larger projects a too small cache size can cause # doxygen to be busy swapping symbols to and from disk most of the time # causing a significant performance penality. # If the system has enough physical memory increasing the cache will improve the # performance by keeping more symbols in memory. Note that the value works on # a logarithmic scale so increasing the size by one will rougly double the # memory usage. The cache size is given by this formula: # 2^(16+SYMBOL_CACHE_SIZE). The valid range is 0..9, the default is 0, # corresponding to a cache size of 2^16 = 65536 symbols SYMBOL_CACHE_SIZE = 0 #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- # If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in # documentation are documented, even if no documentation was available. # Private class members and static file members will be hidden unless # the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES EXTRACT_ALL = NO # If the EXTRACT_PRIVATE tag is set to YES all private members of a class # will be included in the documentation. EXTRACT_PRIVATE = NO # If the EXTRACT_STATIC tag is set to YES all static members of a file # will be included in the documentation. EXTRACT_STATIC = NO # If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) # defined locally in source files will be included in the documentation. # If set to NO only classes defined in header files are included. EXTRACT_LOCAL_CLASSES = YES # This flag is only useful for Objective-C code. When set to YES local # methods, which are defined in the implementation section but not in # the interface are included in the documentation. # If set to NO (the default) only methods in the interface are included. EXTRACT_LOCAL_METHODS = NO # If this flag is set to YES, the members of anonymous namespaces will be # extracted and appear in the documentation as a namespace called # 'anonymous_namespace{file}', where file will be replaced with the base # name of the file that contains the anonymous namespace. By default # anonymous namespace are hidden. EXTRACT_ANON_NSPACES = NO # If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all # undocumented members of documented classes, files or namespaces. # If set to NO (the default) these members will be included in the # various overviews, but no documentation section is generated. # This option has no effect if EXTRACT_ALL is enabled. HIDE_UNDOC_MEMBERS = NO # If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all # undocumented classes that are normally visible in the class hierarchy. # If set to NO (the default) these classes will be included in the various # overviews. This option has no effect if EXTRACT_ALL is enabled. HIDE_UNDOC_CLASSES = NO # If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all # friend (class|struct|union) declarations. # If set to NO (the default) these declarations will be included in the # documentation. HIDE_FRIEND_COMPOUNDS = NO # If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any # documentation blocks found inside the body of a function. # If set to NO (the default) these blocks will be appended to the # function's detailed documentation block. HIDE_IN_BODY_DOCS = NO # The INTERNAL_DOCS tag determines if documentation # that is typed after a \internal command is included. If the tag is set # to NO (the default) then the documentation will be excluded. # Set it to YES to include the internal documentation. INTERNAL_DOCS = NO # If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate # file names in lower-case letters. If set to YES upper-case letters are also # allowed. This is useful if you have classes or files whose names only differ # in case and if your file system supports case sensitive file names. Windows # and Mac users are advised to set this option to NO. CASE_SENSE_NAMES = YES # If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen # will show members with their full class and namespace scopes in the # documentation. If set to YES the scope will be hidden. HIDE_SCOPE_NAMES = YES # If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen # will put a list of the files that are included by a file in the documentation # of that file. SHOW_INCLUDE_FILES = YES # If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen # will list include files with double quotes in the documentation # rather than with sharp brackets. FORCE_LOCAL_INCLUDES = NO # If the INLINE_INFO tag is set to YES (the default) then a tag [inline] # is inserted in the documentation for inline members. INLINE_INFO = YES # If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen # will sort the (detailed) documentation of file and class members # alphabetically by member name. If set to NO the members will appear in # declaration order. SORT_MEMBER_DOCS = YES # If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the # brief documentation of file, namespace and class members alphabetically # by member name. If set to NO (the default) the members will appear in # declaration order. SORT_BRIEF_DOCS = NO # If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the (brief and detailed) documentation of class members so that constructors and destructors are listed first. If set to NO (the default) the constructors will appear in the respective orders defined by SORT_MEMBER_DOCS and SORT_BRIEF_DOCS. This tag will be ignored for brief docs if SORT_BRIEF_DOCS is set to NO and ignored for detailed docs if SORT_MEMBER_DOCS is set to NO. SORT_MEMBERS_CTORS_1ST = NO # If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the # hierarchy of group names into alphabetical order. If set to NO (the default) # the group names will appear in their defined order. SORT_GROUP_NAMES = NO # If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be # sorted by fully-qualified names, including namespaces. If set to # NO (the default), the class list will be sorted only by class name, # not including the namespace part. # Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. # Note: This option applies only to the class list, not to the # alphabetical list. SORT_BY_SCOPE_NAME = NO # The GENERATE_TODOLIST tag can be used to enable (YES) or # disable (NO) the todo list. This list is created by putting \todo # commands in the documentation. GENERATE_TODOLIST = YES # The GENERATE_TESTLIST tag can be used to enable (YES) or # disable (NO) the test list. This list is created by putting \test # commands in the documentation. GENERATE_TESTLIST = YES # The GENERATE_BUGLIST tag can be used to enable (YES) or # disable (NO) the bug list. This list is created by putting \bug # commands in the documentation. GENERATE_BUGLIST = YES # The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or # disable (NO) the deprecated list. This list is created by putting # \deprecated commands in the documentation. GENERATE_DEPRECATEDLIST= YES # The ENABLED_SECTIONS tag can be used to enable conditional # documentation sections, marked by \if sectionname ... \endif. ENABLED_SECTIONS = # The MAX_INITIALIZER_LINES tag determines the maximum number of lines # the initial value of a variable or define consists of for it to appear in # the documentation. If the initializer consists of more lines than specified # here it will be hidden. Use a value of 0 to hide initializers completely. # The appearance of the initializer of individual variables and defines in the # documentation can be controlled using \showinitializer or \hideinitializer # command in the documentation regardless of this setting. MAX_INITIALIZER_LINES = 30 # Set the SHOW_USED_FILES tag to NO to disable the list of files generated # at the bottom of the documentation of classes and structs. If set to YES the # list will mention the files that were used to generate the documentation. SHOW_USED_FILES = YES # If the sources in your project are distributed over multiple directories # then setting the SHOW_DIRECTORIES tag to YES will show the directory hierarchy # in the documentation. The default is NO. SHOW_DIRECTORIES = NO # Set the SHOW_FILES tag to NO to disable the generation of the Files page. # This will remove the Files entry from the Quick Index and from the # Folder Tree View (if specified). The default is YES. SHOW_FILES = YES # Set the SHOW_NAMESPACES tag to NO to disable the generation of the # Namespaces page. # This will remove the Namespaces entry from the Quick Index # and from the Folder Tree View (if specified). The default is YES. SHOW_NAMESPACES = YES # The FILE_VERSION_FILTER tag can be used to specify a program or script that # doxygen should invoke to get the current version for each file (typically from # the version control system). Doxygen will invoke the program by executing (via # popen()) the command , where is the value of # the FILE_VERSION_FILTER tag, and is the name of an input file # provided by doxygen. Whatever the program writes to standard output # is used as the file version. See the manual for examples. FILE_VERSION_FILTER = # The LAYOUT_FILE tag can be used to specify a layout file which will be parsed by # doxygen. The layout file controls the global structure of the generated output files # in an output format independent way. The create the layout file that represents # doxygen's defaults, run doxygen with the -l option. You can optionally specify a # file name after the option, if omitted DoxygenLayout.xml will be used as the name # of the layout file. LAYOUT_FILE = #--------------------------------------------------------------------------- # configuration options related to warning and progress messages #--------------------------------------------------------------------------- # The QUIET tag can be used to turn on/off the messages that are generated # by doxygen. Possible values are YES and NO. If left blank NO is used. QUIET = NO # The WARNINGS tag can be used to turn on/off the warning messages that are # generated by doxygen. Possible values are YES and NO. If left blank # NO is used. WARNINGS = YES # If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings # for undocumented members. If EXTRACT_ALL is set to YES then this flag will # automatically be disabled. WARN_IF_UNDOCUMENTED = YES # If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for # potential errors in the documentation, such as not documenting some # parameters in a documented function, or documenting parameters that # don't exist or using markup commands wrongly. WARN_IF_DOC_ERROR = YES # This WARN_NO_PARAMDOC option can be abled to get warnings for # functions that are documented, but have no documentation for their parameters # or return value. If set to NO (the default) doxygen will only warn about # wrong or incomplete parameter documentation, but not about the absence of # documentation. WARN_NO_PARAMDOC = NO # The WARN_FORMAT tag determines the format of the warning messages that # doxygen can produce. The string should contain the $file, $line, and $text # tags, which will be replaced by the file and line number from which the # warning originated and the warning text. Optionally the format may contain # $version, which will be replaced by the version of the file (if it could # be obtained via FILE_VERSION_FILTER) WARN_FORMAT = "$file:$line: $text" # The WARN_LOGFILE tag can be used to specify a file to which warning # and error messages should be written. If left blank the output is written # to stderr. WARN_LOGFILE = #--------------------------------------------------------------------------- # configuration options related to the input files #--------------------------------------------------------------------------- # The INPUT tag can be used to specify the files and/or directories that contain # documented source files. You may enter file names like "myfile.cpp" or # directories like "/usr/src/myproject". Separate the files or directories # with spaces. INPUT = ../src # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is # also the default input encoding. Doxygen uses libiconv (or the iconv built # into libc) for the transcoding. See http://www.gnu.org/software/libiconv for # the list of possible encodings. INPUT_ENCODING = UTF-8 # If the value of the INPUT tag contains directories, you can use the # FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp # and *.h) to filter out the source-files in the directories. If left # blank the following patterns are tested: # *.c *.cc *.cxx *.cpp *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh *.hxx # *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.py *.f90 FILE_PATTERNS = # The RECURSIVE tag can be used to turn specify whether or not subdirectories # should be searched for input files as well. Possible values are YES and NO. # If left blank NO is used. RECURSIVE = YES # The EXCLUDE tag can be used to specify files and/or directories that should # excluded from the INPUT source files. This way you can easily exclude a # subdirectory from a directory tree whose root is specified with the INPUT tag. EXCLUDE = # The EXCLUDE_SYMLINKS tag can be used select whether or not files or # directories that are symbolic links (a Unix filesystem feature) are excluded # from the input. EXCLUDE_SYMLINKS = NO # If the value of the INPUT tag contains directories, you can use the # EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude # certain files from those directories. Note that the wildcards are matched # against the file with absolute path, so to exclude all test directories # for example use the pattern */test/* EXCLUDE_PATTERNS = # The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names # (namespaces, classes, functions, etc.) that should be excluded from the # output. The symbol name can be a fully qualified name, a word, or if the # wildcard * is used, a substring. Examples: ANamespace, AClass, # AClass::ANamespace, ANamespace::*Test EXCLUDE_SYMBOLS = # The EXAMPLE_PATH tag can be used to specify one or more files or # directories that contain example code fragments that are included (see # the \include command). EXAMPLE_PATH = example # If the value of the EXAMPLE_PATH tag contains directories, you can use the # EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp # and *.h) to filter out the source-files in the directories. If left # blank all files are included. EXAMPLE_PATTERNS = # If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be # searched for input files to be used with the \include or \dontinclude # commands irrespective of the value of the RECURSIVE tag. # Possible values are YES and NO. If left blank NO is used. EXAMPLE_RECURSIVE = YES # The IMAGE_PATH tag can be used to specify one or more files or # directories that contain image that are included in the documentation (see # the \image command). IMAGE_PATH = # The INPUT_FILTER tag can be used to specify a program that doxygen should # invoke to filter for each input file. Doxygen will invoke the filter program # by executing (via popen()) the command , where # is the value of the INPUT_FILTER tag, and is the name of an # input file. Doxygen will then use the output that the filter program writes # to standard output. # If FILTER_PATTERNS is specified, this tag will be # ignored. INPUT_FILTER = # The FILTER_PATTERNS tag can be used to specify filters on a per file pattern # basis. # Doxygen will compare the file name with each pattern and apply the # filter if there is a match. # The filters are a list of the form: # pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further # info on how filters are used. If FILTER_PATTERNS is empty, INPUT_FILTER # is applied to all files. FILTER_PATTERNS = # If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using # INPUT_FILTER) will be used to filter the input files when producing source # files to browse (i.e. when SOURCE_BROWSER is set to YES). FILTER_SOURCE_FILES = NO #--------------------------------------------------------------------------- # configuration options related to source browsing #--------------------------------------------------------------------------- # If the SOURCE_BROWSER tag is set to YES then a list of source files will # be generated. Documented entities will be cross-referenced with these sources. # Note: To get rid of all source code in the generated output, make sure also # VERBATIM_HEADERS is set to NO. SOURCE_BROWSER = NO # Setting the INLINE_SOURCES tag to YES will include the body # of functions and classes directly in the documentation. INLINE_SOURCES = NO # Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct # doxygen to hide any special comment blocks from generated source code # fragments. Normal C and C++ comments will always remain visible. STRIP_CODE_COMMENTS = YES # If the REFERENCED_BY_RELATION tag is set to YES # then for each documented function all documented # functions referencing it will be listed. REFERENCED_BY_RELATION = NO # If the REFERENCES_RELATION tag is set to YES # then for each documented function all documented entities # called/used by that function will be listed. REFERENCES_RELATION = NO # If the REFERENCES_LINK_SOURCE tag is set to YES (the default) # and SOURCE_BROWSER tag is set to YES, then the hyperlinks from # functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will # link to the source code. # Otherwise they will link to the documentation. REFERENCES_LINK_SOURCE = YES # If the USE_HTAGS tag is set to YES then the references to source code # will point to the HTML generated by the htags(1) tool instead of doxygen # built-in source browser. The htags tool is part of GNU's global source # tagging system (see http://www.gnu.org/software/global/global.html). You # will need version 4.8.6 or higher. USE_HTAGS = NO # If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen # will generate a verbatim copy of the header file for each class for # which an include is specified. Set to NO to disable this. VERBATIM_HEADERS = YES #--------------------------------------------------------------------------- # configuration options related to the alphabetical class index #--------------------------------------------------------------------------- # If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index # of all compounds will be generated. Enable this if the project # contains a lot of classes, structs, unions or interfaces. ALPHABETICAL_INDEX = NO # If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then # the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns # in which this list will be split (can be a number in the range [1..20]) COLS_IN_ALPHA_INDEX = 5 # In case all classes in a project start with a common prefix, all # classes will be put under the same header in the alphabetical index. # The IGNORE_PREFIX tag can be used to specify one or more prefixes that # should be ignored while generating the index headers. IGNORE_PREFIX = #--------------------------------------------------------------------------- # configuration options related to the HTML output #--------------------------------------------------------------------------- # If the GENERATE_HTML tag is set to YES (the default) Doxygen will # generate HTML output. GENERATE_HTML = YES # The HTML_OUTPUT tag is used to specify where the HTML docs will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `html' will be used as the default path. HTML_OUTPUT = html # The HTML_FILE_EXTENSION tag can be used to specify the file extension for # each generated HTML page (for example: .htm,.php,.asp). If it is left blank # doxygen will generate files with .html extension. HTML_FILE_EXTENSION = .html # The HTML_HEADER tag can be used to specify a personal HTML header for # each generated HTML page. If it is left blank doxygen will generate a # standard header. HTML_HEADER = # The HTML_FOOTER tag can be used to specify a personal HTML footer for # each generated HTML page. If it is left blank doxygen will generate a # standard footer. HTML_FOOTER = # The HTML_STYLESHEET tag can be used to specify a user-defined cascading # style sheet that is used by each HTML page. It can be used to # fine-tune the look of the HTML output. If the tag is left blank doxygen # will generate a default style sheet. Note that doxygen will try to copy # the style sheet file to the HTML output directory, so don't put your own # stylesheet in the HTML output directory as well, or it will be erased! HTML_STYLESHEET = # If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML # page will contain the date and time when the page was generated. Setting # this to NO can help when comparing the output of multiple runs. HTML_TIMESTAMP = YES # If the HTML_ALIGN_MEMBERS tag is set to YES, the members of classes, # files or namespaces will be aligned in HTML using tables. If set to # NO a bullet list will be used. HTML_ALIGN_MEMBERS = YES # If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML # documentation will contain sections that can be hidden and shown after the # page has loaded. For this to work a browser that supports # JavaScript and DHTML is required (for instance Mozilla 1.0+, Firefox # Netscape 6.0+, Internet explorer 5.0+, Konqueror, or Safari). HTML_DYNAMIC_SECTIONS = NO # If the GENERATE_DOCSET tag is set to YES, additional index files # will be generated that can be used as input for Apple's Xcode 3 # integrated development environment, introduced with OSX 10.5 (Leopard). # To create a documentation set, doxygen will generate a Makefile in the # HTML output directory. Running make will produce the docset in that # directory and running "make install" will install the docset in # ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find # it at startup. # See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html for more information. GENERATE_DOCSET = NO # When GENERATE_DOCSET tag is set to YES, this tag determines the name of the # feed. A documentation feed provides an umbrella under which multiple # documentation sets from a single provider (such as a company or product suite) # can be grouped. DOCSET_FEEDNAME = "Doxygen generated docs" # When GENERATE_DOCSET tag is set to YES, this tag specifies a string that # should uniquely identify the documentation set bundle. This should be a # reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen # will append .docset to the name. DOCSET_BUNDLE_ID = org.doxygen.Project # If the GENERATE_HTMLHELP tag is set to YES, additional index files # will be generated that can be used as input for tools like the # Microsoft HTML help workshop to generate a compiled HTML help file (.chm) # of the generated HTML documentation. GENERATE_HTMLHELP = NO # If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can # be used to specify the file name of the resulting .chm file. You # can add a path in front of the file if the result should not be # written to the html output directory. CHM_FILE = # If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can # be used to specify the location (absolute path including file name) of # the HTML help compiler (hhc.exe). If non-empty doxygen will try to run # the HTML help compiler on the generated index.hhp. HHC_LOCATION = # If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag # controls if a separate .chi index file is generated (YES) or that # it should be included in the master .chm file (NO). GENERATE_CHI = NO # If the GENERATE_HTMLHELP tag is set to YES, the CHM_INDEX_ENCODING # is used to encode HtmlHelp index (hhk), content (hhc) and project file # content. CHM_INDEX_ENCODING = # If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag # controls whether a binary table of contents is generated (YES) or a # normal table of contents (NO) in the .chm file. BINARY_TOC = NO # The TOC_EXPAND flag can be set to YES to add extra items for group members # to the contents of the HTML help documentation and to the tree view. TOC_EXPAND = NO # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and QHP_VIRTUAL_FOLDER # are set, an additional index file will be generated that can be used as input for # Qt's qhelpgenerator to generate a Qt Compressed Help (.qch) of the generated # HTML documentation. GENERATE_QHP = NO # If the QHG_LOCATION tag is specified, the QCH_FILE tag can # be used to specify the file name of the resulting .qch file. # The path specified is relative to the HTML output folder. QCH_FILE = # The QHP_NAMESPACE tag specifies the namespace to use when generating # Qt Help Project output. For more information please see # http://doc.trolltech.com/qthelpproject.html#namespace QHP_NAMESPACE = org.doxygen.Project # The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating # Qt Help Project output. For more information please see # http://doc.trolltech.com/qthelpproject.html#virtual-folders QHP_VIRTUAL_FOLDER = doc # If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to add. # For more information please see # http://doc.trolltech.com/qthelpproject.html#custom-filters QHP_CUST_FILTER_NAME = # The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the custom filter to add.For more information please see # Qt Help Project / Custom Filters. QHP_CUST_FILTER_ATTRS = # The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this project's # filter section matches. # Qt Help Project / Filter Attributes. QHP_SECT_FILTER_ATTRS = # If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can # be used to specify the location of Qt's qhelpgenerator. # If non-empty doxygen will try to run qhelpgenerator on the generated # .qhp file. QHG_LOCATION = # If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files # will be generated, which together with the HTML files, form an Eclipse help # plugin. To install this plugin and make it available under the help contents # menu in Eclipse, the contents of the directory containing the HTML and XML # files needs to be copied into the plugins directory of eclipse. The name of # the directory within the plugins directory should be the same as # the ECLIPSE_DOC_ID value. After copying Eclipse needs to be restarted before the help appears. GENERATE_ECLIPSEHELP = NO # A unique identifier for the eclipse help plugin. When installing the plugin # the directory name containing the HTML and XML files should also have # this name. ECLIPSE_DOC_ID = org.doxygen.Project # The DISABLE_INDEX tag can be used to turn on/off the condensed index at # top of each HTML page. The value NO (the default) enables the index and # the value YES disables it. DISABLE_INDEX = NO # This tag can be used to set the number of enum values (range [1..20]) # that doxygen will group on one line in the generated HTML documentation. ENUM_VALUES_PER_LINE = 4 # The GENERATE_TREEVIEW tag is used to specify whether a tree-like index # structure should be generated to display hierarchical information. # If the tag value is set to YES, a side panel will be generated # containing a tree-like index structure (just like the one that # is generated for HTML Help). For this to work a browser that supports # JavaScript, DHTML, CSS and frames is required (i.e. any modern browser). # Windows users are probably better off using the HTML help feature. GENERATE_TREEVIEW = NO # By enabling USE_INLINE_TREES, doxygen will generate the Groups, Directories, # and Class Hierarchy pages using a tree view instead of an ordered list. USE_INLINE_TREES = NO # If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be # used to set the initial width (in pixels) of the frame in which the tree # is shown. TREEVIEW_WIDTH = 250 # Use this tag to change the font size of Latex formulas included # as images in the HTML documentation. The default is 10. Note that # when you change the font size after a successful doxygen run you need # to manually remove any form_*.png images from the HTML output directory # to force them to be regenerated. FORMULA_FONTSIZE = 10 # When the SEARCHENGINE tag is enabled doxygen will generate a search box for the HTML output. The underlying search engine uses javascript # and DHTML and should work on any modern browser. Note that when using HTML help (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) there is already a search function so this one should # typically be disabled. For large projects the javascript based search engine # can be slow, then enabling SERVER_BASED_SEARCH may provide a better solution. SEARCHENGINE = YES # When the SERVER_BASED_SEARCH tag is enabled the search engine will be implemented using a PHP enabled web server instead of at the web client using Javascript. Doxygen will generate the search PHP script and index # file to put on the web server. The advantage of the server based approach is that it scales better to large projects and allows full text search. The disadvances is that it is more difficult to setup # and does not have live searching capabilities. SERVER_BASED_SEARCH = NO #--------------------------------------------------------------------------- # configuration options related to the LaTeX output #--------------------------------------------------------------------------- # If the GENERATE_LATEX tag is set to YES (the default) Doxygen will # generate Latex output. GENERATE_LATEX = YES # The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `latex' will be used as the default path. LATEX_OUTPUT = latex # The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be # invoked. If left blank `latex' will be used as the default command name. # Note that when enabling USE_PDFLATEX this option is only used for # generating bitmaps for formulas in the HTML output, but not in the # Makefile that is written to the output directory. LATEX_CMD_NAME = latex # The MAKEINDEX_CMD_NAME tag can be used to specify the command name to # generate index for LaTeX. If left blank `makeindex' will be used as the # default command name. MAKEINDEX_CMD_NAME = makeindex # If the COMPACT_LATEX tag is set to YES Doxygen generates more compact # LaTeX documents. This may be useful for small projects and may help to # save some trees in general. COMPACT_LATEX = NO # The PAPER_TYPE tag can be used to set the paper type that is used # by the printer. Possible values are: a4, a4wide, letter, legal and # executive. If left blank a4wide will be used. PAPER_TYPE = a4wide # The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX # packages that should be included in the LaTeX output. EXTRA_PACKAGES = # The LATEX_HEADER tag can be used to specify a personal LaTeX header for # the generated latex document. The header should contain everything until # the first chapter. If it is left blank doxygen will generate a # standard header. Notice: only use this tag if you know what you are doing! LATEX_HEADER = # If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated # is prepared for conversion to pdf (using ps2pdf). The pdf file will # contain links (just like the HTML output) instead of page references # This makes the output suitable for online browsing using a pdf viewer. PDF_HYPERLINKS = YES # If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of # plain latex in the generated Makefile. Set this option to YES to get a # higher quality PDF documentation. USE_PDFLATEX = YES # If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. # command to the generated LaTeX files. This will instruct LaTeX to keep # running if errors occur, instead of asking the user for help. # This option is also used when generating formulas in HTML. LATEX_BATCHMODE = NO # If LATEX_HIDE_INDICES is set to YES then doxygen will not # include the index chapters (such as File Index, Compound Index, etc.) # in the output. LATEX_HIDE_INDICES = NO # If LATEX_SOURCE_CODE is set to YES then doxygen will include source code with syntax highlighting in the LaTeX output. Note that which sources are shown also depends on other settings such as SOURCE_BROWSER. LATEX_SOURCE_CODE = NO #--------------------------------------------------------------------------- # configuration options related to the RTF output #--------------------------------------------------------------------------- # If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output # The RTF output is optimized for Word 97 and may not look very pretty with # other RTF readers or editors. GENERATE_RTF = NO # The RTF_OUTPUT tag is used to specify where the RTF docs will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `rtf' will be used as the default path. RTF_OUTPUT = rtf # If the COMPACT_RTF tag is set to YES Doxygen generates more compact # RTF documents. This may be useful for small projects and may help to # save some trees in general. COMPACT_RTF = NO # If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated # will contain hyperlink fields. The RTF file will # contain links (just like the HTML output) instead of page references. # This makes the output suitable for online browsing using WORD or other # programs which support those fields. # Note: wordpad (write) and others do not support links. RTF_HYPERLINKS = NO # Load stylesheet definitions from file. Syntax is similar to doxygen's # config file, i.e. a series of assignments. You only have to provide # replacements, missing definitions are set to their default value. RTF_STYLESHEET_FILE = # Set optional variables used in the generation of an rtf document. # Syntax is similar to doxygen's config file. RTF_EXTENSIONS_FILE = #--------------------------------------------------------------------------- # configuration options related to the man page output #--------------------------------------------------------------------------- # If the GENERATE_MAN tag is set to YES (the default) Doxygen will # generate man pages GENERATE_MAN = NO # The MAN_OUTPUT tag is used to specify where the man pages will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `man' will be used as the default path. MAN_OUTPUT = man # The MAN_EXTENSION tag determines the extension that is added to # the generated man pages (default is the subroutine's section .3) MAN_EXTENSION = .3 # If the MAN_LINKS tag is set to YES and Doxygen generates man output, # then it will generate one additional man file for each entity # documented in the real man page(s). These additional files # only source the real man page, but without them the man command # would be unable to find the correct page. The default is NO. MAN_LINKS = NO #--------------------------------------------------------------------------- # configuration options related to the XML output #--------------------------------------------------------------------------- # If the GENERATE_XML tag is set to YES Doxygen will # generate an XML file that captures the structure of # the code including all documentation. GENERATE_XML = NO # The XML_OUTPUT tag is used to specify where the XML pages will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `xml' will be used as the default path. XML_OUTPUT = xml # The XML_SCHEMA tag can be used to specify an XML schema, # which can be used by a validating XML parser to check the # syntax of the XML files. XML_SCHEMA = # The XML_DTD tag can be used to specify an XML DTD, # which can be used by a validating XML parser to check the # syntax of the XML files. XML_DTD = # If the XML_PROGRAMLISTING tag is set to YES Doxygen will # dump the program listings (including syntax highlighting # and cross-referencing information) to the XML output. Note that # enabling this will significantly increase the size of the XML output. XML_PROGRAMLISTING = YES #--------------------------------------------------------------------------- # configuration options for the AutoGen Definitions output #--------------------------------------------------------------------------- # If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will # generate an AutoGen Definitions (see autogen.sf.net) file # that captures the structure of the code including all # documentation. Note that this feature is still experimental # and incomplete at the moment. GENERATE_AUTOGEN_DEF = NO #--------------------------------------------------------------------------- # configuration options related to the Perl module output #--------------------------------------------------------------------------- # If the GENERATE_PERLMOD tag is set to YES Doxygen will # generate a Perl module file that captures the structure of # the code including all documentation. Note that this # feature is still experimental and incomplete at the # moment. GENERATE_PERLMOD = NO # If the PERLMOD_LATEX tag is set to YES Doxygen will generate # the necessary Makefile rules, Perl scripts and LaTeX code to be able # to generate PDF and DVI output from the Perl module output. PERLMOD_LATEX = NO # If the PERLMOD_PRETTY tag is set to YES the Perl module output will be # nicely formatted so it can be parsed by a human reader. # This is useful # if you want to understand what is going on. # On the other hand, if this # tag is set to NO the size of the Perl module output will be much smaller # and Perl will parse it just the same. PERLMOD_PRETTY = YES # The names of the make variables in the generated doxyrules.make file # are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. # This is useful so different doxyrules.make files included by the same # Makefile don't overwrite each other's variables. PERLMOD_MAKEVAR_PREFIX = #--------------------------------------------------------------------------- # Configuration options related to the preprocessor #--------------------------------------------------------------------------- # If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will # evaluate all C-preprocessor directives found in the sources and include # files. ENABLE_PREPROCESSING = YES # If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro # names in the source code. If set to NO (the default) only conditional # compilation will be performed. Macro expansion can be done in a controlled # way by setting EXPAND_ONLY_PREDEF to YES. MACRO_EXPANSION = NO # If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES # then the macro expansion is limited to the macros specified with the # PREDEFINED and EXPAND_AS_DEFINED tags. EXPAND_ONLY_PREDEF = NO # If the SEARCH_INCLUDES tag is set to YES (the default) the includes files # in the INCLUDE_PATH (see below) will be search if a #include is found. SEARCH_INCLUDES = YES # The INCLUDE_PATH tag can be used to specify one or more directories that # contain include files that are not input files but should be processed by # the preprocessor. INCLUDE_PATH = # You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard # patterns (like *.h and *.hpp) to filter out the header-files in the # directories. If left blank, the patterns specified with FILE_PATTERNS will # be used. INCLUDE_FILE_PATTERNS = # The PREDEFINED tag can be used to specify one or more macro names that # are defined before the preprocessor is started (similar to the -D option of # gcc). The argument of the tag is a list of macros of the form: name # or name=definition (no spaces). If the definition and the = are # omitted =1 is assumed. To prevent a macro definition from being # undefined via #undef or recursively expanded use the := operator # instead of the = operator. PREDEFINED = # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then # this tag can be used to specify a list of macro names that should be expanded. # The macro definition that is found in the sources will be used. # Use the PREDEFINED tag if you want to use a different macro definition. EXPAND_AS_DEFINED = # If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then # doxygen's preprocessor will remove all function-like macros that are alone # on a line, have an all uppercase name, and do not end with a semicolon. Such # function macros are typically used for boiler-plate code, and will confuse # the parser if not removed. SKIP_FUNCTION_MACROS = YES #--------------------------------------------------------------------------- # Configuration::additions related to external references #--------------------------------------------------------------------------- # The TAGFILES option can be used to specify one or more tagfiles. # Optionally an initial location of the external documentation # can be added for each tagfile. The format of a tag file without # this location is as follows: # # TAGFILES = file1 file2 ... # Adding location for the tag files is done as follows: # # TAGFILES = file1=loc1 "file2 = loc2" ... # where "loc1" and "loc2" can be relative or absolute paths or # URLs. If a location is present for each tag, the installdox tool # does not have to be run to correct the links. # Note that each tag file must have a unique name # (where the name does NOT include the path) # If a tag file is not located in the directory in which doxygen # is run, you must also specify the path to the tagfile here. TAGFILES = # When a file name is specified after GENERATE_TAGFILE, doxygen will create # a tag file that is based on the input files it reads. GENERATE_TAGFILE = # If the ALLEXTERNALS tag is set to YES all external classes will be listed # in the class index. If set to NO only the inherited external classes # will be listed. ALLEXTERNALS = NO # If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed # in the modules index. If set to NO, only the current project's groups will # be listed. EXTERNAL_GROUPS = YES # The PERL_PATH should be the absolute path and name of the perl script # interpreter (i.e. the result of `which perl'). PERL_PATH = /usr/bin/perl #--------------------------------------------------------------------------- # Configuration options related to the dot tool #--------------------------------------------------------------------------- # If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will # generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base # or super classes. Setting the tag to NO turns the diagrams off. Note that # this option is superseded by the HAVE_DOT option below. This is only a # fallback. It is recommended to install and use dot, since it yields more # powerful graphs. CLASS_DIAGRAMS = YES # You can define message sequence charts within doxygen comments using the \msc # command. Doxygen will then run the mscgen tool (see # http://www.mcternan.me.uk/mscgen/) to produce the chart and insert it in the # documentation. The MSCGEN_PATH tag allows you to specify the directory where # the mscgen tool resides. If left empty the tool is assumed to be found in the # default search path. MSCGEN_PATH = # If set to YES, the inheritance and collaboration graphs will hide # inheritance and usage relations if the target is undocumented # or is not a class. HIDE_UNDOC_RELATIONS = YES # If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is # available from the path. This tool is part of Graphviz, a graph visualization # toolkit from AT&T and Lucent Bell Labs. The other options in this section # have no effect if this option is set to NO (the default) HAVE_DOT = NO # By default doxygen will write a font called FreeSans.ttf to the output # directory and reference it in all dot files that doxygen generates. This # font does not include all possible unicode characters however, so when you need # these (or just want a differently looking font) you can specify the font name # using DOT_FONTNAME. You need need to make sure dot is able to find the font, # which can be done by putting it in a standard location or by setting the # DOTFONTPATH environment variable or by setting DOT_FONTPATH to the directory # containing the font. DOT_FONTNAME = FreeSans # The DOT_FONTSIZE tag can be used to set the size of the font of dot graphs. # The default size is 10pt. DOT_FONTSIZE = 10 # By default doxygen will tell dot to use the output directory to look for the # FreeSans.ttf font (which doxygen will put there itself). If you specify a # different font using DOT_FONTNAME you can set the path where dot # can find it using this tag. DOT_FONTPATH = # If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen # will generate a graph for each documented class showing the direct and # indirect inheritance relations. Setting this tag to YES will force the # the CLASS_DIAGRAMS tag to NO. CLASS_GRAPH = YES # If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen # will generate a graph for each documented class showing the direct and # indirect implementation dependencies (inheritance, containment, and # class references variables) of the class with other documented classes. COLLABORATION_GRAPH = YES # If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen # will generate a graph for groups, showing the direct groups dependencies GROUP_GRAPHS = YES # If the UML_LOOK tag is set to YES doxygen will generate inheritance and # collaboration diagrams in a style similar to the OMG's Unified Modeling # Language. UML_LOOK = NO # If set to YES, the inheritance and collaboration graphs will show the # relations between templates and their instances. TEMPLATE_RELATIONS = NO # If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT # tags are set to YES then doxygen will generate a graph for each documented # file showing the direct and indirect include dependencies of the file with # other documented files. INCLUDE_GRAPH = YES # If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and # HAVE_DOT tags are set to YES then doxygen will generate a graph for each # documented header file showing the documented files that directly or # indirectly include this file. INCLUDED_BY_GRAPH = YES # If the CALL_GRAPH and HAVE_DOT options are set to YES then # doxygen will generate a call dependency graph for every global function # or class method. Note that enabling this option will significantly increase # the time of a run. So in most cases it will be better to enable call graphs # for selected functions only using the \callgraph command. CALL_GRAPH = NO # If the CALLER_GRAPH and HAVE_DOT tags are set to YES then # doxygen will generate a caller dependency graph for every global function # or class method. Note that enabling this option will significantly increase # the time of a run. So in most cases it will be better to enable caller # graphs for selected functions only using the \callergraph command. CALLER_GRAPH = NO # If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen # will graphical hierarchy of all classes instead of a textual one. GRAPHICAL_HIERARCHY = YES # If the DIRECTORY_GRAPH, SHOW_DIRECTORIES and HAVE_DOT tags are set to YES # then doxygen will show the dependencies a directory has on other directories # in a graphical way. The dependency relations are determined by the #include # relations between the files in the directories. DIRECTORY_GRAPH = YES # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images # generated by dot. Possible values are png, jpg, or gif # If left blank png will be used. DOT_IMAGE_FORMAT = png # The tag DOT_PATH can be used to specify the path where the dot tool can be # found. If left blank, it is assumed the dot tool can be found in the path. DOT_PATH = # The DOTFILE_DIRS tag can be used to specify one or more directories that # contain dot files that are included in the documentation (see the # \dotfile command). DOTFILE_DIRS = # The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of # nodes that will be shown in the graph. If the number of nodes in a graph # becomes larger than this value, doxygen will truncate the graph, which is # visualized by representing a node as a red box. Note that doxygen if the # number of direct children of the root node in a graph is already larger than # DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note # that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. DOT_GRAPH_MAX_NODES = 50 # The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the # graphs generated by dot. A depth value of 3 means that only nodes reachable # from the root by following a path via at most 3 edges will be shown. Nodes # that lay further from the root node will be omitted. Note that setting this # option to 1 or 2 may greatly reduce the computation time needed for large # code bases. Also note that the size of a graph can be further restricted by # DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. MAX_DOT_GRAPH_DEPTH = 0 # Set the DOT_TRANSPARENT tag to YES to generate images with a transparent # background. This is disabled by default, because dot on Windows does not # seem to support this out of the box. Warning: Depending on the platform used, # enabling this option may lead to badly anti-aliased labels on the edges of # a graph (i.e. they become hard to read). DOT_TRANSPARENT = NO # Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output # files in one run (i.e. multiple -o and -T options on the command line). This # makes dot run faster, but since only newer versions of dot (>1.8.10) # support this, this feature is disabled by default. DOT_MULTI_TARGETS = YES # If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will # generate a legend page explaining the meaning of the various boxes and # arrows in the dot generated graphs. GENERATE_LEGEND = YES # If the DOT_CLEANUP tag is set to YES (the default) Doxygen will # remove the intermediate dot files that are used to generate # the various graphs. DOT_CLEANUP = YES guacamole-client-0.9.9/guacamole-common/doc/example/ExampleTunnelServlet.java0100664 0001751 0001751 00000005047 12645510255 031156 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import javax.servlet.http.HttpServletRequest; import net.sourceforge.guacamole.GuacamoleException; import net.sourceforge.guacamole.properties.GuacamoleProperties; import net.sourceforge.guacamole.net.GuacamoleSocket; import net.sourceforge.guacamole.net.GuacamoleTunnel; import net.sourceforge.guacamole.net.InetGuacamoleSocket; import net.sourceforge.guacamole.protocol.GuacamoleConfiguration; import net.sourceforge.guacamole.protocol.ConfiguredGuacamoleSocket; import net.sourceforge.guacamole.servlet.GuacamoleHTTPTunnelServlet; public class ExampleTunnelServlet extends GuacamoleHTTPTunnelServlet { @Override protected GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException { String hostname = GuacamoleProperties.getProperty( GuacamoleProperties.GUACD_HOSTNAME); int port = GuacamoleProperties.getProperty( GuacamoleProperties.GUACD_PORT); GuacamoleConfiguration config = new GuacamoleConfiguration(); config.setProtocol("vnc"); config.setParameter("hostname", "localhost"); config.setParameter("port", "5901"); config.setParameter("password", "potato"); GuacamoleSocket socket = new ConfiguredGuacamoleSocket( new InetGuacamoleSocket(hostname, port), config ); // Create and return tunnel GuacamoleTunnel tunnel = new GuacamoleTunnel(socket); return tunnel; } } ././@LongLink0100644 0000000 0000000 00000000154 12471273272 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleServerException.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleServerExcepti0100664 0001751 0001751 00000004710 12471273272 035513 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole; import org.glyptodon.guacamole.protocol.GuacamoleStatus; /** * A generic exception thrown when part of the Guacamole API encounters * an unexpected, internal error. An internal error, if correctable, would * require correction on the server side, not the client. * * @author Michael Jumper */ public class GuacamoleServerException extends GuacamoleException { /** * Creates a new GuacamoleServerException with the given message and cause. * * @param message A human readable description of the exception that * occurred. * @param cause The cause of this exception. */ public GuacamoleServerException(String message, Throwable cause) { super(message, cause); } /** * Creates a new GuacamoleServerException with the given message. * * @param message A human readable description of the exception that * occurred. */ public GuacamoleServerException(String message) { super(message); } /** * Creates a new GuacamoleServerException with the given cause. * * @param cause The cause of this exception. */ public GuacamoleServerException(Throwable cause) { super(cause); } @Override public GuacamoleStatus getStatus() { return GuacamoleStatus.SERVER_ERROR; } } ././@LongLink0100644 0000000 0000000 00000000165 12471273272 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleUpstreamTimeoutException.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleUpstreamTimeo0100664 0001751 0001751 00000004660 12471273272 035525 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole; import org.glyptodon.guacamole.protocol.GuacamoleStatus; /** * An exception which indicates than an upstream server (such as the remote * desktop) is taking too long to respond. * * @author Michael Jumper */ public class GuacamoleUpstreamTimeoutException extends GuacamoleUpstreamException { /** * Creates a new GuacamoleUpstreamException with the given message and * cause. * * @param message A human readable description of the exception that * occurred. * @param cause The cause of this exception. */ public GuacamoleUpstreamTimeoutException(String message, Throwable cause) { super(message, cause); } /** * Creates a new GuacamoleUpstreamException with the given message. * * @param message A human readable description of the exception that * occurred. */ public GuacamoleUpstreamTimeoutException(String message) { super(message); } /** * Creates a new GuacamoleUpstreamException with the given cause. * * @param cause The cause of this exception. */ public GuacamoleUpstreamTimeoutException(Throwable cause) { super(cause); } @Override public GuacamoleStatus getStatus() { return GuacamoleStatus.UPSTREAM_TIMEOUT; } } ././@LongLink0100644 0000000 0000000 00000000163 12471273272 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleClientBadTypeException.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleClientBadType0100664 0001751 0001751 00000004624 12471273272 035416 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole; import org.glyptodon.guacamole.protocol.GuacamoleStatus; /** * An exception which is thrown when data has been submitted with an unsupported * mimetype. * * @author Michael Jumper */ public class GuacamoleClientBadTypeException extends GuacamoleClientException { /** * Creates a new GuacamoleClientBadTypeException with the given message and cause. * * @param message A human readable description of the exception that * occurred. * @param cause The cause of this exception. */ public GuacamoleClientBadTypeException(String message, Throwable cause) { super(message, cause); } /** * Creates a new GuacamoleClientBadTypeException with the given message. * * @param message A human readable description of the exception that * occurred. */ public GuacamoleClientBadTypeException(String message) { super(message); } /** * Creates a new GuacamoleClientBadTypeException with the given cause. * * @param cause The cause of this exception. */ public GuacamoleClientBadTypeException(Throwable cause) { super(cause); } @Override public GuacamoleStatus getStatus() { return GuacamoleStatus.CLIENT_BAD_TYPE; } } ././@LongLink0100644 0000000 0000000 00000000163 12471273272 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleClientOverrunException.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleClientOverrun0100664 0001751 0001751 00000005043 12471273272 035522 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole; import org.glyptodon.guacamole.protocol.GuacamoleStatus; /** * An exception which is thrown when the client has sent too much data. This * usually indicates that a server-side buffer is not large enough to * accommodate the data, or protocol specifications prohibit data of the size * received. * * @author Michael Jumper */ public class GuacamoleClientOverrunException extends GuacamoleClientException { /** * Creates a new GuacamoleClientOverrunException with the given message and cause. * * @param message A human readable description of the exception that * occurred. * @param cause The cause of this exception. */ public GuacamoleClientOverrunException(String message, Throwable cause) { super(message, cause); } /** * Creates a new GuacamoleClientOverrunException with the given message. * * @param message A human readable description of the exception that * occurred. */ public GuacamoleClientOverrunException(String message) { super(message); } /** * Creates a new GuacamoleClientOverrunException with the given cause. * * @param cause The cause of this exception. */ public GuacamoleClientOverrunException(Throwable cause) { super(cause); } @Override public GuacamoleStatus getStatus() { return GuacamoleStatus.CLIENT_OVERRUN; } } ././@LongLink0100644 0000000 0000000 00000000154 12471273272 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleStatus.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleStat0100664 0001751 0001751 00000011664 12471273272 035505 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.protocol; /** * All possible statuses returned by various Guacamole instructions, each having * a corresponding code. * * @author Michael Jumper */ public enum GuacamoleStatus { /** * The operation succeeded. */ SUCCESS(200, 1000, 0x0000), /** * The requested operation is unsupported. */ UNSUPPORTED(501, 1011, 0x0100), /** * The operation could not be performed due to an internal failure. */ SERVER_ERROR(500, 1011, 0x0200), /** * The operation could not be performed as the server is busy. */ SERVER_BUSY(503, 1008, 0x0201), /** * The operation could not be performed because the upstream server is not * responding. */ UPSTREAM_TIMEOUT(504, 1011, 0x0202), /** * The operation was unsuccessful due to an error or otherwise unexpected * condition of the upstream server. */ UPSTREAM_ERROR(502, 1011, 0x0203), /** * The operation could not be performed as the requested resource does not * exist. */ RESOURCE_NOT_FOUND(404, 1002, 0x0204), /** * The operation could not be performed as the requested resource is already * in use. */ RESOURCE_CONFLICT(409, 1008, 0x0205), /** * The operation could not be performed because bad parameters were given. */ CLIENT_BAD_REQUEST(400, 1002, 0x0300), /** * Permission was denied to perform the operation, as the user is not yet * authorized (not yet logged in, for example). As HTTP 401 has implications * for HTTP-specific authorization schemes, this status continues to map to * HTTP 403 ("Forbidden"). To do otherwise would risk unintended effects. */ CLIENT_UNAUTHORIZED(403, 1008, 0x0301), /** * Permission was denied to perform the operation, and this operation will * not be granted even if the user is authorized. */ CLIENT_FORBIDDEN(403, 1008, 0x0303), /** * The client took too long to respond. */ CLIENT_TIMEOUT(408, 1002, 0x0308), /** * The client sent too much data. */ CLIENT_OVERRUN(413, 1009, 0x030D), /** * The client sent data of an unsupported or unexpected type. */ CLIENT_BAD_TYPE(415, 1003, 0x030F), /** * The operation failed because the current client is already using too * many resources. */ CLIENT_TOO_MANY(429, 1008, 0x031D); /** * The most applicable HTTP error code. */ private final int http_code; /** * The most applicable WebSocket error code. */ private final int websocket_code; /** * The Guacamole protocol status code. */ private final int guac_code; /** * Initializes a GuacamoleStatusCode with the given HTTP and Guacamole * status/error code values. * * @param http_code The most applicable HTTP error code. * @param websocket_code The most applicable WebSocket error code. * @param guac_code The Guacamole protocol status code. */ private GuacamoleStatus(int http_code, int websocket_code, int guac_code) { this.http_code = http_code; this.websocket_code = websocket_code; this.guac_code = guac_code; } /** * Returns the most applicable HTTP error code. * * @return The most applicable HTTP error code. */ public int getHttpStatusCode() { return http_code; } /** * Returns the most applicable HTTP error code. * * @return The most applicable HTTP error code. */ public int getWebSocketCode() { return websocket_code; } /** * Returns the corresponding Guacamole protocol status code. * * @return The corresponding Guacamole protocol status code. */ public int getGuacamoleStatusCode() { return guac_code; } } ././@LongLink0100644 0000000 0000000 00000000154 12471273272 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleFilter.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleFilt0100664 0001751 0001751 00000004337 12471273272 035467 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.protocol; import org.glyptodon.guacamole.GuacamoleException; /** * Interface which provides for the filtering of individual instructions. Each * filtered instruction may be allowed through untouched, modified, replaced, * dropped, or explicitly denied. * * @author Michael Jumper */ public interface GuacamoleFilter { /** * Applies the filter to the given instruction, returning the original * instruction, a modified version of the original, or null, depending * on the implementation. * * @param instruction The instruction to filter. * @return The original instruction, if the instruction is to be allowed, * a modified version of the instruction, if the instruction is * to be overridden, or null, if the instruction is to be dropped. * @throws GuacamoleException If an error occurs filtering the instruction, * or if the instruction must be explicitly * denied. */ public GuacamoleInstruction filter(GuacamoleInstruction instruction) throws GuacamoleException; } ././@LongLink0100644 0000000 0000000 00000000166 12645510255 011643 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/ConfiguredGuacamoleSocket.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/ConfiguredGua0100664 0001751 0001751 00000021333 12645510255 035466 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.protocol; import java.util.List; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleServerException; import org.glyptodon.guacamole.io.GuacamoleReader; import org.glyptodon.guacamole.io.GuacamoleWriter; import org.glyptodon.guacamole.net.GuacamoleSocket; /** * A GuacamoleSocket which pre-configures the connection based on a given * GuacamoleConfiguration, completing the initial protocol handshake before * accepting data for read or write. * * This is useful for forcing a connection to the Guacamole proxy server with * a specific configuration while disallowing the client that will be using * this GuacamoleSocket from manually controlling the initial protocol * handshake. * * @author Michael Jumper */ public class ConfiguredGuacamoleSocket implements GuacamoleSocket { /** * The wrapped socket. */ private GuacamoleSocket socket; /** * The configuration to use when performing the Guacamole protocol * handshake. */ private GuacamoleConfiguration config; /** * The unique identifier associated with this connection, as determined * by the "ready" instruction received from the Guacamole proxy. */ private String id; /** * Waits for the instruction having the given opcode, returning that * instruction once it has been read. If the instruction is never read, * an exception is thrown. * * @param reader The reader to read instructions from. * @param opcode The opcode of the instruction we are expecting. * @return The instruction having the given opcode. * @throws GuacamoleException If an error occurs while reading, or if * the expected instruction is not read. */ private GuacamoleInstruction expect(GuacamoleReader reader, String opcode) throws GuacamoleException { // Wait for an instruction GuacamoleInstruction instruction = reader.readInstruction(); if (instruction == null) throw new GuacamoleServerException("End of stream while waiting for \"" + opcode + "\"."); // Ensure instruction has expected opcode if (!instruction.getOpcode().equals(opcode)) throw new GuacamoleServerException("Expected \"" + opcode + "\" instruction but instead received \"" + instruction.getOpcode() + "\"."); return instruction; } /** * Creates a new ConfiguredGuacamoleSocket which uses the given * GuacamoleConfiguration to complete the initial protocol handshake over * the given GuacamoleSocket. A default GuacamoleClientInformation object * is used to provide basic client information. * * @param socket The GuacamoleSocket to wrap. * @param config The GuacamoleConfiguration to use to complete the initial * protocol handshake. * @throws GuacamoleException If an error occurs while completing the * initial protocol handshake. */ public ConfiguredGuacamoleSocket(GuacamoleSocket socket, GuacamoleConfiguration config) throws GuacamoleException { this(socket, config, new GuacamoleClientInformation()); } /** * Creates a new ConfiguredGuacamoleSocket which uses the given * GuacamoleConfiguration and GuacamoleClientInformation to complete the * initial protocol handshake over the given GuacamoleSocket. * * @param socket The GuacamoleSocket to wrap. * @param config The GuacamoleConfiguration to use to complete the initial * protocol handshake. * @param info The GuacamoleClientInformation to use to complete the initial * protocol handshake. * @throws GuacamoleException If an error occurs while completing the * initial protocol handshake. */ public ConfiguredGuacamoleSocket(GuacamoleSocket socket, GuacamoleConfiguration config, GuacamoleClientInformation info) throws GuacamoleException { this.socket = socket; this.config = config; // Get reader and writer GuacamoleReader reader = socket.getReader(); GuacamoleWriter writer = socket.getWriter(); // Get protocol / connection ID String select_arg = config.getConnectionID(); if (select_arg == null) select_arg = config.getProtocol(); // Send requested protocol or connection ID writer.writeInstruction(new GuacamoleInstruction("select", select_arg)); // Wait for server args GuacamoleInstruction args = expect(reader, "args"); // Build args list off provided names and config List arg_names = args.getArgs(); String[] arg_values = new String[arg_names.size()]; for (int i=0; i ready_args = ready.getArgs(); if (ready_args.isEmpty()) throw new GuacamoleServerException("No connection ID received"); id = ready.getArgs().get(0); } /** * Returns the GuacamoleConfiguration used to configure this * ConfiguredGuacamoleSocket. * * @return The GuacamoleConfiguration used to configure this * ConfiguredGuacamoleSocket. */ public GuacamoleConfiguration getConfiguration() { return config; } /** * Returns the unique ID associated with the Guacamole connection * negotiated by this ConfiguredGuacamoleSocket. The ID is provided by * the "ready" instruction returned by the Guacamole proxy. * * @return The ID of the negotiated Guacamole connection. */ public String getConnectionID() { return id; } @Override public GuacamoleWriter getWriter() { return socket.getWriter(); } @Override public GuacamoleReader getReader() { return socket.getReader(); } @Override public void close() throws GuacamoleException { socket.close(); } @Override public boolean isOpen() { return socket.isOpen(); } } ././@LongLink0100644 0000000 0000000 00000000163 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleConfiguration.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleConf0100664 0001751 0001751 00000014077 12612226251 035450 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.protocol; import java.io.Serializable; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; /** * All information necessary to complete the initial protocol handshake of a * Guacamole session. * * @author Michael Jumper */ public class GuacamoleConfiguration implements Serializable { /** * Identifier unique to this version of GuacamoleConfiguration. */ private static final long serialVersionUID = 1L; /** * The ID of the connection being joined. If this value is present, * the protocol need not be specified. */ private String connectionID; /** * The name of the protocol associated with this configuration. */ private String protocol; /** * Map of all associated parameter values, indexed by parameter name. */ private final Map parameters = new HashMap(); /** * Creates a new, blank GuacamoleConfiguration with its protocol, connection * ID, and parameters unset. */ public GuacamoleConfiguration() { } /** * Copies the given GuacamoleConfiguration, creating a new, indepedent * GuacamoleConfiguration containing the same protocol, connection ID, * and parameter values, if any. * * @param config The GuacamoleConfiguration to copy. */ public GuacamoleConfiguration(GuacamoleConfiguration config) { // Copy protocol and connection ID protocol = config.getProtocol(); connectionID = config.getConnectionID(); // Copy parameter values for (String name : config.getParameterNames()) parameters.put(name, config.getParameter(name)); } /** * Returns the ID of the connection being joined, if any. If no connection * is being joined, this returns null, and the protocol must be set. * * @return The ID of the connection being joined, or null if no connection * is being joined. */ public String getConnectionID() { return connectionID; } /** * Sets the ID of the connection being joined, if any. If no connection * is being joined, this value must be omitted, and the protocol must be * set instead. * * @param connectionID The ID of the connection being joined. */ public void setConnectionID(String connectionID) { this.connectionID = connectionID; } /** * Returns the name of the protocol to be used. * @return The name of the protocol to be used. */ public String getProtocol() { return protocol; } /** * Sets the name of the protocol to be used. * @param protocol The name of the protocol to be used. */ public void setProtocol(String protocol) { this.protocol = protocol; } /** * Returns the value set for the parameter with the given name, if any. * @param name The name of the parameter to return the value for. * @return The value of the parameter with the given name, or null if * that parameter has not been set. */ public String getParameter(String name) { return parameters.get(name); } /** * Sets the value for the parameter with the given name. * * @param name The name of the parameter to set the value for. * @param value The value to set for the parameter with the given name. */ public void setParameter(String name, String value) { parameters.put(name, value); } /** * Removes the value set for the parameter with the given name. * * @param name The name of the parameter to remove the value of. */ public void unsetParameter(String name) { parameters.remove(name); } /** * Returns a set of all currently defined parameter names. Each name * corresponds to a parameter that has a value set on this * GuacamoleConfiguration via setParameter(). * * @return A set of all currently defined parameter names. */ public Set getParameterNames() { return Collections.unmodifiableSet(parameters.keySet()); } /** * Returns a map which contains parameter name/value pairs as key/value * pairs. Changes to this map will affect the parameters stored within * this configuration. * * @return * A map which contains all parameter name/value pairs as key/value * pairs. */ public Map getParameters() { return parameters; } /** * Replaces all current parameters with the parameters defined within the * given map. Key/value pairs within the map represent parameter name/value * pairs. * * @param parameters * A map which contains all parameter name/value pairs as key/value * pairs. */ public void setParameters(Map parameters) { this.parameters.clear(); this.parameters.putAll(parameters); } } ././@LongLink0100644 0000000 0000000 00000000167 12645510255 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleClientInformation.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleClie0100664 0001751 0001751 00000011552 12645510255 035440 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.protocol; import java.util.ArrayList; import java.util.List; /** * An abstract representation of Guacamole client information, including all * information required by the Guacamole protocol during the preamble. * * @author Michael Jumper */ public class GuacamoleClientInformation { /** * The optimal screen width requested by the client, in pixels. */ private int optimalScreenWidth = 1024; /** * The optimal screen height requested by the client, in pixels. */ private int optimalScreenHeight = 768; /** * The resolution of the optimal dimensions given, in DPI. */ private int optimalResolution = 96; /** * The list of audio mimetypes reported by the client to be supported. */ private final List audioMimetypes = new ArrayList(); /** * The list of video mimetypes reported by the client to be supported. */ private final List videoMimetypes = new ArrayList(); /** * The list of image mimetypes reported by the client to be supported. */ private final List imageMimetypes = new ArrayList(); /** * Returns the optimal screen width requested by the client, in pixels. * @return The optimal screen width requested by the client, in pixels. */ public int getOptimalScreenWidth() { return optimalScreenWidth; } /** * Sets the client's optimal screen width. * @param optimalScreenWidth The optimal screen width of the client. */ public void setOptimalScreenWidth(int optimalScreenWidth) { this.optimalScreenWidth = optimalScreenWidth; } /** * Returns the optimal screen height requested by the client, in pixels. * @return The optimal screen height requested by the client, in pixels. */ public int getOptimalScreenHeight() { return optimalScreenHeight; } /** * Sets the client's optimal screen height. * @param optimalScreenHeight The optimal screen height of the client. */ public void setOptimalScreenHeight(int optimalScreenHeight) { this.optimalScreenHeight = optimalScreenHeight; } /** * Returns the resolution of the screen if the optimal width and height are * used, in DPI. * * @return The optimal screen resolution. */ public int getOptimalResolution() { return optimalResolution; } /** * Sets the resolution of the screen if the optimal width and height are * used, in DPI. * * @param optimalResolution The optimal screen resolution in DPI. */ public void setOptimalResolution(int optimalResolution) { this.optimalResolution = optimalResolution; } /** * Returns the list of audio mimetypes supported by the client. To add or * removed supported mimetypes, the list returned by this function can be * modified. * * @return The set of audio mimetypes supported by the client. */ public List getAudioMimetypes() { return audioMimetypes; } /** * Returns the list of video mimetypes supported by the client. To add or * removed supported mimetypes, the list returned by this function can be * modified. * * @return The set of video mimetypes supported by the client. */ public List getVideoMimetypes() { return videoMimetypes; } /** * Returns the list of image mimetypes supported by the client. To add or * removed supported mimetypes, the list returned by this function can be * modified. * * @return * The set of image mimetypes supported by the client. */ public List getImageMimetypes() { return imageMimetypes; } } ././@LongLink0100644 0000000 0000000 00000000151 12471273272 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/package-info.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/package-info.0100664 0001751 0001751 00000002312 12471273272 035344 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes relating directly to the Guacamole protocol. */ package org.glyptodon.guacamole.protocol; ././@LongLink0100644 0000000 0000000 00000000164 12471273272 011643 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/FilteredGuacamoleReader.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/FilteredGuaca0100664 0001751 0001751 00000006321 12471273272 035445 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.protocol; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.io.GuacamoleReader; /** * GuacamoleReader which applies a given GuacamoleFilter to observe or alter all * read instructions. Instructions may also be dropped or denied by the the * filter. * * @author Michael Jumper */ public class FilteredGuacamoleReader implements GuacamoleReader { /** * The wrapped GuacamoleReader. */ private final GuacamoleReader reader; /** * The filter to apply when reading instructions. */ private final GuacamoleFilter filter; /** * Wraps the given GuacamoleReader, applying the given filter to all read * instructions. Future reads will return only instructions which pass * the filter. * * @param reader The GuacamoleReader to wrap. * @param filter The filter which dictates which instructions are read, and * how. */ public FilteredGuacamoleReader(GuacamoleReader reader, GuacamoleFilter filter) { this.reader = reader; this.filter = filter; } @Override public boolean available() throws GuacamoleException { return reader.available(); } @Override public char[] read() throws GuacamoleException { GuacamoleInstruction filteredInstruction = readInstruction(); if (filteredInstruction == null) return null; return filteredInstruction.toString().toCharArray(); } @Override public GuacamoleInstruction readInstruction() throws GuacamoleException { GuacamoleInstruction filteredInstruction; // Read and filter instructions until no instructions are dropped do { // Read next instruction GuacamoleInstruction unfilteredInstruction = reader.readInstruction(); if (unfilteredInstruction == null) return null; // Apply filter filteredInstruction = filter.filter(unfilteredInstruction); } while (filteredInstruction == null); return filteredInstruction; } } ././@LongLink0100644 0000000 0000000 00000000164 12471273272 011643 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/FilteredGuacamoleWriter.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/FilteredGuaca0100664 0001751 0001751 00000007230 12471273272 035445 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.protocol; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleServerException; import org.glyptodon.guacamole.io.GuacamoleWriter; /** * GuacamoleWriter which applies a given GuacamoleFilter to observe or alter * all written instructions. Instructions may also be dropped or denied by * the filter. * * @author Michael Jumper */ public class FilteredGuacamoleWriter implements GuacamoleWriter { /** * The wrapped GuacamoleWriter. */ private final GuacamoleWriter writer; /** * The filter to apply when writing instructions. */ private final GuacamoleFilter filter; /** * Parser for reading instructions prior to writing, such that they can be * passed on to the filter. */ private final GuacamoleParser parser = new GuacamoleParser(); /** * Wraps the given GuacamoleWriter, applying the given filter to all written * instructions. Future writes will only write instructions which pass * the filter. * * @param writer The GuacamoleWriter to wrap. * @param filter The filter which dictates which instructions are written, * and how. */ public FilteredGuacamoleWriter(GuacamoleWriter writer, GuacamoleFilter filter) { this.writer = writer; this.filter = filter; } @Override public void write(char[] chunk, int offset, int length) throws GuacamoleException { // Write all data in chunk while (length > 0) { // Pass as much data through the parser as possible int parsed; while ((parsed = parser.append(chunk, offset, length)) != 0) { offset += parsed; length -= parsed; } // If no instruction is available, it must be incomplete if (!parser.hasNext()) throw new GuacamoleServerException("Filtered write() contained an incomplete instruction."); // Write single instruction through filter writeInstruction(parser.next()); } } @Override public void write(char[] chunk) throws GuacamoleException { write(chunk, 0, chunk.length); } @Override public void writeInstruction(GuacamoleInstruction instruction) throws GuacamoleException { // Write instruction only if not dropped GuacamoleInstruction filteredInstruction = filter.filter(instruction); if (filteredInstruction != null) writer.writeInstruction(filteredInstruction); } } ././@LongLink0100644 0000000 0000000 00000000161 12555526005 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleInstruction.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleInst0100664 0001751 0001751 00000007651 12555526005 035506 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.protocol; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * An abstract representation of a Guacamole instruction, as defined by the * Guacamole protocol. * * @author Michael Jumper */ public class GuacamoleInstruction { /** * The opcode of this instruction. */ private String opcode; /** * All arguments of this instruction, in order. */ private List args; /** * Creates a new GuacamoleInstruction having the given Operation and * list of arguments values. * * @param opcode The opcode of the instruction to create. * @param args The list of argument values to provide in the new * instruction if any. */ public GuacamoleInstruction(String opcode, String... args) { this.opcode = opcode; this.args = Collections.unmodifiableList(Arrays.asList(args)); } /** * Creates a new GuacamoleInstruction having the given Operation and * list of arguments values. The list given will be used to back the * internal list of arguments and the list returned by getArgs(). * * @param opcode The opcode of the instruction to create. * @param args The list of argument values to provide in the new * instruction if any. */ public GuacamoleInstruction(String opcode, List args) { this.opcode = opcode; this.args = Collections.unmodifiableList(args); } /** * Returns the opcode associated with this GuacamoleInstruction. * @return The opcode associated with this GuacamoleInstruction. */ public String getOpcode() { return opcode; } /** * Returns a List of all argument values specified for this * GuacamoleInstruction. Note that the List returned is immutable. * Attempts to modify the list will result in exceptions. * * @return A List of all argument values specified for this * GuacamoleInstruction. */ public List getArgs() { return args; } /** * Returns this GuacamoleInstruction in the form it would be sent over the * Guacamole protocol. * * @return This GuacamoleInstruction in the form it would be sent over the * Guacamole protocol. */ @Override public String toString() { StringBuilder buff = new StringBuilder(); // Write opcode buff.append(opcode.length()); buff.append('.'); buff.append(opcode); // Write argument values for (String value : args) { buff.append(','); buff.append(value.length()); buff.append('.'); buff.append(value); } // Write terminator buff.append(';'); return buff.toString(); } } ././@LongLink0100644 0000000 0000000 00000000164 12471273272 011643 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/FilteredGuacamoleSocket.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/FilteredGuaca0100664 0001751 0001751 00000006565 12471273272 035457 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.protocol; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.io.GuacamoleReader; import org.glyptodon.guacamole.io.GuacamoleWriter; import org.glyptodon.guacamole.net.GuacamoleSocket; /** * Implementation of GuacamoleSocket which allows individual instructions to be * intercepted, overridden, etc. * * @author Michael Jumper */ public class FilteredGuacamoleSocket implements GuacamoleSocket { /** * Wrapped GuacamoleSocket. */ private final GuacamoleSocket socket; /** * A reader for the wrapped GuacamoleSocket which may be filtered. */ private final GuacamoleReader reader; /** * A writer for the wrapped GuacamoleSocket which may be filtered. */ private final GuacamoleWriter writer; /** * Creates a new FilteredGuacamoleSocket which uses the given filters to * determine whether instructions read/written are allowed through, * modified, etc. If reads or writes should be unfiltered, simply specify * null rather than a particular filter. * * @param socket The GuacamoleSocket to wrap. * @param readFilter The GuacamoleFilter to apply to all read instructions, * if any. * @param writeFilter The GuacamoleFilter to apply to all written * instructions, if any. */ public FilteredGuacamoleSocket(GuacamoleSocket socket, GuacamoleFilter readFilter, GuacamoleFilter writeFilter) { this.socket = socket; // Apply filter to reader if (readFilter != null) reader = new FilteredGuacamoleReader(socket.getReader(), readFilter); else reader = socket.getReader(); // Apply filter to writer if (writeFilter != null) writer = new FilteredGuacamoleWriter(socket.getWriter(), writeFilter); else writer = socket.getWriter(); } @Override public GuacamoleReader getReader() { return reader; } @Override public GuacamoleWriter getWriter() { return writer; } @Override public void close() throws GuacamoleException { socket.close(); } @Override public boolean isOpen() { return socket.isOpen(); } } ././@LongLink0100644 0000000 0000000 00000000154 12555526005 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleParser.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamolePars0100664 0001751 0001751 00000017264 12555526005 035477 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.protocol; import java.util.Arrays; import java.util.Iterator; import java.util.List; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleServerException; /** * Parser for the Guacamole protocol. Arbitrary instruction data is appended, * and instructions are returned as a result. Invalid instructions result in * exceptions. * * @author Michael Jumper */ public class GuacamoleParser implements Iterator { /** * The maximum number of characters per instruction. */ public static final int INSTRUCTION_MAX_LENGTH = 8192; /** * The maximum number of digits to allow per length prefix. */ public static final int INSTRUCTION_MAX_DIGITS = 5; /** * The maximum number of elements per instruction, including the opcode. */ public static final int INSTRUCTION_MAX_ELEMENTS = 64; /** * All possible states of the instruction parser. */ private enum State { /** * The parser is currently waiting for data to complete the length prefix * of the current element of the instruction. */ PARSING_LENGTH, /** * The parser has finished reading the length prefix and is currently * waiting for data to complete the content of the instruction. */ PARSING_CONTENT, /** * The instruction has been fully parsed. */ COMPLETE, /** * The instruction cannot be parsed because of a protocol error. */ ERROR } /** * The latest parsed instruction, if any. */ private GuacamoleInstruction parsedInstruction; /** * The parse state of the instruction. */ private State state = State.PARSING_LENGTH; /** * The length of the current element, if known. */ private int elementLength = 0; /** * The number of elements currently parsed. */ private int elementCount = 0; /** * All currently parsed elements. */ private final String elements[] = new String[INSTRUCTION_MAX_ELEMENTS]; /** * Appends data from the given buffer to the current instruction. * * @param chunk The buffer containing the data to append. * @param offset The offset within the buffer where the data begins. * @param length The length of the data to append. * @return The number of characters appended, or 0 if complete instructions * have already been parsed and must be read via next() before * more data can be appended. * @throws GuacamoleException If an error occurs while parsing the new data. */ public int append(char chunk[], int offset, int length) throws GuacamoleException { int charsParsed = 0; // Do not exceed maximum number of elements if (elementCount == INSTRUCTION_MAX_ELEMENTS && state != State.COMPLETE) { state = State.ERROR; throw new GuacamoleServerException("Instruction contains too many elements."); } // Parse element length if (state == State.PARSING_LENGTH) { int parsedLength = elementLength; while (charsParsed < length) { // Pull next character char c = chunk[offset + charsParsed++]; // If digit, add to length if (c >= '0' && c <= '9') parsedLength = parsedLength*10 + c - '0'; // If period, switch to parsing content else if (c == '.') { state = State.PARSING_CONTENT; break; } // If not digit, parse error else { state = State.ERROR; throw new GuacamoleServerException("Non-numeric character in element length."); } } // If too long, parse error if (parsedLength > INSTRUCTION_MAX_LENGTH) { state = State.ERROR; throw new GuacamoleServerException("Instruction exceeds maximum length."); } // Save length elementLength = parsedLength; } // end parse length // Parse element content, if available if (state == State.PARSING_CONTENT && charsParsed + elementLength + 1 <= length) { // Read element String element = new String(chunk, offset + charsParsed, elementLength); charsParsed += elementLength; elementLength = 0; // Read terminator char following element char terminator = chunk[offset + charsParsed++]; // Add element to currently parsed elements elements[elementCount++] = element; // If semicolon, store end-of-instruction if (terminator == ';') { state = State.COMPLETE; parsedInstruction = new GuacamoleInstruction(elements[0], Arrays.asList(elements).subList(1, elementCount)); } // If comma, move on to next element else if (terminator == ',') state = State.PARSING_LENGTH; // Otherwise, parse error else { state = State.ERROR; throw new GuacamoleServerException("Element terminator of instruction was not ';' nor ','"); } } // end parse content return charsParsed; } /** * Appends data from the given buffer to the current instruction. * * @param chunk The data to append. * @return The number of characters appended, or 0 if complete instructions * have already been parsed and must be read via next() before * more data can be appended. * @throws GuacamoleException If an error occurs while parsing the new data. */ public int append(char chunk[]) throws GuacamoleException { return append(chunk, 0, chunk.length); } @Override public boolean hasNext() { return state == State.COMPLETE; } @Override public GuacamoleInstruction next() { // No instruction to return if not yet complete if (state != State.COMPLETE) return null; // Reset for next instruction. state = State.PARSING_LENGTH; elementCount = 0; elementLength = 0; return parsedInstruction; } @Override public void remove() { throw new UnsupportedOperationException("GuacamoleParser does not support remove()."); } } ././@LongLink0100644 0000000 0000000 00000000166 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleConnectionClosedException.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleConnectionClo0100664 0001751 0001751 00000004707 12612226251 035456 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole; import org.glyptodon.guacamole.protocol.GuacamoleStatus; /** * An exception which is thrown when an operation cannot be performed because * its corresponding connection is closed. * * @author Michael Jumper */ public class GuacamoleConnectionClosedException extends GuacamoleServerException { /** * Creates a new GuacamoleConnectionClosedException with the given message * and cause. * * @param message A human readable description of the exception that * occurred. * @param cause The cause of this exception. */ public GuacamoleConnectionClosedException(String message, Throwable cause) { super(message, cause); } /** * Creates a new GuacamoleConnectionClosedException with the given message. * * @param message A human readable description of the exception that * occurred. */ public GuacamoleConnectionClosedException(String message) { super(message); } /** * Creates a new GuacamoleConnectionClosedException with the given cause. * * @param cause The cause of this exception. */ public GuacamoleConnectionClosedException(Throwable cause) { super(cause); } @Override public GuacamoleStatus getStatus() { return GuacamoleStatus.SERVER_ERROR; } } ././@LongLink0100644 0000000 0000000 00000000162 12645510255 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPTunnelMap.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPT0100664 0001751 0001751 00000016446 12645510255 035321 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.servlet; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Map-style object which tracks in-use HTTP tunnels, automatically removing * and closing tunnels which have not been used recently. This class is * intended for use only within the GuacamoleHTTPTunnelServlet implementation, * and has no real utility outside that implementation. * * @author Michael Jumper */ class GuacamoleHTTPTunnelMap { /** * Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(GuacamoleHTTPTunnelMap.class); /** * The number of seconds to wait between tunnel accesses before timing out * Note that this will be enforced only within a factor of 2. If a tunnel * is unused, it will take between TUNNEL_TIMEOUT and TUNNEL_TIMEOUT*2 * seconds before that tunnel is closed and removed. */ private static final int TUNNEL_TIMEOUT = 15; /** * Executor service which runs the periodic tunnel timeout task. */ private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); /** * Map of all tunnels that are using HTTP, indexed by tunnel UUID. */ private final ConcurrentMap tunnelMap = new ConcurrentHashMap(); /** * Creates a new GuacamoleHTTPTunnelMap which automatically closes and * removes HTTP tunnels which are no longer in use. */ public GuacamoleHTTPTunnelMap() { // Check for unused tunnels every few seconds executor.scheduleAtFixedRate( new TunnelTimeoutTask(TUNNEL_TIMEOUT * 1000l), TUNNEL_TIMEOUT, TUNNEL_TIMEOUT, TimeUnit.SECONDS); } /** * Task which iterates through all registered tunnels, removing and those * tunnels which have not been accessed for a given number of milliseconds. */ private class TunnelTimeoutTask implements Runnable { /** * The maximum amount of time to allow between accesses to any one * HTTP tunnel, in milliseconds. */ private final long tunnelTimeout; /** * Creates a new task which automatically closes and removes tunnels * which have not been accessed for at least the given number of * milliseconds. * * @param tunnelTimeout * The maximum amount of time to allow between separate tunnel * read/write requests, in milliseconds. */ public TunnelTimeoutTask(long tunnelTimeout) { this.tunnelTimeout = tunnelTimeout; } @Override public void run() { // Get current time long now = System.currentTimeMillis(); // For each tunnel, close and remove any tunnels which have expired Iterator> entries = tunnelMap.entrySet().iterator(); while (entries.hasNext()) { Map.Entry entry = entries.next(); GuacamoleHTTPTunnel tunnel = entry.getValue(); // Get elapsed time since last access long age = now - tunnel.getLastAccessedTime(); // If tunnel is too old, close and remove it if (age >= tunnelTimeout) { // Remove old entry logger.debug("HTTP tunnel \"{}\" has timed out.", entry.getKey()); entries.remove(); // Attempt to close tunnel try { tunnel.close(); } catch (GuacamoleException e) { logger.debug("Unable to close expired HTTP tunnel.", e); } } } // end for each tunnel } // end timeout task run() } /** * Returns the GuacamoleTunnel having the given UUID, wrapped within a * GuacamoleHTTPTunnel. If the no tunnel having the given UUID is * available, null is returned. * * @param uuid * The UUID of the tunnel to retrieve. * * @return * The GuacamoleTunnel having the given UUID, wrapped within a * GuacamoleHTTPTunnel, if such a tunnel exists, or null if there is no * such tunnel. */ public GuacamoleHTTPTunnel get(String uuid) { // Update the last access time GuacamoleHTTPTunnel tunnel = tunnelMap.get(uuid); if (tunnel != null) tunnel.access(); // Return tunnel, if any return tunnel; } /** * Registers that a new connection has been established using HTTP via the * given GuacamoleTunnel. * * @param uuid * The UUID of the tunnel being added (registered). * * @param tunnel * The GuacamoleTunnel being registered, its associated connection * having just been established via HTTP. */ public void put(String uuid, GuacamoleTunnel tunnel) { tunnelMap.put(uuid, new GuacamoleHTTPTunnel(tunnel)); } /** * Removes the GuacamoleTunnel having the given UUID, if such a tunnel * exists. The original tunnel is returned wrapped within a * GuacamoleHTTPTunnel. * * @param uuid * The UUID of the tunnel to remove (deregister). * * @return * The GuacamoleTunnel having the given UUID, if such a tunnel exists, * wrapped within a GuacamoleHTTPTunnel, or null if no such tunnel * exists and no removal was performed. */ public GuacamoleHTTPTunnel remove(String uuid) { return tunnelMap.remove(uuid); } /** * Shuts down this tunnel map, disallowing future tunnels from being * registered and reclaiming any resources. */ public void shutdown() { executor.shutdownNow(); } } ././@LongLink0100644 0000000 0000000 00000000157 12645510255 011643 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPTunnel.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPT0100664 0001751 0001751 00000005705 12645510255 035315 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.servlet; import org.glyptodon.guacamole.net.DelegatingGuacamoleTunnel; import org.glyptodon.guacamole.net.GuacamoleTunnel; /** * Tracks the last time a particular GuacamoleTunnel was accessed. This * information is not necessary for tunnels associated with WebSocket * connections, as each WebSocket connection has its own read thread which * continuously checks the state of the tunnel and which will automatically * timeout when the underlying socket times out, but the HTTP tunnel has no * such thread. Because the HTTP tunnel requires the stream to be split across * multiple requests, tracking of activity on the tunnel must be performed * independently of the HTTP requests. * * @author Michael Jumper */ class GuacamoleHTTPTunnel extends DelegatingGuacamoleTunnel { /** * The last time this tunnel was accessed. */ private long lastAccessedTime; /** * Creates a new GuacamoleHTTPTunnel which wraps the given tunnel. * Absolutely all function calls on this new GuacamoleHTTPTunnel will be * delegated to the underlying GuacamoleTunnel. * * @param wrappedTunnel * The GuacamoleTunnel to wrap within this GuacamoleHTTPTunnel. */ public GuacamoleHTTPTunnel(GuacamoleTunnel wrappedTunnel) { super(wrappedTunnel); } /** * Updates this tunnel, marking it as recently accessed. */ public void access() { lastAccessedTime = System.currentTimeMillis(); } /** * Returns the time this tunnel was last accessed, as the number of * milliseconds since midnight January 1, 1970 GMT. Tunnel access must * be explicitly marked through calls to the access() function. * * @return * The time this tunnel was last accessed. */ public long getLastAccessedTime() { return lastAccessedTime; } } ././@LongLink0100644 0000000 0000000 00000000150 12471273272 011636 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/package-info.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/package-info.j0100664 0001751 0001751 00000002375 12471273272 035352 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes which build upon the Java Servlet API, providing an HTTP-based * tunnel and session management. */ package org.glyptodon.guacamole.servlet; ././@LongLink0100644 0000000 0000000 00000000154 12645510255 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleSession.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleSessi0100664 0001751 0001751 00000007147 12645510255 035502 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.servlet; import javax.servlet.http.HttpSession; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Provides abstract access to the tunnels associated with a Guacamole session. * * @author Michael Jumper */ @Deprecated public class GuacamoleSession { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(GuacamoleSession.class); /** * Creates a new GuacamoleSession. In prior versions of Guacamole, the * GuacamoleSession object stored the tunnels associated with a particular * user's use of the HTTP tunnel. The HTTP tunnel now stores all of these * tunnels itself, and thus this class is no longer necessary. Its use will * result in a warning being logged, and its functions will have no effect. * * @param session * The HttpSession that older versions of Guacamole would use as tunnel * storage. This parameter is now ignored, and the GuacamoleSession * class overall is deprecated. */ public GuacamoleSession(HttpSession session) { logger.warn("GuacamoleSession is deprecated. It is no longer " + "necessary and its use will have no effect."); } /** * Attaches the given tunnel to this GuacamoleSession. The GuacamoleSession * class is now deprecated, and this function has no effect. * * @param tunnel * The tunnel to attach to this GucacamoleSession. */ public void attachTunnel(GuacamoleTunnel tunnel) { // Deprecated - no effect } /** * Detaches the given tunnel to this GuacamoleSession. The GuacamoleSession * class is now deprecated, and this function has no effect. * * @param tunnel * The tunnel to detach to this GucacamoleSession. */ public void detachTunnel(GuacamoleTunnel tunnel) { // Deprecated - no effect } /** * Returns the tunnel with the given UUID attached to this GuacamoleSession, * if any. The GuacamoleSession class is now deprecated, and this function * has no effect. It will ALWAYS return null. * * @param tunnelUUID * The UUID of an attached tunnel. * * @return * The tunnel corresponding to the given UUID, if attached, or null if * if no such tunnel is attached. */ public GuacamoleTunnel getTunnel(String tunnelUUID) { // Deprecated - no effect return null; } } ././@LongLink0100644 0000000 0000000 00000000166 12645510255 011643 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPTunnelServlet.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPT0100664 0001751 0001751 00000043645 12645510255 035322 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.servlet; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleConnectionClosedException; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; import org.glyptodon.guacamole.GuacamoleServerException; import org.glyptodon.guacamole.io.GuacamoleReader; import org.glyptodon.guacamole.io.GuacamoleWriter; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.protocol.GuacamoleStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A HttpServlet implementing and abstracting the operations required by the * HTTP implementation of the JavaScript Guacamole client's tunnel. * * @author Michael Jumper */ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet { /** * Logger for this class. */ private final Logger logger = LoggerFactory.getLogger(GuacamoleHTTPTunnelServlet.class); /** * Map of absolutely all active tunnels using HTTP, indexed by tunnel UUID. */ private final GuacamoleHTTPTunnelMap tunnels = new GuacamoleHTTPTunnelMap(); /** * The prefix of the query string which denotes a tunnel read operation. */ private static final String READ_PREFIX = "read:"; /** * The prefix of the query string which denotes a tunnel write operation. */ private static final String WRITE_PREFIX = "write:"; /** * The length of the read prefix, in characters. */ private static final int READ_PREFIX_LENGTH = READ_PREFIX.length(); /** * The length of the write prefix, in characters. */ private static final int WRITE_PREFIX_LENGTH = WRITE_PREFIX.length(); /** * The length of every tunnel UUID, in characters. */ private static final int UUID_LENGTH = 36; /** * Registers the given tunnel such that future read/write requests to that * tunnel will be properly directed. * * @param tunnel * The tunnel to register. */ protected void registerTunnel(GuacamoleTunnel tunnel) { tunnels.put(tunnel.getUUID().toString(), tunnel); logger.debug("Registered tunnel \"{}\".", tunnel.getUUID()); } /** * Deregisters the given tunnel such that future read/write requests to * that tunnel will be rejected. * * @param tunnel * The tunnel to deregister. */ protected void deregisterTunnel(GuacamoleTunnel tunnel) { tunnels.remove(tunnel.getUUID().toString()); logger.debug("Deregistered tunnel \"{}\".", tunnel.getUUID()); } /** * Returns the tunnel with the given UUID, if it has been registered with * registerTunnel() and not yet deregistered with deregisterTunnel(). * * @param tunnelUUID * The UUID of registered tunnel. * * @return * The tunnel corresponding to the given UUID. * * @throws GuacamoleException * If the requested tunnel does not exist because it has not yet been * registered or it has been deregistered. */ protected GuacamoleTunnel getTunnel(String tunnelUUID) throws GuacamoleException { // Pull tunnel from map GuacamoleTunnel tunnel = tunnels.get(tunnelUUID); if (tunnel == null) throw new GuacamoleResourceNotFoundException("No such tunnel."); return tunnel; } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException { handleTunnelRequest(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException { handleTunnelRequest(request, response); } /** * Sends an error on the given HTTP response using the information within * the given GuacamoleStatus. * * @param response * The HTTP response to use to send the error. * * @param guacStatus * The status to send * * @param message * A human-readable message that can be presented to the user. * * @throws ServletException * If an error prevents sending of the error code. */ protected void sendError(HttpServletResponse response, GuacamoleStatus guacStatus, String message) throws ServletException { try { // If response not committed, send error code and message if (!response.isCommitted()) { response.addHeader("Guacamole-Status-Code", Integer.toString(guacStatus.getGuacamoleStatusCode())); response.addHeader("Guacamole-Error-Message", message); response.sendError(guacStatus.getHttpStatusCode()); } } catch (IOException ioe) { // If unable to send error at all due to I/O problems, // rethrow as servlet exception throw new ServletException(ioe); } } /** * Dispatches every HTTP GET and POST request to the appropriate handler * function based on the query string. * * @param request * The HttpServletRequest associated with the GET or POST request * received. * * @param response * The HttpServletResponse associated with the GET or POST request * received. * * @throws ServletException * If an error occurs while servicing the request. */ protected void handleTunnelRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException { try { String query = request.getQueryString(); if (query == null) throw new GuacamoleClientException("No query string provided."); // If connect operation, call doConnect() and return tunnel UUID // in response. if (query.equals("connect")) { GuacamoleTunnel tunnel = doConnect(request); if (tunnel != null) { // Register newly-created tunnel registerTunnel(tunnel); try { // Ensure buggy browsers do not cache response response.setHeader("Cache-Control", "no-cache"); // Send UUID to client response.getWriter().print(tunnel.getUUID().toString()); } catch (IOException e) { throw new GuacamoleServerException(e); } } // Failed to connect else throw new GuacamoleResourceNotFoundException("No tunnel created."); } // If read operation, call doRead() with tunnel UUID, ignoring any // characters following the tunnel UUID. else if(query.startsWith(READ_PREFIX)) doRead(request, response, query.substring( READ_PREFIX_LENGTH, READ_PREFIX_LENGTH + UUID_LENGTH)); // If write operation, call doWrite() with tunnel UUID, ignoring any // characters following the tunnel UUID. else if(query.startsWith(WRITE_PREFIX)) doWrite(request, response, query.substring( WRITE_PREFIX_LENGTH, WRITE_PREFIX_LENGTH + UUID_LENGTH)); // Otherwise, invalid operation else throw new GuacamoleClientException("Invalid tunnel operation: " + query); } // Catch any thrown guacamole exception and attempt to pass within the // HTTP response, logging each error appropriately. catch (GuacamoleClientException e) { logger.warn("HTTP tunnel request rejected: {}", e.getMessage()); sendError(response, e.getStatus(), e.getMessage()); } catch (GuacamoleException e) { logger.error("HTTP tunnel request failed: {}", e.getMessage()); logger.debug("Internal error in HTTP tunnel.", e); sendError(response, e.getStatus(), "Internal server error."); } } /** * Called whenever the JavaScript Guacamole client makes a connection * request via HTTP. It it up to the implementor of this function to define * what conditions must be met for a tunnel to be configured and returned * as a result of this connection request (whether some sort of credentials * must be specified, for example). * * @param request * The HttpServletRequest associated with the connection request * received. Any parameters specified along with the connection request * can be read from this object. * * @return * A newly constructed GuacamoleTunnel if successful, null otherwise. * * @throws GuacamoleException * If an error occurs while constructing the GuacamoleTunnel, or if the * conditions required for connection are not met. */ protected abstract GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException; /** * Called whenever the JavaScript Guacamole client makes a read request. * This function should in general not be overridden, as it already * contains a proper implementation of the read operation. * * @param request * The HttpServletRequest associated with the read request received. * * @param response * The HttpServletResponse associated with the write request received. * Any data to be sent to the client in response to the write request * should be written to the response body of this HttpServletResponse. * * @param tunnelUUID * The UUID of the tunnel to read from, as specified in the write * request. This tunnel must have been created by a previous call to * doConnect(). * * @throws GuacamoleException * If an error occurs while handling the read request. */ protected void doRead(HttpServletRequest request, HttpServletResponse response, String tunnelUUID) throws GuacamoleException { // Get tunnel, ensure tunnel exists GuacamoleTunnel tunnel = getTunnel(tunnelUUID); // Ensure tunnel is open if (!tunnel.isOpen()) throw new GuacamoleResourceNotFoundException("Tunnel is closed."); // Obtain exclusive read access GuacamoleReader reader = tunnel.acquireReader(); try { // Note that although we are sending text, Webkit browsers will // buffer 1024 bytes before starting a normal stream if we use // anything but application/octet-stream. response.setContentType("application/octet-stream"); response.setHeader("Cache-Control", "no-cache"); // Get writer for response Writer out = new BufferedWriter(new OutputStreamWriter( response.getOutputStream(), "UTF-8")); // Stream data to response, ensuring output stream is closed try { // Deregister tunnel and throw error if we reach EOF without // having ever sent any data char[] message = reader.read(); if (message == null) throw new GuacamoleConnectionClosedException("Tunnel reached end of stream."); // For all messages, until another stream is ready (we send at least one message) do { // Get message output bytes out.write(message, 0, message.length); // Flush if we expect to wait if (!reader.available()) { out.flush(); response.flushBuffer(); } // No more messages another stream can take over if (tunnel.hasQueuedReaderThreads()) break; } while (tunnel.isOpen() && (message = reader.read()) != null); // Close tunnel immediately upon EOF if (message == null) { deregisterTunnel(tunnel); tunnel.close(); } // End-of-instructions marker out.write("0.;"); out.flush(); response.flushBuffer(); } // Send end-of-stream marker and close tunnel if connection is closed catch (GuacamoleConnectionClosedException e) { // Deregister and close deregisterTunnel(tunnel); tunnel.close(); // End-of-instructions marker out.write("0.;"); out.flush(); response.flushBuffer(); } catch (GuacamoleException e) { // Deregister and close deregisterTunnel(tunnel); tunnel.close(); throw e; } // Always close output stream finally { out.close(); } } catch (IOException e) { // Log typically frequent I/O error if desired logger.debug("Error writing to servlet output stream", e); // Deregister and close deregisterTunnel(tunnel); tunnel.close(); } finally { tunnel.releaseReader(); } } /** * Called whenever the JavaScript Guacamole client makes a write request. * This function should in general not be overridden, as it already * contains a proper implementation of the write operation. * * @param request * The HttpServletRequest associated with the write request received. * Any data to be written will be specified within the body of this * request. * * @param response * The HttpServletResponse associated with the write request received. * * @param tunnelUUID * The UUID of the tunnel to write to, as specified in the write * request. This tunnel must have been created by a previous call to * doConnect(). * * @throws GuacamoleException * If an error occurs while handling the write request. */ protected void doWrite(HttpServletRequest request, HttpServletResponse response, String tunnelUUID) throws GuacamoleException { GuacamoleTunnel tunnel = getTunnel(tunnelUUID); // We still need to set the content type to avoid the default of // text/html, as such a content type would cause some browsers to // attempt to parse the result, even though the JavaScript client // does not explicitly request such parsing. response.setContentType("application/octet-stream"); response.setHeader("Cache-Control", "no-cache"); response.setContentLength(0); // Send data try { // Get writer from tunnel GuacamoleWriter writer = tunnel.acquireWriter(); // Get input reader for HTTP stream Reader input = new InputStreamReader( request.getInputStream(), "UTF-8"); // Transfer data from input stream to tunnel output, ensuring // input is always closed try { // Buffer int length; char[] buffer = new char[8192]; // Transfer data using buffer while (tunnel.isOpen() && (length = input.read(buffer, 0, buffer.length)) != -1) writer.write(buffer, 0, length); } // Close input stream in all cases finally { input.close(); } } catch (GuacamoleConnectionClosedException e) { logger.debug("Connection to guacd closed.", e); } catch (IOException e) { // Deregister and close deregisterTunnel(tunnel); tunnel.close(); throw new GuacamoleServerException("I/O Error sending data to server: " + e.getMessage(), e); } finally { tunnel.releaseWriter(); } } @Override public void destroy() { tunnels.shutdown(); } } /** * \example ExampleTunnelServlet.java * * A basic example demonstrating extending GuacamoleTunnelServlet and * implementing doConnect() to configure the Guacamole connection as * desired. */ ././@LongLink0100644 0000000 0000000 00000000163 12471273272 011642 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleClientTooManyException.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleClientTooMany0100664 0001751 0001751 00000004751 12471273272 035455 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole; import org.glyptodon.guacamole.protocol.GuacamoleStatus; /** * An exception which is thrown when too many requests have been received * by the current client, and further requests are being rejected, either * temporarily or permanently. * * @author Michael Jumper */ public class GuacamoleClientTooManyException extends GuacamoleClientException { /** * Creates a new GuacamoleClientTooManyException with the given message and cause. * * @param message A human readable description of the exception that * occurred. * @param cause The cause of this exception. */ public GuacamoleClientTooManyException(String message, Throwable cause) { super(message, cause); } /** * Creates a new GuacamoleClientTooManyException with the given message. * * @param message A human readable description of the exception that * occurred. */ public GuacamoleClientTooManyException(String message) { super(message); } /** * Creates a new GuacamoleClientTooManyException with the given cause. * * @param cause The cause of this exception. */ public GuacamoleClientTooManyException(Throwable cause) { super(cause); } @Override public GuacamoleStatus getStatus() { return GuacamoleStatus.CLIENT_TOO_MANY; } } ././@LongLink0100644 0000000 0000000 00000000166 12471273272 011645 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleResourceConflictException.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleResourceConfl0100664 0001751 0001751 00000004764 12471273272 035505 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole; import org.glyptodon.guacamole.protocol.GuacamoleStatus; /** * An exception which is thrown when a resource has been requested, but that * resource is locked or currently in use, and cannot be accessed by the * current user. * * @author Michael Jumper */ public class GuacamoleResourceConflictException extends GuacamoleClientException { /** * Creates a new GuacamoleResourceConflictException with the given message and cause. * * @param message A human readable description of the exception that * occurred. * @param cause The cause of this exception. */ public GuacamoleResourceConflictException(String message, Throwable cause) { super(message, cause); } /** * Creates a new GuacamoleResourceConflictException with the given message. * * @param message A human readable description of the exception that * occurred. */ public GuacamoleResourceConflictException(String message) { super(message); } /** * Creates a new GuacamoleResourceConflictException with the given cause. * * @param cause The cause of this exception. */ public GuacamoleResourceConflictException(Throwable cause) { super(cause); } @Override public GuacamoleStatus getStatus() { return GuacamoleStatus.RESOURCE_CONFLICT; } } ././@LongLink0100644 0000000 0000000 00000000160 12471273272 011637 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleServerBusyException.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleServerBusyExc0100664 0001751 0001751 00000004564 12471273272 035503 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole; import org.glyptodon.guacamole.protocol.GuacamoleStatus; /** * An exception which is thrown when the server is too busy to service the * request. * * @author Michael Jumper */ public class GuacamoleServerBusyException extends GuacamoleServerException { /** * Creates a new GuacamoleServerBusyException with the given message and cause. * * @param message A human readable description of the exception that * occurred. * @param cause The cause of this exception. */ public GuacamoleServerBusyException(String message, Throwable cause) { super(message, cause); } /** * Creates a new GuacamoleServerBusyException with the given message. * * @param message A human readable description of the exception that * occurred. */ public GuacamoleServerBusyException(String message) { super(message); } /** * Creates a new GuacamoleServerBusyException with the given cause. * * @param cause The cause of this exception. */ public GuacamoleServerBusyException(Throwable cause) { super(cause); } @Override public GuacamoleStatus getStatus() { return GuacamoleStatus.SERVER_BUSY; } } ././@LongLink0100644 0000000 0000000 00000000152 12471273272 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/net/SSLGuacamoleSocket.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/net/SSLGuacamoleSocket0100664 0001751 0001751 00000011347 12471273272 035327 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleServerException; import org.glyptodon.guacamole.io.GuacamoleReader; import org.glyptodon.guacamole.io.GuacamoleWriter; import org.glyptodon.guacamole.io.ReaderGuacamoleReader; import org.glyptodon.guacamole.io.WriterGuacamoleWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Provides abstract socket-like access to a Guacamole connection over SSL to * a given hostname and port. * * @author Michael Jumper */ public class SSLGuacamoleSocket implements GuacamoleSocket { /** * Logger for this class. */ private Logger logger = LoggerFactory.getLogger(SSLGuacamoleSocket.class); /** * The GuacamoleReader this socket should read from. */ private GuacamoleReader reader; /** * The GuacamoleWriter this socket should write to. */ private GuacamoleWriter writer; /** * The number of milliseconds to wait for data on the TCP socket before * timing out. */ private static final int SOCKET_TIMEOUT = 15000; /** * The TCP socket that the GuacamoleReader and GuacamoleWriter exposed * by this class should affect. */ private Socket sock; /** * Creates a new SSLGuacamoleSocket which reads and writes instructions * to the Guacamole instruction stream of the Guacamole proxy server * running at the given hostname and port using SSL. * * @param hostname The hostname of the Guacamole proxy server to connect to. * @param port The port of the Guacamole proxy server to connect to. * @throws GuacamoleException If an error occurs while connecting to the * Guacamole proxy server. */ public SSLGuacamoleSocket(String hostname, int port) throws GuacamoleException { // Get factory for SSL sockets SocketFactory socket_factory = SSLSocketFactory.getDefault(); try { logger.debug("Connecting to guacd at {}:{} via SSL/TLS.", hostname, port); // Get address SocketAddress address = new InetSocketAddress( InetAddress.getByName(hostname), port ); // Connect with timeout sock = socket_factory.createSocket(); sock.connect(address, SOCKET_TIMEOUT); // Set read timeout sock.setSoTimeout(SOCKET_TIMEOUT); // On successful connect, retrieve I/O streams reader = new ReaderGuacamoleReader(new InputStreamReader(sock.getInputStream(), "UTF-8")); writer = new WriterGuacamoleWriter(new OutputStreamWriter(sock.getOutputStream(), "UTF-8")); } catch (IOException e) { throw new GuacamoleServerException(e); } } @Override public void close() throws GuacamoleException { try { logger.debug("Closing socket to guacd."); sock.close(); } catch (IOException e) { throw new GuacamoleServerException(e); } } @Override public GuacamoleReader getReader() { return reader; } @Override public GuacamoleWriter getWriter() { return writer; } @Override public boolean isOpen() { return !sock.isClosed(); } } ././@LongLink0100644 0000000 0000000 00000000153 12471273272 011641 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/net/InetGuacamoleSocket.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/net/InetGuacamoleSocke0100664 0001751 0001751 00000011352 12471273272 035375 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net; import org.glyptodon.guacamole.io.GuacamoleReader; import org.glyptodon.guacamole.io.ReaderGuacamoleReader; import org.glyptodon.guacamole.io.WriterGuacamoleWriter; import org.glyptodon.guacamole.io.GuacamoleWriter; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.SocketTimeoutException; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleServerException; import org.glyptodon.guacamole.GuacamoleUpstreamTimeoutException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Provides abstract socket-like access to a Guacamole connection over a given * hostname and port. * * @author Michael Jumper */ public class InetGuacamoleSocket implements GuacamoleSocket { /** * Logger for this class. */ private Logger logger = LoggerFactory.getLogger(InetGuacamoleSocket.class); /** * The GuacamoleReader this socket should read from. */ private GuacamoleReader reader; /** * The GuacamoleWriter this socket should write to. */ private GuacamoleWriter writer; /** * The number of milliseconds to wait for data on the TCP socket before * timing out. */ private static final int SOCKET_TIMEOUT = 15000; /** * The TCP socket that the GuacamoleReader and GuacamoleWriter exposed * by this class should affect. */ private Socket sock; /** * Creates a new InetGuacamoleSocket which reads and writes instructions * to the Guacamole instruction stream of the Guacamole proxy server * running at the given hostname and port. * * @param hostname The hostname of the Guacamole proxy server to connect to. * @param port The port of the Guacamole proxy server to connect to. * @throws GuacamoleException If an error occurs while connecting to the * Guacamole proxy server. */ public InetGuacamoleSocket(String hostname, int port) throws GuacamoleException { try { logger.debug("Connecting to guacd at {}:{}.", hostname, port); // Get address SocketAddress address = new InetSocketAddress( InetAddress.getByName(hostname), port ); // Connect with timeout sock = new Socket(); sock.connect(address, SOCKET_TIMEOUT); // Set read timeout sock.setSoTimeout(SOCKET_TIMEOUT); // On successful connect, retrieve I/O streams reader = new ReaderGuacamoleReader(new InputStreamReader(sock.getInputStream(), "UTF-8")); writer = new WriterGuacamoleWriter(new OutputStreamWriter(sock.getOutputStream(), "UTF-8")); } catch (SocketTimeoutException e) { throw new GuacamoleUpstreamTimeoutException("Connection timed out.", e); } catch (IOException e) { throw new GuacamoleServerException(e); } } @Override public void close() throws GuacamoleException { try { logger.debug("Closing socket to guacd."); sock.close(); } catch (IOException e) { throw new GuacamoleServerException(e); } } @Override public GuacamoleReader getReader() { return reader; } @Override public GuacamoleWriter getWriter() { return writer; } @Override public boolean isOpen() { return !sock.isClosed(); } } ././@LongLink0100644 0000000 0000000 00000000161 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/net/DelegatingGuacamoleTunnel.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/net/DelegatingGuacamol0100664 0001751 0001751 00000005546 12612226251 035407 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net; import java.util.UUID; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.io.GuacamoleReader; import org.glyptodon.guacamole.io.GuacamoleWriter; /** * GuacamoleTunnel implementation which simply delegates all function calls to * an underlying GuacamoleTunnel. * * @author Michael Jumper */ public class DelegatingGuacamoleTunnel implements GuacamoleTunnel { /** * The wrapped GuacamoleTunnel. */ private final GuacamoleTunnel tunnel; /** * Wraps the given tunnel such that all function calls against this tunnel * will be delegated to it. * * @param tunnel * The GuacamoleTunnel to wrap. */ public DelegatingGuacamoleTunnel(GuacamoleTunnel tunnel) { this.tunnel = tunnel; } @Override public GuacamoleReader acquireReader() { return tunnel.acquireReader(); } @Override public void releaseReader() { tunnel.releaseReader(); } @Override public boolean hasQueuedReaderThreads() { return tunnel.hasQueuedReaderThreads(); } @Override public GuacamoleWriter acquireWriter() { return tunnel.acquireWriter(); } @Override public void releaseWriter() { tunnel.releaseWriter(); } @Override public boolean hasQueuedWriterThreads() { return tunnel.hasQueuedWriterThreads(); } @Override public UUID getUUID() { return tunnel.getUUID(); } @Override public GuacamoleSocket getSocket() { return tunnel.getSocket(); } @Override public void close() throws GuacamoleException { tunnel.close(); } @Override public boolean isOpen() { return tunnel.isOpen(); } } guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/net/package-info.java0100664 0001751 0001751 00000002354 12471273272 035141 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * Classes which apply to network-specific concepts, such as low-level sockets * and tunnels. */ package org.glyptodon.guacamole.net; ././@LongLink0100644 0000000 0000000 00000000157 12612226251 011635 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/net/AbstractGuacamoleTunnel.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/net/AbstractGuacamoleT0100664 0001751 0001751 00000010254 12612226251 035370 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net; import java.util.concurrent.locks.ReentrantLock; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.io.GuacamoleReader; import org.glyptodon.guacamole.io.GuacamoleWriter; /** * Base GuacamoleTunnel implementation which synchronizes access to the * underlying reader and writer with reentrant locks. Implementations need only * provide the tunnel's UUID and socket. * * @author Michael Jumper */ public abstract class AbstractGuacamoleTunnel implements GuacamoleTunnel { /** * Lock acquired when a read operation is in progress. */ private final ReentrantLock readerLock; /** * Lock acquired when a write operation is in progress. */ private final ReentrantLock writerLock; /** * Creates a new GuacamoleTunnel which synchronizes access to the * Guacamole instruction stream associated with the underlying * GuacamoleSocket. */ public AbstractGuacamoleTunnel() { readerLock = new ReentrantLock(); writerLock = new ReentrantLock(); } /** * Acquires exclusive read access to the Guacamole instruction stream * and returns a GuacamoleReader for reading from that stream. * * @return A GuacamoleReader for reading from the Guacamole instruction * stream. */ @Override public GuacamoleReader acquireReader() { readerLock.lock(); return getSocket().getReader(); } /** * Relinquishes exclusive read access to the Guacamole instruction * stream. This function should be called whenever a thread finishes using * a GuacamoleTunnel's GuacamoleReader. */ @Override public void releaseReader() { readerLock.unlock(); } /** * Returns whether there are threads waiting for read access to the * Guacamole instruction stream. * * @return true if threads are waiting for read access the Guacamole * instruction stream, false otherwise. */ @Override public boolean hasQueuedReaderThreads() { return readerLock.hasQueuedThreads(); } /** * Acquires exclusive write access to the Guacamole instruction stream * and returns a GuacamoleWriter for writing to that stream. * * @return A GuacamoleWriter for writing to the Guacamole instruction * stream. */ @Override public GuacamoleWriter acquireWriter() { writerLock.lock(); return getSocket().getWriter(); } /** * Relinquishes exclusive write access to the Guacamole instruction * stream. This function should be called whenever a thread finishes using * a GuacamoleTunnel's GuacamoleWriter. */ @Override public void releaseWriter() { writerLock.unlock(); } @Override public boolean hasQueuedWriterThreads() { return writerLock.hasQueuedThreads(); } @Override public void close() throws GuacamoleException { getSocket().close(); } @Override public boolean isOpen() { return getSocket().isOpen(); } } ././@LongLink0100644 0000000 0000000 00000000147 12612226251 011634 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/net/GuacamoleTunnel.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/net/GuacamoleTunnel.ja0100664 0001751 0001751 00000007744 12612226251 035351 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net; import java.util.UUID; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.io.GuacamoleReader; import org.glyptodon.guacamole.io.GuacamoleWriter; /** * Provides a unique identifier and synchronized access to the GuacamoleReader * and GuacamoleWriter associated with a GuacamoleSocket. * * @author Michael Jumper */ public interface GuacamoleTunnel { /** * Acquires exclusive read access to the Guacamole instruction stream * and returns a GuacamoleReader for reading from that stream. * * @return A GuacamoleReader for reading from the Guacamole instruction * stream. */ GuacamoleReader acquireReader(); /** * Relinquishes exclusive read access to the Guacamole instruction * stream. This function should be called whenever a thread finishes using * a GuacamoleTunnel's GuacamoleReader. */ void releaseReader(); /** * Returns whether there are threads waiting for read access to the * Guacamole instruction stream. * * @return true if threads are waiting for read access the Guacamole * instruction stream, false otherwise. */ boolean hasQueuedReaderThreads(); /** * Acquires exclusive write access to the Guacamole instruction stream * and returns a GuacamoleWriter for writing to that stream. * * @return A GuacamoleWriter for writing to the Guacamole instruction * stream. */ GuacamoleWriter acquireWriter(); /** * Relinquishes exclusive write access to the Guacamole instruction * stream. This function should be called whenever a thread finishes using * a GuacamoleTunnel's GuacamoleWriter. */ void releaseWriter(); /** * Returns whether there are threads waiting for write access to the * Guacamole instruction stream. * * @return true if threads are waiting for write access the Guacamole * instruction stream, false otherwise. */ boolean hasQueuedWriterThreads(); /** * Returns the unique identifier associated with this GuacamoleTunnel. * * @return The unique identifier associated with this GuacamoleTunnel. */ UUID getUUID(); /** * Returns the GuacamoleSocket used by this GuacamoleTunnel for reading * and writing. * * @return The GuacamoleSocket used by this GuacamoleTunnel. */ GuacamoleSocket getSocket(); /** * Release all resources allocated to this GuacamoleTunnel. * * @throws GuacamoleException if an error occurs while releasing * resources. */ void close() throws GuacamoleException; /** * Returns whether this GuacamoleTunnel is open, or has been closed. * * @return true if this GuacamoleTunnel is open, false if it is closed. */ boolean isOpen(); } ././@LongLink0100644 0000000 0000000 00000000155 12612226251 011633 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/net/SimpleGuacamoleTunnel.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/net/SimpleGuacamoleTun0100664 0001751 0001751 00000004421 12612226251 035420 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net; import java.util.UUID; /** * GuacamoleTunnel implementation which uses a provided socket. The UUID of * the tunnel will be randomly generated. * * @author Michael Jumper */ public class SimpleGuacamoleTunnel extends AbstractGuacamoleTunnel { /** * The UUID associated with this tunnel. Every tunnel must have a * corresponding UUID such that tunnel read/write requests can be * directed to the proper tunnel. */ private final UUID uuid = UUID.randomUUID(); /** * The GuacamoleSocket that tunnel should use for communication on * behalf of the connecting user. */ private final GuacamoleSocket socket; /** * Creates a new GuacamoleTunnel which synchronizes access to the * Guacamole instruction stream associated with the given GuacamoleSocket. * * @param socket The GuacamoleSocket to provide synchronized access for. */ public SimpleGuacamoleTunnel(GuacamoleSocket socket) { this.socket = socket; } @Override public UUID getUUID() { return uuid; } @Override public GuacamoleSocket getSocket() { return socket; } } ././@LongLink0100644 0000000 0000000 00000000147 12471273272 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/net/GuacamoleSocket.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/net/GuacamoleSocket.ja0100664 0001751 0001751 00000005121 12471273272 035327 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.net; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.io.GuacamoleReader; import org.glyptodon.guacamole.io.GuacamoleWriter; /** * Provides abstract socket-like access to a Guacamole connection. * * @author Michael Jumper */ public interface GuacamoleSocket { /** * Returns a GuacamoleReader which can be used to read from the * Guacamole instruction stream associated with the connection * represented by this GuacamoleSocket. * * @return A GuacamoleReader which can be used to read from the * Guacamole instruction stream. */ public GuacamoleReader getReader(); /** * Returns a GuacamoleWriter which can be used to write to the * Guacamole instruction stream associated with the connection * represented by this GuacamoleSocket. * * @return A GuacamoleWriter which can be used to write to the * Guacamole instruction stream. */ public GuacamoleWriter getWriter(); /** * Releases all resources in use by the connection represented by this * GuacamoleSocket. * * @throws GuacamoleException If an error occurs while releasing resources. */ public void close() throws GuacamoleException; /** * Returns whether this GuacamoleSocket is open and can be used for reading * and writing. * * @return true if this GuacamoleSocket is open, false otherwise. */ public boolean isOpen(); } ././@LongLink0100644 0000000 0000000 00000000156 12471273272 011644 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleSecurityException.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleSecurityExcep0100664 0001751 0001751 00000004576 12471273272 035531 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole; import org.glyptodon.guacamole.protocol.GuacamoleStatus; /** * A security-related exception thrown when parts of the Guacamole API is * denying access to a resource. * * @author Michael Jumper */ public class GuacamoleSecurityException extends GuacamoleClientException { /** * Creates a new GuacamoleSecurityException with the given message and cause. * * @param message A human readable description of the exception that * occurred. * @param cause The cause of this exception. */ public GuacamoleSecurityException(String message, Throwable cause) { super(message, cause); } /** * Creates a new GuacamoleSecurityException with the given message. * * @param message A human readable description of the exception that * occurred. */ public GuacamoleSecurityException(String message) { super(message); } /** * Creates a new GuacamoleSecurityException with the given cause. * * @param cause The cause of this exception. */ public GuacamoleSecurityException(Throwable cause) { super(cause); } @Override public GuacamoleStatus getStatus() { return GuacamoleStatus.CLIENT_FORBIDDEN; } } ././@LongLink0100644 0000000 0000000 00000000166 12471273272 011645 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleResourceNotFoundException.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/GuacamoleResourceNotFo0100664 0001751 0001751 00000004733 12471273272 035465 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole; import org.glyptodon.guacamole.protocol.GuacamoleStatus; /** * A generic exception thrown when part of the Guacamole API fails to find * a requested resource, such as a configuration or tunnel. * * @author Michael Jumper */ public class GuacamoleResourceNotFoundException extends GuacamoleClientException { /** * Creates a new GuacamoleResourceNotFoundException with the given message * and cause. * * @param message A human readable description of the exception that * occurred. * @param cause The cause of this exception. */ public GuacamoleResourceNotFoundException(String message, Throwable cause) { super(message, cause); } /** * Creates a new GuacamoleResourceNotFoundException with the given message. * * @param message A human readable description of the exception that * occurred. */ public GuacamoleResourceNotFoundException(String message) { super(message); } /** * Creates a new GuacamoleResourceNotFoundException with the given cause. * * @param cause The cause of this exception. */ public GuacamoleResourceNotFoundException(Throwable cause) { super(cause); } @Override public GuacamoleStatus getStatus() { return GuacamoleStatus.RESOURCE_NOT_FOUND; } } guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/package-info.java0100664 0001751 0001751 00000002455 12471273272 034355 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * All classes which apply generally across the Guacamole web application * and all other web applications which use the API provided by the * Guacamole project. */ package org.glyptodon.guacamole; guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/io/package-info.java0100664 0001751 0001751 00000002306 12471273272 034757 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * All classes relating directly to data input or output. */ package org.glyptodon.guacamole.io; ././@LongLink0100644 0000000 0000000 00000000154 12612226251 011632 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/io/ReaderGuacamoleReader.javaguacamole-client-0.9.9/guacamole-common/src/main/java/org/glyptodon/guacamole/io/ReaderGuacamoleRead0100664 0001751 0001751 00000021435 12612226251 035323 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.io; import java.io.IOException; import java.io.Reader; import java.net.SocketException; import java.net.SocketTimeoutException; import java.util.Deque; import java.util.LinkedList; import org.glyptodon.guacamole.GuacamoleConnectionClosedException; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleServerException; import org.glyptodon.guacamole.GuacamoleUpstreamTimeoutException; import org.glyptodon.guacamole.protocol.GuacamoleInstruction; /** * A GuacamoleReader which wraps a standard Java Reader, using that Reader as * the Guacamole instruction stream. * * @author Michael Jumper */ public class ReaderGuacamoleReader implements GuacamoleReader { /** * Wrapped Reader to be used for all input. */ private Reader input; /** * Creates a new ReaderGuacamoleReader which will use the given Reader as * the Guacamole instruction stream. * * @param input The Reader to use as the Guacamole instruction stream. */ public ReaderGuacamoleReader(Reader input) { this.input = input; } /** * The location within the received data buffer that parsing should begin * when more data is read. */ private int parseStart; /** * The buffer holding all received, unparsed data. */ private char[] buffer = new char[20480]; /** * The number of characters currently used within the data buffer. All * other characters within the buffer are free space available for * future reads. */ private int usedLength = 0; @Override public boolean available() throws GuacamoleException { try { return input.ready() || usedLength != 0; } catch (IOException e) { throw new GuacamoleServerException(e); } } @Override public char[] read() throws GuacamoleException { try { // While we're blocking, or input is available for (;;) { // Length of element int elementLength = 0; // Resume where we left off int i = parseStart; // Parse instruction in buffer while (i < usedLength) { // Read character char readChar = buffer[i++]; // If digit, update length if (readChar >= '0' && readChar <= '9') elementLength = elementLength * 10 + readChar - '0'; // If not digit, check for end-of-length character else if (readChar == '.') { // Check if element present in buffer if (i + elementLength < usedLength) { // Get terminator char terminator = buffer[i + elementLength]; // Move to character after terminator i += elementLength + 1; // Reset length elementLength = 0; // Continue here if necessary parseStart = i; // If terminator is semicolon, we have a full // instruction. if (terminator == ';') { // Copy instruction data char[] instruction = new char[i]; System.arraycopy(buffer, 0, instruction, 0, i); // Update buffer usedLength -= i; parseStart = 0; System.arraycopy(buffer, i, buffer, 0, usedLength); return instruction; } // Handle invalid terminator characters else if (terminator != ',') throw new GuacamoleServerException("Element terminator of instruction was not ';' nor ','"); } // Otherwise, read more data else break; } // Otherwise, parse error else throw new GuacamoleServerException("Non-numeric character in element length."); } // If past threshold, resize buffer before reading if (usedLength > buffer.length/2) { char[] biggerBuffer = new char[buffer.length*2]; System.arraycopy(buffer, 0, biggerBuffer, 0, usedLength); buffer = biggerBuffer; } // Attempt to fill buffer int numRead = input.read(buffer, usedLength, buffer.length - usedLength); if (numRead == -1) return null; // Update used length usedLength += numRead; } // End read loop } catch (SocketTimeoutException e) { throw new GuacamoleUpstreamTimeoutException("Connection to guacd timed out.", e); } catch (SocketException e) { throw new GuacamoleConnectionClosedException("Connection to guacd is closed.", e); } catch (IOException e) { throw new GuacamoleServerException(e); } } @Override public GuacamoleInstruction readInstruction() throws GuacamoleException { // Get instruction char[] instructionBuffer = read(); // If EOF, return EOF if (instructionBuffer == null) return null; // Start of element int elementStart = 0; // Build list of elements Deque elements = new LinkedList(); while (elementStart < instructionBuffer.length) { // Find end of length int lengthEnd = -1; for (int i=elementStart; i() { @Override public void onMessage(String message) { GuacamoleWebSocketTunnelEndpoint.this.onMessage(message); } }); // Prepare read transfer thread Thread readThread = new Thread() { /** * Remote (client) side of this connection */ private final RemoteEndpoint.Basic remote = session.getBasicRemote(); @Override public void run() { StringBuilder buffer = new StringBuilder(BUFFER_SIZE); GuacamoleReader reader = tunnel.acquireReader(); char[] readMessage; try { try { // Attempt to read while ((readMessage = reader.read()) != null) { // Buffer message buffer.append(readMessage); // Flush if we expect to wait or buffer is getting full if (!reader.available() || buffer.length() >= BUFFER_SIZE) { remote.sendText(buffer.toString()); buffer.setLength(0); } } // No more data closeConnection(session, GuacamoleStatus.SUCCESS); } // Catch any thrown guacamole exception and attempt // to pass within the WebSocket connection, logging // each error appropriately. catch (GuacamoleClientException e) { logger.info("WebSocket connection terminated: {}", e.getMessage()); logger.debug("WebSocket connection terminated due to client error.", e); closeConnection(session, e.getStatus()); } catch (GuacamoleConnectionClosedException e) { logger.debug("Connection to guacd closed.", e); closeConnection(session, GuacamoleStatus.SUCCESS); } catch (GuacamoleException e) { logger.error("Connection to guacd terminated abnormally: {}", e.getMessage()); logger.debug("Internal error during connection to guacd.", e); closeConnection(session, e.getStatus()); } } catch (IOException e) { logger.debug("I/O error prevents further reads.", e); } } }; readThread.start(); } @OnMessage public void onMessage(String message) { // Ignore inbound messages if there is no associated tunnel if (tunnel == null) return; GuacamoleWriter writer = tunnel.acquireWriter(); try { // Write received message writer.write(message.toCharArray()); } catch (GuacamoleConnectionClosedException e) { logger.debug("Connection to guacd closed.", e); } catch (GuacamoleException e) { logger.debug("WebSocket tunnel write failed.", e); } tunnel.releaseWriter(); } @Override @OnClose public void onClose(Session session, CloseReason closeReason) { try { if (tunnel != null) tunnel.close(); } catch (GuacamoleException e) { logger.debug("Unable to close WebSocket tunnel.", e); } } } ././@LongLink0100644 0000000 0000000 00000000160 12471273273 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/test/java/org/glyptodon/guacamole/protocol/GuacamoleParserTest.javaguacamole-client-0.9.9/guacamole-common/src/test/java/org/glyptodon/guacamole/protocol/GuacamolePars0100664 0001751 0001751 00000010504 12471273273 035523 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.protocol; import org.glyptodon.guacamole.GuacamoleException; import static org.junit.Assert.*; import org.junit.Test; /** * Unit test for GuacamoleParser. Verifies that parsing of the Guacamole * protocol works as required. * * @author Michael Jumper */ public class GuacamoleParserTest { /** * The GuacamoleParser instance being tested. */ private final GuacamoleParser parser = new GuacamoleParser(); /** * Test of append method, of class GuacamoleParser. * * @throws GuacamoleException If a parse error occurs while parsing the * known-good test string. */ @Test public void testParser() throws GuacamoleException { // Test string char buffer[] = "1.a,2.bc,3.def,10.helloworld;4.test,5.test2;0.;3.foo;".toCharArray(); int offset = 0; int length = buffer.length; GuacamoleInstruction instruction; int parsed; // Parse more data while (length > 0 && (parsed = parser.append(buffer, offset, length)) != 0) { offset += parsed; length -= parsed; } // Validate first test instruction assertTrue(parser.hasNext()); instruction = parser.next(); assertNotNull(instruction); assertEquals(3, instruction.getArgs().size()); assertEquals("a", instruction.getOpcode()); assertEquals("bc", instruction.getArgs().get(0)); assertEquals("def", instruction.getArgs().get(1)); assertEquals("helloworld", instruction.getArgs().get(2)); // Parse more data while (length > 0 && (parsed = parser.append(buffer, offset, length)) != 0) { offset += parsed; length -= parsed; } // Validate second test instruction assertTrue(parser.hasNext()); instruction = parser.next(); assertNotNull(instruction); assertEquals(1, instruction.getArgs().size()); assertEquals("test", instruction.getOpcode()); assertEquals("test2", instruction.getArgs().get(0)); // Parse more data while (length > 0 && (parsed = parser.append(buffer, offset, length)) != 0) { offset += parsed; length -= parsed; } // Validate third test instruction assertTrue(parser.hasNext()); instruction = parser.next(); assertNotNull(instruction); assertEquals(0, instruction.getArgs().size()); assertEquals("", instruction.getOpcode()); // Parse more data while (length > 0 && (parsed = parser.append(buffer, offset, length)) != 0) { offset += parsed; length -= parsed; } // Validate fourth test instruction assertTrue(parser.hasNext()); instruction = parser.next(); assertNotNull(instruction); assertEquals(0, instruction.getArgs().size()); assertEquals("foo", instruction.getOpcode()); // Parse more data while (length > 0 && (parsed = parser.append(buffer, offset, length)) != 0) { offset += parsed; length -= parsed; } // There should be no more instructions assertFalse(parser.hasNext()); } } ././@LongLink0100644 0000000 0000000 00000000170 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/test/java/org/glyptodon/guacamole/protocol/FilteredGuacamoleReaderTest.javaguacamole-client-0.9.9/guacamole-common/src/test/java/org/glyptodon/guacamole/protocol/FilteredGuaca0100664 0001751 0001751 00000006522 12612226251 035473 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.protocol; import java.io.StringReader; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.io.GuacamoleReader; import org.glyptodon.guacamole.io.ReaderGuacamoleReader; import static org.junit.Assert.*; import org.junit.Test; /** * Test which validates filtering of Guacamole instructions with * FilteredGuacamoleReader. * * @author Michael Jumper */ public class FilteredGuacamoleReaderTest { /** * Filter which allows through "yes" instructions but drops all others. */ private static class TestFilter implements GuacamoleFilter { @Override public GuacamoleInstruction filter(GuacamoleInstruction instruction) throws GuacamoleException { if (instruction.getOpcode().equals("yes")) return instruction; return null; } } @Test public void testFilter() throws Exception { // Test string final String test = "3.yes,1.A;2.no,1.B;3.yes,1.C;3.yes,1.D;4.nope,1.E;"; GuacamoleReader reader = new FilteredGuacamoleReader(new ReaderGuacamoleReader(new StringReader(test)), new TestFilter()); GuacamoleInstruction instruction; // Validate first instruction instruction = reader.readInstruction(); assertNotNull(instruction); assertEquals("yes", instruction.getOpcode()); assertEquals(1, instruction.getArgs().size()); assertEquals("A", instruction.getArgs().get(0)); // Validate second instruction instruction = reader.readInstruction(); assertNotNull(instruction); assertEquals("yes", instruction.getOpcode()); assertEquals(1, instruction.getArgs().size()); assertEquals("C", instruction.getArgs().get(0)); // Validate third instruction instruction = reader.readInstruction(); assertNotNull(instruction); assertEquals("yes", instruction.getOpcode()); assertEquals(1, instruction.getArgs().size()); assertEquals("D", instruction.getArgs().get(0)); // Should be done now instruction = reader.readInstruction(); assertNull(instruction); } } ././@LongLink0100644 0000000 0000000 00000000170 12612226251 011630 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/test/java/org/glyptodon/guacamole/protocol/FilteredGuacamoleWriterTest.javaguacamole-client-0.9.9/guacamole-common/src/test/java/org/glyptodon/guacamole/protocol/FilteredGuaca0100664 0001751 0001751 00000005226 12612226251 035473 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.protocol; import java.io.StringWriter; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.io.GuacamoleWriter; import org.glyptodon.guacamole.io.WriterGuacamoleWriter; import static org.junit.Assert.*; import org.junit.Test; /** * Test which validates filtering of Guacamole instructions with * FilteredGuacamoleWriter. * * @author Michael Jumper */ public class FilteredGuacamoleWriterTest { /** * Filter which allows through "yes" instructions but drops all others. */ private static class TestFilter implements GuacamoleFilter { @Override public GuacamoleInstruction filter(GuacamoleInstruction instruction) throws GuacamoleException { if (instruction.getOpcode().equals("yes")) return instruction; return null; } } @Test public void testFilter() throws Exception { StringWriter stringWriter = new StringWriter(); GuacamoleWriter writer = new FilteredGuacamoleWriter(new WriterGuacamoleWriter(stringWriter), new TestFilter()); // Write a few chunks of complete instructions writer.write("3.yes,1.A;2.no,1.B;3.yes,1.C;3.yes,1.D;4.nope,1.E;".toCharArray()); writer.write("1.n,3.abc;3.yes,5.hello;2.no,4.test;3.yes,5.world;".toCharArray()); // Validate filtered results assertEquals("3.yes,1.A;3.yes,1.C;3.yes,1.D;3.yes,5.hello;3.yes,5.world;", stringWriter.toString()); } } ././@LongLink0100644 0000000 0000000 00000000160 12471273273 011640 Lustar 0000000 0000000 guacamole-client-0.9.9/guacamole-common/src/test/java/org/glyptodon/guacamole/io/ReaderGuacamoleReaderTest.javaguacamole-client-0.9.9/guacamole-common/src/test/java/org/glyptodon/guacamole/io/ReaderGuacamoleRead0100664 0001751 0001751 00000006512 12471273273 035366 0ustar00mjumpermjumper0000000 0000000 /* * Copyright (C) 2014 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.glyptodon.guacamole.io; import java.io.StringReader; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.protocol.GuacamoleInstruction; import org.junit.Test; import static org.junit.Assert.*; /** * Tests the ReaderGuacamoleReader implementation of GuacamoleReader, validating * that instructions are parsed correctly. * * @author Michael Jumper */ public class ReaderGuacamoleReaderTest { /** * Test of ReaderGuacamoleReader parsing. * * @throws GuacamoleException If a parse error occurs while parsing the * known-good test string. */ @Test public void testReader() throws GuacamoleException { // Test string final String test = "1.a,2.bc,3.def,10.helloworld;4.test,5.test2;0.;3.foo;"; GuacamoleReader reader = new ReaderGuacamoleReader(new StringReader(test)); GuacamoleInstruction instruction; // Validate first test instruction instruction = reader.readInstruction(); assertNotNull(instruction); assertEquals(3, instruction.getArgs().size()); assertEquals("a", instruction.getOpcode()); assertEquals("bc", instruction.getArgs().get(0)); assertEquals("def", instruction.getArgs().get(1)); assertEquals("helloworld", instruction.getArgs().get(2)); // Validate second test instruction instruction = reader.readInstruction(); assertNotNull(instruction); assertEquals(1, instruction.getArgs().size()); assertEquals("test", instruction.getOpcode()); assertEquals("test2", instruction.getArgs().get(0)); // Validate third test instruction instruction = reader.readInstruction(); assertNotNull(instruction); assertEquals(0, instruction.getArgs().size()); assertEquals("", instruction.getOpcode()); // Validate fourth test instruction instruction = reader.readInstruction(); assertNotNull(instruction); assertEquals(0, instruction.getArgs().size()); assertEquals("foo", instruction.getOpcode()); // There should be no more instructions instruction = reader.readInstruction(); assertNull(instruction); } } guacamole-client-0.9.9/guacamole-common/pom.xml0100664 0001751 0001751 00000010466 12645510255 023303 0ustar00mjumpermjumper0000000 0000000 4.0.0 org.glyptodon.guacamole guacamole-common jar 0.9.9 guacamole-common http://guac-dev.org/ The base Java API of the Guacamole project, providing Java support for the Guacamole stack. The MIT License http://www.opensource.org/licenses/mit-license.php repo https://github.com/glyptodon/guacamole-client scm:git:git://github.com/glyptodon/guacamole-client.git mike.jumper Michael Jumper mike.jumper@guac-dev.org james.muehlner James Muehlner james.muehlner@guac-dev.org UTF-8 org.apache.maven.plugins maven-compiler-plugin 3.3 1.6 1.6 -Xlint:all -Werror true org.apache.maven.plugins maven-source-plugin 2.4 attach-sources jar org.apache.maven.plugins maven-javadoc-plugin 2.10.3 false attach-javadocs jar javax.servlet servlet-api 2.5 provided javax.websocket javax.websocket-api 1.0 provided org.slf4j slf4j-api 1.7.7 junit junit 4.10 test guacamole-client-0.9.9/guacamole-common/ChangeLog0100664 0001751 0001751 00000002250 12424302752 023524 0ustar00mjumpermjumper0000000 0000000 2012-10-24 Michael Jumper * Implement audio/video/size preamble in handshake 2012-10-16 Michael Jumper * Ignore data after tunnel UUID (part of fix for ticket #201) 2012-10-03 Michael Jumper * Fix NPE in handshake if end-of-stream encountered (ticket #195) 2012-08-09 Michael Jumper * Improve documentation 2012-07-24 Michael Jumper * Fix Unicode bug 2012-05-04 Michael Jumper * Improved logging and exception handling * Removed minor dependency on Apache Commons * Improved API usability 2011-12-11 Michael Jumper * Implemented improved instruction format * Fixed failed connections when using Konqueror * Relicensed as Mozilla/LGPL/GPL 2011-07-13 Michael Jumper * Fixed "no element found" errors * Added timeouts * Multiple tunnel support * Better error handling * Added JavaDoc * Added logging 2011-03-02 Michael Jumper * Initial release guacamole-client-0.9.9/guacamole-common/LICENSE0100664 0001751 0001751 00000002041 12471273272 022763 0ustar00mjumpermjumper0000000 0000000 Copyright (C) 2013 Glyptodon LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.