pax_global_header00006660000000000000000000000064130072030550014505gustar00rootroot0000000000000052 comment=36d6582f98b64c309609ef88119ab831421910d8 ricochet-1.1.4/000077500000000000000000000000001300720305500133105ustar00rootroot00000000000000ricochet-1.1.4/AUTHORS.md000066400000000000000000000015101300720305500147540ustar00rootroot00000000000000### Development * John Brooks (@special) `` ### Sponsors * Blueprint for Free Speech - https://blueprintforfreespeech.net/ ### Translations * Bulgarian - ivopetkovcz * Czech - Einfach * Danish - Mikkel Kroman * Dutch - mijnheer, Meternalf * Finnish - reviewjolla * French - rike, Creaprog, CrumpyGat, Jordi, franck99 * German - djsmith85, rike * Italian - HostFat, GIANNAT * Polish - Kacper Kołodziej * Portuguese (Brazil) - swperman * Russian - vla8752, qualte * Spanish - strel * Swedish - rawtaz * Tagalog - taskmaster * Turkish - cbolat, basarancaner * Ukrainian - l3rixon, nergal ### Thanks * Helder Ribeiro (@obvio171) - "Ricochet" * Robin Burchell (@rburchell) * prof7bit (TorChat) - Inspiration * Patrick Gray - invisible.im * Suelette Dreyfus * HD Moore * The Grugq * Lawrence Eastland - "R" icon ricochet-1.1.4/BUILDING.md000066400000000000000000000121451300720305500150320ustar00rootroot00000000000000## Building Ricochet These instructions are intended for people who wish to build or modify Ricochet from source. Most users should [download releases](https://github.com/ricochet-im/ricochet/releases) instead. Clone with git from `https://github.com/ricochet-im/ricochet.git`, or download source packages [on github](https://github.com/ricochet-im/ricochet/releases). Then proceed to instructions for your platform. If you're interested in helping to package Ricochet for common Linux platforms, please get in touch! ## Hints Add `CONFIG+=debug` or `CONFIG+=release` to the qmake command for a debug or release build. Debug builds enable logging to standard output, and shouldn't be used in sensitive environments. By default, Ricochet will be portable, and configuration is stored in a folder named `config` next to the binary. Add `DEFINES+=RICOCHET_NO_PORTABLE` to the qmake command for a system-wide installation using platform configuration paths instead. ## Linux You will need: * Qt >= 5.1.0 * OpenSSL (libcrypto) * pkg-config * Protocol Buffers (libprotobuf, protoc) #### Fedora ```sh yum install make gcc-c++ protobuf-devel protobuf-compiler openssl-devel yum install qt5-qtbase qt5-qttools-devel qt5-qttools qt5-qtquickcontrols qt5-qtdeclarative qt5-qtbase-devel qt5-qtbase-gui qt5-qtdeclarative-devel qt5-qtmultimedia-devel yum install tor # or build your own ``` #### Debian & Ubuntu ```sh apt-get install build-essential libssl-dev pkg-config libprotobuf-dev protobuf-compiler apt-get install qt5-qmake qt5-default qtbase5-dev qttools5-dev-tools qtdeclarative5-dev qtmultimedia5-dev apt-get install qml-module-qtquick-controls qml-module-qtquick-dialogs qml-module-qtmultimedia apt-get install tor # or build your own ``` If the `qml-module-qtquick` packages aren't available, try `qtdeclarative5-controls-plugin` instead. #### Qt SDK The [Qt SDK](https://www.qt.io/download/) is available for most Linux systems and includes an IDE as well as all Qt dependencies. To build, simply run: ```sh qmake # qmake-qt5 for some platforms make ``` For a system-wide installation, use: ```sh qmake DEFINES+=RICOCHET_NO_PORTABLE make make install # as root ``` You must have a `tor` binary installed on the system (in $PATH), or placed next to the `ricochet` binary. In portable mode (default), all configuration is stored in a folder called `config` with the binary. When installed, the platform's user configuration path is used instead. The [buildscripts](https://github.com/ricochet-im/buildscripts) repository contains a set of scripts to build a fully static Ricochet on a clean Debian system. These are used to create the generic linux binary packages. #### Hardening Ricochet will use aggressive compiler hardening flags if available. `qmake` will print the results of these tests on first run, or when run with `CONFIG+=recheck`. To take full advantage of the sanitizer options, you may need to install `libasan` and `libubsan`. ## OS X You will need: * Xcode (for toolchain) * Qt 5 - preferably the [Qt SDK](https://www.qt.io/download/) * Protocol Buffers (libprotobuf, protoc) - `brew install protobuf` You can either load `ricochet.pro` in Qt Creator and build normally, or build command-line with: ```sh /path/to/qtsdk/5.3/clang_64/bin/qmake make ``` You also need a `tor` binary in $PATH or inside the build's `ricochet.app/Contents/MacOS` folder. The easiest solution is to use `brew install tor`. If you copy the `tor` binary, you will need to keep it up to date. Normally, configuration will be stored in a `config.ricochet` folder, in the same location as `ricochet.app`. However, if the bundle is installed to `/Applications`, the system location `~/Library/Application Support/Ricochet` is used instead. You can force that behavior by adding `DEFINES+=RICOCHET_NO_PORTABLE` to the qmake command. The `packaging/osx/release_osx.sh` script demonstrates how to build a redistributable app bundle. Since the openssl header files were removed in El Capitan, have qmake use the openssl that comes with brew (see the OPENSSLDIR var below). Steps: ``` brew install protobuf qt5 tor /usr/local/opt/qt5/bin/qmake OPENSSLDIR=/usr/local/opt/openssl/ CONFIG+=debug make ``` ## Windows Building for Windows is difficult. The process and scripts used for release builds are documented in the [buildscripts repository](https://github.com/ricochet-im/buildscripts/tree/master/mingw). For development builds, you will want: * Visual Studio C++ or MinGW * Qt 5 - preferably the [Qt SDK](https://www.qt.io/download/) * OpenSSL (including libs and headers) * Protocol Buffers >= 2.6.0 Compile OpenSSL and protobuf first, according to their instructions. After installing the Qt SDK, open the `ricochet.pro` project in Qt Creator. Before building, you must click the 'Projects' tab on the left side, and under 'Build Steps', modify 'Additional arguments' to add: ``` OPENSSLDIR=C:\path\to\openssl\ PROTOBUFDIR=C:\path\to\protobuf ``` Use the 'Build -> Run qmake' menu to test your changes. You also need a `tor.exe` binary, placed in the same folder as `ricochet.exe`. The windows installer can be built using Inno Setup. See `packaging\installer` for more information. ricochet-1.1.4/LICENSE000066400000000000000000000267651300720305500143350ustar00rootroot00000000000000 Ricochet - https://ricochet.im/ Copyright (C) 2014-2016, John Brooks Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the names of the copyright owners nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ------------------------------------------------------------------------------- Tor is a registered trademark of The Tor Project, Inc. Ricochet is not in any way affiliated with or endorsed by The Tor Project. For more information about Tor, see https://www.torproject.org/. Tor is distributed under this license: Copyright (c) 2001-2004, Roger Dingledine Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson Copyright (c) 2007-2010, The Tor Project, Inc. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the names of the copyright owners nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. src/common/strlcat.c and src/common/strlcpy.c by Todd C. Miller are licensed under the following license: * Copyright (c) 1998 Todd C. Miller * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ------------------------------------------------------------------------------- The Qt Toolkit is Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies) and other contributors. Qt is licensed under the GNU Lesser General Public License version 2.1, available online at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html, and a special exception, the text of which is distributed with Qt. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. ------------------------------------------------------------------------------- Ricochet "R" icon by Lawrence Eastland, CC-BY-SA 4.0. See icons/LICENSE. ------------------------------------------------------------------------------- /* ==================================================================== * Copyright (c) 1998-2008 The OpenSSL Project. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. All advertising materials mentioning features or use of this * software must display the following acknowledgment: * "This product includes software developed by the OpenSSL Project * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" * * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to * endorse or promote products derived from this software without * prior written permission. For written permission, please contact * openssl-core@openssl.org. * * 5. Products derived from this software may not be called "OpenSSL" * nor may "OpenSSL" appear in their names without prior written * permission of the OpenSSL Project. * * 6. Redistributions of any form whatsoever must retain the following * acknowledgment: * "This product includes software developed by the OpenSSL Project * for use in the OpenSSL Toolkit (http://www.openssl.org/)" * * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. * ==================================================================== * * This product includes cryptographic software written by Eric Young * (eay@cryptsoft.com). This product includes software written by Tim * Hudson (tjh@cryptsoft.com). * */ Original SSLeay License ----------------------- /* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) * All rights reserved. * * This package is an SSL implementation written * by Eric Young (eay@cryptsoft.com). * The implementation was written so as to conform with Netscapes SSL. * * This library is free for commercial and non-commercial use as long as * the following conditions are aheared to. The following conditions * apply to all code found in this distribution, be it the RC4, RSA, * lhash, DES, etc., code; not just the SSL code. The SSL documentation * included with this distribution is covered by the same copyright terms * except that the holder is Tim Hudson (tjh@cryptsoft.com). * * Copyright remains Eric Young's, and as such any Copyright notices in * the code are not to be removed. * If this package is used in a product, Eric Young should be given attribution * as the author of the parts of the library used. * This can be in the form of a textual message at program startup or * in documentation (online or textual) provided with the package. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. All advertising materials mentioning features or use of this software * must display the following acknowledgement: * "This product includes cryptographic software written by * Eric Young (eay@cryptsoft.com)" * The word 'cryptographic' can be left out if the rouines from the library * being used are not cryptographic related :-). * 4. If you include any Windows specific code (or a derivative thereof) from * the apps directory (application code) you must include an acknowledgement: * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" * * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * The licence and distribution terms for any publically available version or * derivative of this code cannot be changed. i.e. this code cannot simply be * copied and put under another distribution licence * [including the GNU Public Licence.] */ ricochet-1.1.4/README.md000066400000000000000000000057251300720305500146000ustar00rootroot00000000000000### Anonymous metadata-resistant instant messaging that just works. Ricochet is an experimental kind of instant messaging that **doesn't trust anyone** with your identity, your contact list, or your communications. * You can chat without exposing your identity (or IP address) to *anyone* * Nobody can discover who your contacts are or when you talk (*metadata-free!*) * There are no servers or operators that could be compromised, exposing your information. * It's cross-platform and easy for non-technical users. ![Screenshot](ricochetscreen.png) ### How it works Ricochet is a peer-to-peer instant messaging system built on the Tor Network [hidden services](https://www.torproject.org/docs/hidden-services.html.en). Your login is your hidden service address, and contacts connect to you (not an intermediate server) through Tor. The rendezvous system makes it extremely hard for anyone to learn your identity from your address. Ricochet is not affiliated with or endorsed by The Tor Project. For more information, you can [read about Tor](https://www.torproject.org/about/overview.html.en) and [learn about Ricochet's design](https://github.com/ricochet-im/ricochet/blob/master/doc/design.md) or [protocol](https://github.com/ricochet-im/ricochet/blob/master/doc/protocol.md) (or the [old protocol](https://github.com/ricochet-im/ricochet/blob/master/doc/deprecated/protocol-1.0.txt)). Everything is [open-source](https://github.com/ricochet-im/ricochet/blob/master/LICENSE) and open to contribution. ### Experimental This software is an experiment. It hasn't been audited or formally reviewed by anyone. Security and anonymity are difficult topics, and you should carefully evaluate your risks and exposure with any software. *Do not rely on Ricochet for your safety* unless you have more trust in my work than it deserves. That said, I believe it does more to try to protect your privacy than any similar software, and is the best chance you have of withholding your personal information. ### Downloads Ricochet is available for Windows, OS X (10.7 or later), and as a generic Linux binary package. Visit the [releases page](https://github.com/ricochet-im/ricochet/releases) for the latest version and changelog. All releases and signatures are also available at https://ricochet.im/releases/. Binaries are PGP signed by `9032 CAE4 CBFA 933A 5A21 45D5 FF97 C53F 183C 045D`. ### Building from source See [BUILDING](https://github.com/ricochet-im/ricochet/blob/master/BUILDING.md) for Linux, OS X, and Windows build instructions. ### Other Bugs can be reported on the [issue tracker](https://github.com/ricochet-im/ricochet/issues). Translations can be contributed on [Transifex](https://www.transifex.com/projects/p/ricochet/). You can contact me at `ricochet:rs7ce36jsj24ogfw` or `john.brooks@dereferenced.net`. You should support [The Tor Project](https://www.torproject.org/donate/donate.html.en), [EFF](https://www.eff.org/), and [run a Tor relay](https://www.torproject.org/docs/tor-relay-debian.html.en). ricochet-1.1.4/config.tests/000077500000000000000000000000001300720305500157165ustar00rootroot00000000000000ricochet-1.1.4/config.tests/mingw-64aslr/000077500000000000000000000000001300720305500201505ustar00rootroot00000000000000ricochet-1.1.4/config.tests/mingw-64aslr/mingw-64aslr.pro000066400000000000000000000001361300720305500231240ustar00rootroot00000000000000include(../../hardened.pri) QMAKE_LFLAGS += $$HARDENED_MINGW_64ASLR_FLAGS SOURCES += test.cpp ricochet-1.1.4/config.tests/mingw-64aslr/test.cpp000066400000000000000000000000311300720305500216250ustar00rootroot00000000000000int main() { return 0; } ricochet-1.1.4/config.tests/sanitize-ubsan-more/000077500000000000000000000000001300720305500216125ustar00rootroot00000000000000ricochet-1.1.4/config.tests/sanitize-ubsan-more/sanitize-ubsan-more.pro000066400000000000000000000002341300720305500262270ustar00rootroot00000000000000include(../../hardened.pri) QMAKE_CXXFLAGS += $$HARDENED_SANITIZE_UBSAN_MORE_FLAGS QMAKE_LFLAGS += $$HARDENED_SANITIZE_UBSAN_MORE_FLAGS SOURCES += test.cpp ricochet-1.1.4/config.tests/sanitize-ubsan-more/test.cpp000066400000000000000000000000311300720305500232670ustar00rootroot00000000000000int main() { return 0; } ricochet-1.1.4/config.tests/sanitize-ubsan/000077500000000000000000000000001300720305500206525ustar00rootroot00000000000000ricochet-1.1.4/config.tests/sanitize-ubsan/sanitize-ubsan.pro000066400000000000000000000002221300720305500243240ustar00rootroot00000000000000include(../../hardened.pri) QMAKE_CXXFLAGS += $$HARDENED_SANITIZE_UBSAN_FLAGS QMAKE_LFLAGS += $$HARDENED_SANITIZE_UBSAN_FLAGS SOURCES += test.cpp ricochet-1.1.4/config.tests/sanitize-ubsan/test.cpp000066400000000000000000000000311300720305500223270ustar00rootroot00000000000000int main() { return 0; } ricochet-1.1.4/config.tests/sanitize/000077500000000000000000000000001300720305500175445ustar00rootroot00000000000000ricochet-1.1.4/config.tests/sanitize/sanitize.pro000066400000000000000000000002061300720305500221120ustar00rootroot00000000000000include(../../hardened.pri) QMAKE_CXXFLAGS += $$HARDENED_SANITIZE_FLAGS QMAKE_LFLAGS += $$HARDENED_SANITIZE_FLAGS SOURCES += test.cpp ricochet-1.1.4/config.tests/sanitize/test.cpp000066400000000000000000000000311300720305500212210ustar00rootroot00000000000000int main() { return 0; } ricochet-1.1.4/config.tests/stack-protector-strong/000077500000000000000000000000001300720305500223545ustar00rootroot00000000000000ricochet-1.1.4/config.tests/stack-protector-strong/stack-protector-strong.pro000066400000000000000000000002421300720305500275320ustar00rootroot00000000000000include(../../hardened.pri) QMAKE_CXXFLAGS += $$HARDENED_STACK_PROTECTOR_STRONG_FLAGS QMAKE_LFLAGS += $$HARDENED_STACK_PROTECTOR_STRONG_FLAGS SOURCES += test.cpp ricochet-1.1.4/config.tests/stack-protector-strong/test.cpp000066400000000000000000000000311300720305500240310ustar00rootroot00000000000000int main() { return 0; } ricochet-1.1.4/config.tests/stack-protector/000077500000000000000000000000001300720305500210425ustar00rootroot00000000000000ricochet-1.1.4/config.tests/stack-protector/stack-protector.pro000066400000000000000000000002241300720305500247060ustar00rootroot00000000000000include(../../hardened.pri) QMAKE_CXXFLAGS += $$HARDENED_STACK_PROTECTOR_FLAGS QMAKE_LFLAGS += $$HARDENED_STACK_PROTECTOR_FLAGS SOURCES += test.cpp ricochet-1.1.4/config.tests/stack-protector/test.cpp000066400000000000000000000000311300720305500225170ustar00rootroot00000000000000int main() { return 0; } ricochet-1.1.4/config.tests/vtable-verify/000077500000000000000000000000001300720305500204755ustar00rootroot00000000000000ricochet-1.1.4/config.tests/vtable-verify/test.cpp000066400000000000000000000000311300720305500221520ustar00rootroot00000000000000int main() { return 0; } ricochet-1.1.4/config.tests/vtable-verify/vtable-verify.pro000066400000000000000000000002201300720305500237700ustar00rootroot00000000000000include(../../hardened.pri) QMAKE_CXXFLAGS += $$HARDENED_VTABLE_VERIFY_FLAGS QMAKE_LFLAGS += $$HARDENED_VTABLE_VERIFY_FLAGS SOURCES += test.cpp ricochet-1.1.4/contrib/000077500000000000000000000000001300720305500147505ustar00rootroot00000000000000ricochet-1.1.4/contrib/README.sandboxing000066400000000000000000000034311300720305500177640ustar00rootroot00000000000000This document explains some of the sandboxing contributions available for Ricochet. Debian GNU/Linux sandboxing: usr.bin.ricochet-apparmor is a basic AppArmor policy to be installed in /etc/apparmor.d/; it may be enabled like so: cp usr.bin.ricochet-apparmor /etc/apparmor.d/usr.bin.ricochet-apparmor aa-enforce /etc/apparmor.d/usr.bin.ricochet-apparmor the minijail tool originally from the ChromeOS project is an easy way to use seccomp as a generic wrapper around any program. We use it to add seccomp support to ricochet without using the more useful or privileged modes. If minijail is supported for your platform, congratulations. If not, it may be built and used like so: apt-get install libcap-dev git clone https://chromium.googlesource.com/chromiumos/platform/minijail export CC=gcc make ./minijail0 -h ricochet-seccomp-amd64.policy is a loose seccomp filter policy to be used with the minijail tool originally from the ChromeOS project: minijail0 -n -S ricochet-seccomp-amd64.policy /usr/bin/ricochet ricochet-seccomp-x86.policy and ricochet-seccomp-arm.policy are the base policies for the x86 and arm architectures. They may need tweaking before they are useful. They need testing - only the amd64 code has been used extensively. It is also possible to run Ricochet inside of xpra like so: xpra start :23 --start-child ricochet --exit-with-children \ --no-pulseaudio \ --no-microphone \ --no-sharing \ --no-xsettings \ --no-mdns \ --no-notifications \ --no-bell \ --no-opengl=no \ --no-daemon && xpra attach :23 --no-tray --title=@title@ It is also possible to combine the AppArmor protection as well as the xpra and minijail commands. This allows for a reasonable start for sandboxing Ricochet from the host system where it is running on Debian GNU/Linux. ricochet-1.1.4/contrib/ricochet-seccomp-amd64.policy000066400000000000000000000023611300720305500223330ustar00rootroot00000000000000accept4: 1 access: 1 alarm: 1 arch_prctl: 1 bind: 1 brk: 1 capget: 1 capset: 1 chdir: 1 chmod: 1 chown: 1 clock_getres: 1 clone: 1 close: 1 connect: 1 dup: 1 dup2: 1 epoll_create: 1 epoll_ctl: 1 epoll_wait: 1 eventfd2: 1 execve: 1 exit: 1 exit_group: 1 faccessat: 1 fadvise64: 1 fchmod: 1 fchown: 1 fcntl: 1 fdatasync: 1 flock: 1 fstat: 1 fstatfs: 1 ftruncate: 1 futex: 1 getcwd: 1 getdents: 1 getegid: 1 geteuid: 1 getgid: 1 getpeername: 1 getpgrp: 1 getpid: 1 getppid: 1 getrandom: 1 getresgid: 1 getresuid: 1 getrlimit: 1 getrusage: 1 getsockname: 1 getsockopt: 1 gettid: 1 getuid: 1 ioctl: 1 kill: 1 listen: 1 lseek: 1 lstat: 1 madvise: 1 mkdir: 1 mmap: 1 mprotect: 1 mremap: 1 munmap: 1 nanosleep: 1 open: 1 openat: 1 pipe: 1 pipe2: 1 poll: 1 prctl: 1 pselect6: 1 read: 1 readlink: 1 readlinkat: 1 recvfrom: 1 recvmsg: 1 rename: 1 rmdir: 1 rt_sigaction: 1 rt_sigprocmask: 1 rt_sigreturn: 1 sched_getaffinity: 1 sched_yield: 1 select: 1 sendmsg: 1 sendto: 1 setgid: 1 setgroups: 1 setpgid: 1 setresgid: 1 setresuid: 1 setrlimit: 1 set_robust_list: 1 setsid: 1 setsockopt: 1 set_tid_address: 1 setuid: 1 shmat: 1 shmctl: 1 shmdt: 1 shmget: 1 shutdown: 1 socket: 1 socketpair: 1 stat: 1 statfs: 1 tgkill: 1 umask: 1 uname: 1 unlink: 1 wait4: 1 write: 1 writev: 1 ricochet-1.1.4/contrib/ricochet-seccomp-arm.policy000066400000000000000000000023141300720305500221750ustar00rootroot00000000000000accept4: 1 access: 1 alarm: 1 arch_prctl: 1 bind: 1 brk: 1 capget: 1 capset: 1 chdir: 1 chmod: 1 chown: 1 clock_getres: 1 clone: 1 close: 1 connect: 1 dup: 1 dup2: 1 epoll_create: 1 epoll_ctl: 1 epoll_wait: 1 eventfd2: 1 execve: 1 exit: 1 exit_group: 1 faccessat: 1 fadvise64: 1 fchmod: 1 fchown: 1 fcntl: 1 fdatasync: 1 flock: 1 fstat: 1 fstatfs: 1 ftruncate: 1 futex: 1 getcwd: 1 getdents: 1 getegid: 1 geteuid: 1 getgid: 1 getpeername: 1 getpgrp: 1 getpid: 1 getppid: 1 getresgid: 1 getresuid: 1 getrlimit: 1 getrusage: 1 getsockname: 1 getsockopt: 1 getuid: 1 ioctl: 1 kill: 1 listen: 1 lseek: 1 lstat: 1 madvise: 1 mkdir: 1 mmap: 1 mprotect: 1 mremap: 1 munmap: 1 nanosleep: 1 open: 1 openat: 1 pipe: 1 pipe2: 1 poll: 1 prctl: 1 pselect6: 1 read: 1 readlink: 1 recvfrom: 1 recvmsg: 1 rename: 1 rmdir: 1 rt_sigaction: 1 rt_sigprocmask: 1 rt_sigreturn: 1 sched_getaffinity: 1 sched_yield: 1 select: 1 sendmsg: 1 sendto: 1 setgid: 1 setgroups: 1 setpgid: 1 setresgid: 1 setresuid: 1 setrlimit: 1 set_robust_list: 1 setsid: 1 setsockopt: 1 set_tid_address: 1 setuid: 1 shmat: 1 shmctl: 1 shmdt: 1 shmget: 1 shutdown: 1 socket: 1 socketpair: 1 stat: 1 statfs: 1 tgkill: 1 umask: 1 uname: 1 unlink: 1 wait4: 1 write: 1 writev: 1 ricochet-1.1.4/contrib/ricochet-seccomp-x86.policy000066400000000000000000000023141300720305500220430ustar00rootroot00000000000000accept4: 1 access: 1 alarm: 1 arch_prctl: 1 bind: 1 brk: 1 capget: 1 capset: 1 chdir: 1 chmod: 1 chown: 1 clock_getres: 1 clone: 1 close: 1 connect: 1 dup: 1 dup2: 1 epoll_create: 1 epoll_ctl: 1 epoll_wait: 1 eventfd2: 1 execve: 1 exit: 1 exit_group: 1 faccessat: 1 fadvise64: 1 fchmod: 1 fchown: 1 fcntl: 1 fdatasync: 1 flock: 1 fstat: 1 fstatfs: 1 ftruncate: 1 futex: 1 getcwd: 1 getdents: 1 getegid: 1 geteuid: 1 getgid: 1 getpeername: 1 getpgrp: 1 getpid: 1 getppid: 1 getresgid: 1 getresuid: 1 getrlimit: 1 getrusage: 1 getsockname: 1 getsockopt: 1 getuid: 1 ioctl: 1 kill: 1 listen: 1 lseek: 1 lstat: 1 madvise: 1 mkdir: 1 mmap: 1 mprotect: 1 mremap: 1 munmap: 1 nanosleep: 1 open: 1 openat: 1 pipe: 1 pipe2: 1 poll: 1 prctl: 1 pselect6: 1 read: 1 readlink: 1 recvfrom: 1 recvmsg: 1 rename: 1 rmdir: 1 rt_sigaction: 1 rt_sigprocmask: 1 rt_sigreturn: 1 sched_getaffinity: 1 sched_yield: 1 select: 1 sendmsg: 1 sendto: 1 setgid: 1 setgroups: 1 setpgid: 1 setresgid: 1 setresuid: 1 setrlimit: 1 set_robust_list: 1 setsid: 1 setsockopt: 1 set_tid_address: 1 setuid: 1 shmat: 1 shmctl: 1 shmdt: 1 shmget: 1 shutdown: 1 socket: 1 socketpair: 1 stat: 1 statfs: 1 tgkill: 1 umask: 1 uname: 1 unlink: 1 wait4: 1 write: 1 writev: 1 ricochet-1.1.4/contrib/usr.bin.ricochet-apparmor000066400000000000000000000026541300720305500217000ustar00rootroot00000000000000# AppArmor Ricochet profile for Debian GNU/Linux # This profile is Free Software and released under the same license as Ricochet # itself. # # Copyleft 2015 Jacob Appelbaum # #include /usr/bin/ricochet { #include #include #include # Allow TCP connections network inet stream, network inet6 stream, /usr/lib/** mr, # Allow Ricochet to exec pulseaudio # This makes me very sad... # as it seems that you can't isolate playing and recording :( /usr/bin/pulseaudio ixr, # Allow Ricochet to exec tor /usr/bin/tor ixr, /usr/share/tor/geoip r, /usr/share/tor/geoip6 r, # Tor in turn needs various things /proc/sys/kernel/random/uuid r, /sys/devices/system/cpu/ r, # Allow Ricochet to read itself /usr/bin/ricochet r, /proc/[0-9]*/cmdline r, # Allow Ricochet to generate audio owner /{dev,run}/shm/pulse-shm* m, # Allow Ricochet to draw the UX /sys/devices/pci[0-9]*/**/uevent r, /run/udev/data/* r, # Allow Ricochet to load GTK themes /usr/share/themes/* r, /usr/share/themes/**/* r, owner @{HOME}/.gtkrc-2.0 r, # Allow Ricochet to look up all your machine's PII # Why does it need this stuff? BAD NEWS BEARS /etc/machine-id r, /var/lib/dbus/machine-id r, /etc/udev/udev.conf r, owner @{HOME}/.local/share/Ricochet/ rw, owner @{HOME}/.local/share/Ricochet/** rwmk, } ricochet-1.1.4/doc/000077500000000000000000000000001300720305500140555ustar00rootroot00000000000000ricochet-1.1.4/doc/deprecated/000077500000000000000000000000001300720305500161555ustar00rootroot00000000000000ricochet-1.1.4/doc/deprecated/protocol-1.0.txt000066400000000000000000000444061300720305500210630ustar00rootroot00000000000000This file aims to document the low-level details of communication and protocol in Ricochet. Unless otherwise noted, this document describes protocol version 0. Section 1 (protocol negotiation) is intended to be invariant, as it is responsible for negotiating the protocol version. Table of contents: 0. Conventions in this document 1. The hidden service layer 2. Protocol negotiation 2.1. Introduction syntax 3. Connection purpose and authentication 3.1. Authenticated contact connections 4. Connection management 4.1. Command connections 4.2. Data connections 5. Message processing 6. Commands and replies 6.1. Length field 6.2. Command field 6.3. State field 6.4. Identifier field 6.5. Data 7. Defined commands 7.1. 0x00 - Ping 7.2. 0x01 - Get connection secret 7.3. 0x10 - Chat message 8. Contact request connections 0. Conventions in this document 'Client' refers to the peer creating the connection 'Server' refers to the peer receiving an incoming connection. After authentication, this distinction is irrelevant. 1. The hidden service layer [TBD: describe use of hidden services and the properties they make available] Connections are made to port 9878 of the hidden service. 2. Protocol negotiation Immediately after establishing a connection, the client must send an introduction. The server is expected to expire any connection on which this introduction does not arrive in a reasonable amount of time. 2.1. Introduction syntax The client must send the following sequence: 0x49 0x4D nVersions [1 octet] versions [nVersions bytes] 'nVersions' describes the number of version fields to follow. Each version field is a single octet, where each value identifies an incompatible protocol. The value 0xFF is reserved to indicate failure in a later reply. In response, once the entire introduction has been received, the server responds with a single octet containing the protocol version chosen from the list provided by the client. This should be the highest mutually supported version. If there is no mutual version, the server sends 0xff (which is not a valid version value), and terminates the connection. Implementations MUST NOT expect any more than 4 octets (the introduction with one version) to arrive before attempting to process them. The client MUST NOT expect more than one octet (the response version) before processing that data, regardless of any data which may immediately follow. 3. Connection purpose and authentication Immediately after version negotiation has finished (i.e. the server has responded), the client must send a connection purpose field, which defines the type of connection. This is a single octet, with the following currently recognized values: 0x00 Contact command connection 0x01 Contact data connection 0x80 Contact request Any value below 0x20 (non-inclusive) shares the same immediate usage for authentication. The 0x00 value indicates a command connection (4.), while all others are auxiliary connections. The protocol version must be increased when introducing a new purpose. 3.1. Authenticated contact connections For connections with a purpose value below 0x20, the following authentication process is used to prove the identity of existing contacts. After sending the purpose, the client must follow with a 16-octet pre-exchanged secret. This secret is specific to each peer, and is defined during the contact request (or shortly thereafter). Because of the hidden service layer pre-authenticating the server end of the connection, and the existing layers of encryption, this value is sent in plaintext. The server looks up this value and matches it to any known contacts. It then sends a one-octet response code; the value 0x00 for this code indicates success, while all other values are assumed to be failure. Any failure response may be followed by the server immediately closing the connection, or may be followed by additional information. No response other than 0x00 will result in the connection continuing to exist. The following responses are currently defined: 0x00 Success 0x01 General failure; immediately closes the connection 0x02 Unrecognized secret After a success reply, the connection transitions into message parsing and may be used bidirectionally as desired. The "unrecognized secret" response should be considered a permanent failure and disable future connection attempts without issuing a new contact request. 4. Connection management 4.1. Command connections Connections initiated with a purpose of 0x00 (see section 3.) are command connections, over which the protocol defined in section 5 through 7 is used. Generally, there may only be one command connection with a peer, and a new connection is considered to replace any existing connection (causing failure of all incomplete commands). A race is possible when two peers connect simultaniously and each establish a command connection. To resolve this situation, the following rules are used: - If the remote peer establishes a command connection prior to sending authentication data for an outgoing attempt, the outgoing attempt is aborted and the peer's connection is used. - If the existing connection is more than 30 seconds old, measured from the time authentication is sent, it is replaced and closed. - Otherwise, both peers close the connection for which the server's onion-formatted hostname is considered less by a strcmp function. 4.2. Data connections All commands and general communication take place over the command connection. It's important that command connections keep low latency regardless of throughput, so they are not suitable for transferring large data. Peers may establish data connections with a purpose of 0x01 for the transfer of larger amounts of data. These transfers must first be negotiated on the command connection, where the receiving peer assigns a 4 octet identifier for the blob of data. Either peer may use a data connection regardless of why it was originally established. Connections should not be closed by expiration if a pending identifier has been issued for the peer's use. It should be possible for the sending peer in a transfer to indicate that the recipient should establish a data connection, to avoid relying on the connectivity of both peers. Data arrives in the following form: identifier [4 octets] length [64-bit unsigned big endian integer] data [length octets] Unexpected identifiers may be ignored or result in closing the data connection. 5. Message processing Most communication takes place over the command or message protocol, used on authenticated connections with contacts. This is a sequence of messages, each of which is a command or a reply. All commands must generate a reply, unless otherwise specifically defined. This is done to allow intelligent handling of various error situations, and to enable better internal API for managing outstanding commands. Replies have a concept of finality; a command may generate many replies, but must have exactly one final reply, which obviously must be the last. Each command also has an identifier; this is a 16-bit value unique to the sender of the command, which serves to relate replies back to the original command. As a result of this, there is no requirement that replies be immediate, and they may even be interleaved with other replies. The identifier may be considered unused immediately after a final reply arrives with that identifier set. Identifiers are specific to a single connection. 0 is reserved for identifiers, and should not be used. One last distinction is between two types of messages that have very different behavior for processing. Buffered messages (which are the majority) include their length, which may not exceed 65,540 octets inclusive of the header and the length itself, meaning that they may have a maximum payload of 65,534 octets. As the name implies, buffered messages are buffered until all data has arrived, and processed once. 6. Commands and replies The syntax of a message (a command or reply) is: length [16-bit big endian integer] command [1 octet] state [1 octet] identifier [16-bit big endian integer] data [length-1 octets, or unspecified] These fields will be addressed in order. 6.1. Length field The length is a 16-bit big endian integer defining the size in bytes of the data payload in the message. This length does not include the header of the message. It may be 0 for messages with no associated data. 6.2. Command field The command is a one octet identifier for the command that should be performed. When adding commands, great care should be taken to avoid collisions with any other implementation. Command values below 0x80 should be reserved for definition in this official document, while those above are open for third party usage with appropriate precautions. A feature inspection command may be introduced in the future to handle this issue. For details on existing commands, see section 7. 6.3. State field The state field can be thought of as a subdivision of the command. It enables different usages of the same command, and is also used to indicate replies and reply status. The state is one octet, which is to be interpreted as follows, where 0 is the most significant bit (0x80), and 7 is the least significant (0x01). Bit 0 (0x80) indicates if the message is a reply. If bit 0 is NOT set, the message is a command: Bit 1 (0x40) is reserved, and should have a value of 1. Bits 2 through 7 are available for command-specific usage. If bit 0 IS set: Bit 1 (0x40) indicates if the reply is a final reply. No more replies may arrive with the same identifier for that command after the final reply. Bit 2 (0x20) indicates if the command was successful. This bit should be set for any reply which does not indicate failure, but is largely intepreted by the command itself. IF bit 2 is NOT set (i.e. the command failed), all values with bit 3 (0x10) set are reserved for protocol usage as defined below. All other bits (3-7 for success, 4-7 for failure) are available for command-specific usage. The following chart describes these values and their meaning: 0x00 to 0x3F Reserved (unused) 0x40 to 0x7F Command 0x80 to 0x8F Reply Intermediate Failure Available 0x90 to 0x9F Reply Intermediate Failure Reserved 0xA0 to 0xBF Reply Intermediate Successful 0xC0 to 0xCF Reply Final Failure Available 0xD0 to 0xDF Reply Final Failure Reserved 0xE0 to 0xFF Reply Final Successful 6.4. Identifier field The identifier is a 16-bit big endian integer, selected when sending the command. It is unique to the peer and connection. It is illegal to send a command with the same identifier as a command for which a final reply has not arrived. It is illegal to send a reply with an identifier that does not match an outstanding command from the peer. The identifier 0 is reserved. 6.5. Data The data is arbitrary data, specific to the command (and possibly state). Its length is defined by the length field, which may be 0. 7. Defined commands This is a comprehensive listing of all commands implemented in the version of Ricochet to which this document corrosponds, and potentially those used by other applications that are considered reserved for that purpose. 7.1. 0x00 - Ping The ping command has no data, and results in a single, final, successful reply with a command-specific state of 0 (0xE0). It has no other effect. 7.2. 0x01 - Get connection secret The command has no data. If successful, the peer will send a single, final reply with a command state of 0 (0xE0), and the 16-byte secret that is used to authenticate the local (command outgoing) end when establishing a connection. This is a dirty trick used during contact requests to allow the requesting end of the request to discover the secret that it should use. 7.3. 0x10 - Chat message The command sends the following data: timeDelta 32-bit signed big-endian integer; seconds before now that the message was written. A negative value (indicating the future) may be rejected. lastReceived 16-bit big-endian integer; command identifier of the last chat message that had been received from the peer when this message was sent. length 16-big unsigned big-endian integer; length in octets of the text field text UTF8-encoded message text of 'length' octets. If successful, a single, final reply will be sent. 8. Contact request connections Contact requests are indicated by a connection with a purpose of 0x80. Immediately after receiving the purpose, the server will respond with a 16-octet randomly generated cookie. Once the cookie is received, the client (i.e. the peer that is sending a request) must send the following structure: length 16-bit big-endian unsigned integer; length in octets of the entire request message, inclusive of itself. serverHostname 16 octets, base32-encoded onion hostname of the intended recipient of the request. serverCookie 16 octets, the cookie provided by the server connSecret 16 octets, random data that the recipient can use to authenticate for a contact connection. pubKeyLength 16-bit big-endian unsigned integer; length in octets of the pubKey field pubKey PEM-encoded hidden service public key of pubKeyLength octets nicknameLength 16-bit big-endian unsigned integer; length in octets of the nickname field nickname UTF-8 encoded string of nicknameLength octets; suggested nickname for this contact messageLength 16-bit big-endian unsigned integer; length in octets of the message field message UTF-8 encoded string of messageLength octets; freeform message to be shown with the request signatureLength 16-bit big-endian unsigned integer; length in octets of the signature field signature RSA signature of the SHA256 digest of the fields from serverHostname to message inclusive. Upon receiving the request, the server must verify that it is correct: - length must be greater than 58 - serverHostname must equal the onion hostname that received this request - serverCookie must equal the random cookie sent by the server - pubKey must be a parseable and valid RSA public key - at least one of nickname or message must be non-empty - signature must be a valid signature by pubKey of the previous fields, excluding the length. Verifying serverHostname and serverCookie prevents replay and forwarding attacks on the request. The signature authenticates that the origin of the request is able to publish the hidden service represented by pubKey. The requesting service's hostname can be calculated from pubKey. After the request, the server will respond with a response code. All responses with the exception of 0x00 (acknowledge) and 0x01 (accept) are considered errors. The following are currently-defined responses: 0x00 Acknowledged (see below) 0x01 Accepted (see below) 0x40 Rejected by user 0x80 Syntax error 0x81 Verification error 0x82 Request requires either a nickname or a message All of the error responses (that is, any with a value greater than 0x01) will be followed closing the connection. In the case of user rejection, and potentially others, the contact ID used for this request may be added to a blacklist, causing any further requests to be immediately rejected without notifying the user. The acknowledged response is a special case; this indicates that the request was valid and has been processed, but that no response has yet been made. After the acknowledged response, the client should leave open the connection and wait for another response code to arrive. However, this is not a requirement. Immediately after the accepted response, both peers are expected to transition the connection to an established and authenticated command connection (i.e., they begin the message protocol). The requesting peer MUST be willing to accept normal contact connections, authenticated with the connSecret field from the request, at any time after the request has been sent. If such a connection is established, the requesting peer should treat that as implicitly accepting the request. This allows the request to be accepted if the request connection has since been lost. ricochet-1.1.4/doc/design.md000066400000000000000000000126561300720305500156620ustar00rootroot00000000000000# Technical design of Ricochet Ricochet is an instant messaging system designed around Tor hidden services. This document describes the goals and design of that system from a technical perspective. The reader should be familiar with [Tor](https://www.torproject.org/about/overview.html.en) and [hidden services](https://www.torproject.org/docs/hidden-services.html.en). ## Goals for the project To implement a real-time messaging system with these properties: * Users aren't personally identifiable by contacts or their address * Communication is authenticated and private * No person or server can access contact lists, message history, or other metadata * Resist censorship and monitoring at the local network level * Resist blacklisting or denial of service against users * Accessible and understandable for non-technical users * Reliability and interactivity comparable with traditional IM services ## Introduction Each user identity is represented by a hidden service as its connection point. The identity is shared as a contact ID in the form `ricochet:qjj5g7bxwcvs3d7i`, which is unique and sufficient to connect to the service. When online, the user publishes a hidden service corresponding with the onion hostname in the contact ID, and accepts bidirectionally anonymous connections, which are authenticated as known contacts or used to receive contact requests. Known contacts use a [customized protocol](https://github.com/ricochet-im/ricochet/blob/master/doc/protocol.md) over these connections for basic instant messaging features. ## Contact requests Like classic instant messaging systems, you can request to add a user to your contacts using their contact ID. That request must be accepted before messages can be sent or received. A request is made by connecting to the service, indicating that the connection is for a contact request, and providing information including the sender's contact ID. The sender attempts periodically to make a connection for the contact request when online. The request includes: * The hidden service hostname of the recipient hidden service * A random cookie provided at the start of the connection by the recipient * A random secret the recipient can use to authenticate normal connections * The full public key corresponding to the hidden service for the sender's identity * (Optionally) A nickname and short introductory message * An RSA signature of the above with the same public key The recipient can calculate the sender's contact ID based on the public key, and authenticate it by verifying the signature on the request. This proves that the sender can publish the hidden service represented by their contact ID. The recipient user has the choice to accept or reject that request. A rejected public key may be added to a blacklist and rejected automatically from future requests. For more detail, see the [protocol documentation](https://github.com/ricochet-im/ricochet/blob/master/doc/protocol.md#contact-request-channel). ## Contact connections When online, ricochet periodically attempts to make connections to all contacts. If the connection attempt succeeds, it is kept open and that contact is considered online. Only one connection is needed per contact, and it does not matter which end initiated the connection. The hidden service layer conveniently provides confidentiality, ephemerality, and authenticates the server side, so the application protocol is kept very simple. The client side of a connection authenticates with a pre-shared random secret established during or soon after the contact request. A simple command/reply based binary protocol is used to communicate. It attempts to offer some reliability for commands to recover from unstable connections. This was chosen over any existing protocol (such as XMPP) or implementation for simplicity and strict control over the surface exposed for attacks against security and anonymity. The protocol includes a version negotiation step for future expansion. A detailed description of the format and commands can be found in the [protocol documentation](https://github.com/ricochet-im/ricochet/blob/master/doc/protocol.md#introduction-and-version-negotiation). ## UI and usability User interface is an extremely important and often under-considered aspect of security and anonymity. Less technical users should be able to easily learn how to use the software, and what they need to do to keep themselves safe. Ricochet's UI aims to be simple and familiar to users of other IM software. Knowledge of Tor and networking concepts aren't a requirement. It should be easy to do things the right way, hard to accidentally break them, and possible for technical users to tweak. Contributions in this area are very welcome, especially in translations and non-technical documentation and review. ## Future development The design described here is close to the simplest implementation possible. The protocol has the potential to be extended to enable features like file transfer or even voice/video streaming. More advanced use of hidden services (e.g. authentication) can mitigate the risks of publishing a publicly connectable service. Separate services or more elaborate designs can be used to prevent attacks by non-contacts. Future development in Tor can improve the cryptography and principles behind hidden services. Your ideas can go here. Ideas, suggestions, and bugs are welcome on the [issue tracker](https://github.com/ricochet-im/ricochet/issues). Patches are welcome via pull request. ricochet-1.1.4/doc/protocol.md000066400000000000000000000541721300720305500162510ustar00rootroot00000000000000## Overview Ricochet is a peer-to-peer instant messaging system built on anonymity networks. This document defines the communication protocol between two Ricochet instances, as carried out over a Tor hidden service connection. The protocol is defined in three layers: The **connection layer** describes the use of an anonymized TCP-style connection for peer-to-peer communication. The **packet layer** separates the connection into a series of *packets* delivered to *channels*. This allows multiplexing different operations on the same connection, and packetizes data for channel-level parsing. The **channel layer** parses and handles packets according to the *channel type* and the state of that specific channel. ### Connections > TODO: This is a brief explanation; we should reference a design/architecture document with more > details. ##### Hidden services Ricochet uses Tor [hidden services][rend-spec] as a transport; the reader should be familiar with that architecture and the properties it provides. In particular: * The hostname is calculated from a hash of the server's public key, and serves to authenticate the server without relying on a third party * Connections are encrypted end-to-end, using the server's key and a DHE handshake to provide forward secrecy * Both ends of a connection are anonymous in that neither peer should be able to identify or locate the other, and no relay should be able to connect an identity to the requests it makes * Impersonating a server without its private key requires an 80-bit SHA1 collision using a valid RSA key > TODO: We should explore additional cryptography on top of what Tor offers; see > [issue 72](https://github.com/ricochet-im/ricochet/issues/72). ##### Usage Each Ricochet instance publishes a hidden service, which serves as its identity and accepts connections from contacts. When it first comes online, it attempts to connect to the addresses of known contacts. If a connection is made, it is held open; a contact is considered online when there is an open connection. Connections are made on port 9878. > This solution isn't ideal; we'll be exploring better designs on top of hidden services to improve > scalability and anonymity properties. Only one active connection is needed for a contact. Connections are fully bidirectional and all behavior is equivalent regardless of which peer acts as server at the transport level. Ricochet does not use central servers; connections are made to services published directly by your contacts with no intermediary. Keeping open connections to unknown peers poses a risk for various attacks, including resource exhaustion. Clients must either authenticate or take other useful action (e.g. delivering a contact request) quickly. The server side of the connection should expire unknown connections. ### Channels Channels divide up the connection to allow multiplexing, extensibility, and stateful behavior for *packets*. The **channel id** associates packets with an instance of a channel on the connection, which was previously created by an *OpenChannel* message. The **channel type** defines how packets are parsed and handled. Distinct features have separate channel types; for example, `im.ricochet.chat` and `im.ricochet.file-transfer`. By convention, these are in reverse-URI form. Channels exist within a connection. The channel ID is unique only within that connection, and all channels are closed when the connection is lost. Channels must be explicitly created with an *OpenChannel* message. The recipient of that message chooses to accept or reject the channel; for example, it may reject channel types it doesn't support, or won't allow this peer to access. Channel instances also provide a state for messages. For example, all operations associated with the transfer of one file take place on the same channel, and a second file transfer would use a second channel of the same type. Both peers may send packets to the same channel. Depending on the channel type, messages may be fully bidirectional or may be a command-response protocol. At the beginning of the connection, one channel exists automatically: the *control channel*. As a special case, it always has a channel ID of `0`. The control channel provides functionality for creating new channels and maintenance of the underlying connection. ### Authentication Ricochet needs a variety of levels and types of authentication; known contacts might have a strong proof of identity, while a request from a new person comes with a different proof and an anti-spam "proof of work". Some features could allow unauthenticated use. To support these scenarios, there is no pre-protocol authentication step. Peers add credentials to their connection by opening and completing various types of authentication channels. The most common example is `im.ricochet.auth.hidden-service`: the peer creates a channel of this type and carries out its protocol to prove that it has the private key for a hidden service name. Afterwards, that peer can send a contact request, and the recipient is able to know the source of that request. Another hypothetical example is `example.hashcash`: the peer would complete a proof of work as evidence that it doesn't intend to spam the recipient. These credentials are associated with the connection. For example, you may decide to not allow an `im.ricochet.chat` channel unless the peer has completed `im.ricochet.auth.hidden-service` authentication for a known contact's address. The hidden service transport provides one special case: the server end of the connection is authenticated equivalent to `im.ricochet.auth.hidden-service` at the beginning of the connection, and must be given equivalent privileges. ### Conventions Unless otherwise noted, these conventions and definitions are used for the protocol and this document: * *Peer* refers to either Ricochet instance on a connection * *Recipient* refers to the peer which received the message * Channels encode data using [protocol buffers][protobuf], with one protobuf message per packet * Unless the channel type specifies another mechanism, unknown/unparsable messages result in closing the channel. * Protocol behavior which appears malicious or strange may trigger consequences * Strings are UTF-8 encoded and should be carefully validated and handled * Any reply may be artificially delayed, but order must be preserved ## Specification ### Introduction and version negotiation Immediately after establishing a connection, the client side must send an introduction message identifying versions of the protocol that it is able to use. The server responds with one of those versions, or an error indicating that no compatible version exists. This step exists to enable smoother protocol changes in the future, and for better compatibility with old clients. The client begins the connection by sending the following raw sequence of bytes: ``` 0x49 0x4D nVersions // One byte, number of supported protocol versions, must be at least 1 nVersions times: version // One byte, protocol version number ``` The total size is 3 plus the number of supported versions bytes. The number of supported versions must be at least 1. The server side of the connection must respond with a single byte for the selected version number, or 0xFF if no suitable version is found. This document describes protocol version 1. Known versions are: ``` 0 The Ricochet 1.0 protocol 1 This document ``` If the negotiation is successful, the connection can be immediately used to begin exchanging messages (the packet layer, below). ### Packet layer The base layer on the connection is a trivial packet structure: ``` uint16 size // Big endian, including the header bytes uint16 channel // Big endian, channel identifier bytes data // Content of the packet ``` The connection reads and buffers data until it has a full packet, then looks up the channel identifier within the list of open channels. If the channel is found, data is passed to it for parsing and handling. The only other functionality implemented at this layer is closing a channel. A channel is closed by sending a packet to that channel with 0 bytes of data. When a packet is received for an unknown channel, the recipient responds by closing that channel. Note that packets are limited to 65,535 bytes in size, including the 4-byte header. To avoid causing latency on low throughput connections, channels should keep packets as small as possible. If a channel type requires larger packets of data, it must define a way to reassemble them specific to that channel type. ### Control channel The control channel is a special case: it is the only channel open from the beginning of a connection, and it is always assigned the channel identifier `0`. If the control channel is closed, the connection must also terminate. The control channel contains methods used for maintenance of the connection and the creation of other channels. It is a stateless series of protobuf-serialized `ControlMessage`, with one message encoded per packet. Both peers on the connection may send all messages. ##### Packet ```protobuf message Packet { // Must contain exactly one field optional OpenChannel open_channel = 1; optional ChannelResult channel_result = 2; optional KeepAlive keep_alive = 3; optional EnableFeatures enable_features = 4; optional FeaturesEnabled features_enabled = 5; } ``` All packets sent to the control channel must encode a *Packet*, with exactly one field. ##### OpenChannel ```protobuf message OpenChannel { required int32 channel_identifier = 1; // Arbitrary unique identifier for this channel instance required string channel_type = 2; // String identifying channel type; e.g. im.ricochet.chat // It is valid to extend the OpenChannel message to add fields specific // to the requested channel_type. extensions 100 to max; } ``` Requests to open a channel of the type *channel_type*, using the identifier *channel_identifier* for packets. Additional data may be added in extensions to this message. The recipient of an OpenChannel message checks whether it supports the *channel_type*, if the *channel_identifier* is valid and unassigned, and the validity of any extension data. The recipient also checks whether this connection allows channels of this type; for example, if the peer is sufficiently authenticated. If the request is allowed, *channel_identifier* will be sent with packets destined for this channel within this connection. It is also used to associate the *ChannelResult* message with this request. There are several rules that must be followed when choosing or accepting a *channel_identifier*: * The client side of a connection may only open odd-numbered channels * The server side may only open even-numbered channels * The identifier must fit within the range of uint16 * The identifier must not be used by an open channel * The identifier should increase for every OpenChannel message, wrapping if necessary. Identifiers should not be re-used except after wrapping. The even/odd restrictions on *channel_identifier* prevent peers from racing to open a channel with the same id. Channels are tied to a specific connection, so there is no collision between connections. By convention, channel types are in reverse URI format, e.g. `im.ricochet.chat`. A *ChannelResult* message must always be generated in response. If the request is egregiously invalid, the connection may be terminated instead. ##### ChannelResult ```protobuf message ChannelResult { required int32 channel_identifier = 1; // Matching the value from OpenChannel required bool opened = 2; // If the channel is now open enum CommonError { GenericError = 0; UnknownTypeError = 1; UnauthorizedError = 2; BadUsageError = 3; FailedError = 4; } optional CommonError common_error = 3; optional string error_message = 4; // As with OpenChannel, it is valid to extend this message with fields specific // to the channel type. extensions 100 to max; } ``` Sent in response to an *OpenChannel* message, with the same *channel_identifier* value. If *opened* is true, the channel is now ready to accept packets tagged with that identifier. ##### KeepAlive ```protobuf message KeepAlive { required bool response_requested = 1; } ``` A simple ping message. If *response_requested* is true, a *KeepAlive* message is generated in response with *response_requested* as false. ##### EnableFeatures ```protobuf message EnableFeatures { repeated string feature = 1; extensions 100 to max; } message FeaturesEnabled { repeated string feature = 1; extensions 100 to max; } ``` Simple feature negotiation. Either peer may send the *EnableFeatures* message with a list of strings representing protocol changes or features. The recipient must respond with *FeaturesEnabled* containing the subset of those strings it recognizes and has enabled. No such feature strings are currently defined, and the current implementation should always respond with an empty list. ### Chat channel | Channel | Detail | | ------------------ | ------ | | **Channel type** | `im.ricochet.chat` | | **Purpose** | Sending text-based instant messages | | **Direction** | One-way: Only initiator of the channel sends commands, and recipient sends replies | | **Singleton** | Only one chat channel is created by each peer on the connection | | **Authentication** | Requires `im.ricochet.auth.hidden-service` as a known contact | A chat channel allows the initiator (the peer who created the channel) to send messages, and receive acknowledgement for those messages. The opposing peer should also create a chat channel to send its own chat messages. Acknowledgement must be on the same channel as the original message. One peer may not open more than one chat channel on the same connection. Two chat channels (one per peer) are used to avoid ambiguity on which peer creates a chat channel, or which channel would be used in a race situation. ##### Packet ```protobuf message Packet { optional ChatMessage chat_message = 1; optional ChatAcknowledge chat_acknowledge = 2; } ``` ##### ChatMessage ```protobuf message ChatMessage { required string message_text = 1; optional uint32 message_id = 2; // Random ID for ack optional int64 time_delta = 3; // Delta in seconds between now and when message was written } ``` A *message_id* of zero (or omitted) indicates that the recipient doesn't expect acknowledgement. If *message_id* is non-zero, the recipient should acknowledge receiving this message by sending *ChatAcknowledge*. Unacknowledged messages may be re-sent with the same *message_id*, and the recipient should drop duplicate messages with an identical non-zero *message_id*, after sending an acknowledgement. Sometimes, messages may be delayed or potentially lost across connections over a short period of time. In order to allow messages to be re-sent after a lost connection, clients should try to avoid choosing message ids from a recent connection (with the same peer) at the start of a new connection. For example, that can be done by randomizing the first message id for a channel. The *time_delta* field is a delta in seconds between when the message was composed and when it is being transmitted. For messages that are sent immediately, it should be 0 or omitted. If a message was written and couldn't be sent immediately (due to a connection failure, for example), the *time_delta* should be an approximation of when it was composed. A positive value does not make any sense, as it would indicate a message composed in the future. ##### ChatAcknowledge ```protobuf message ChatAcknowledge { optional uint32 message_id = 1; optional bool accepted = 2 [default = true]; } ``` Acknowledge receipt of a *ChatMessage*. ### Contact request channel | Channel | Detail | | ------------------ | ------ | | **Channel type** | `im.ricochet.contact.request` | | **Purpose** | Introduce a new client and ask for user approval to send messages | | **Direction** | One-way: Only initiator of the channel sends commands, and recipient sends replies | | **Singleton** | One instance created only by the client side of a connection | | **Authentication** | Requires `im.ricochet.auth.hidden-service` | Contact requests are sent to introduce oneself to the recipient and ask for further contact, including being put on the recipient's persistent contact list. The requesting client must have authenticated using `im.ricochet.auth.hidden-service` to prove ownership of a hidden service name. The recipient isn't required to immediately respond to a request. If the request is approved, the recipient may connect to the requesting client, and that is treated as implicitly accepting the request. ##### OpenChannel ```protobuf extend Control.OpenChannel { optional ContactRequest contact_request = 200; } extend Control.ChannelResult { optional Response response = 201; } ``` The OpenChannel message on a contact request channel must include the `contact_request` extension. A successful ChannelResult must include the `response` extension. If the response finishes the request, the channel will be closed immediately. Otherwise, the channel remains open to wait for another *Response* message (e.g. going from Pending to Accepted). ##### ContactRequest ```protobuf // Sent only as an attachment to OpenChannel message ContactRequest { optional string nickname = 1; optional string message_text = 2; } ``` Deliver a contact request, usually with a message and nickname attached. The "identity" of the request is proven through `im.ricochet.auth.hidden-service` authentication. The request is sent as an extension on the *OpenChannel* message. ##### Response ```protobuf message Response { enum Status { Undefined = 0; // Not valid in transmitted messages Pending = 1; Accepted = 2; Rejected = 3; Error = 4; } required Status status = 1; optional string error_message = 2; } ``` Indicates the status of a contact request. The *Pending* status may be followed by another *ContactRequestResponse* with a final status. All other statuses must be followed by closing the channel, and may also close the connection. Closing the channel or the connection does not imply having a response - for example, the recipient may decide to time out the connection while it is waiting in the *Pending* state. The initial *Response* is sent as an extension to the *ChannelResult* message when the channel is opened. If that response is final, the channel is closed immediately after. Otherwise, the channel remains open, and the only valid message is another *Response*. If a request is *Rejected*, the requesting client must not send that request again, unless the user has manually cancelled the previous request and made a new one. Recipients should automatically reject excessive or abusive requests. If an *Error* occurs, the requesting client may only request again if it believes the error is solved. Recipients should automatically reject requests after repetitive errors. ### AuthHiddenService | Channel | Detail | | ------------------ | ------ | | **Channel type** | `im.ricochet.auth.hidden-service` | | **Purpose** | Authenticate as the owner of a Tor hidden service | | **Direction** | One-way: Only initiator of the channel sends commands, and recipient sends replies | | **Singleton** | One instance created only by the client side of a connection | | **Authentication** | No prior authentication required | The `im.ricochet.auth.hidden-service` channel is used to prove ownership of a hidden service name by demonstrating ownership of a matching private key. This is used to authenticate as a known contact, or to prove ownership of a service name before sending a contact request. As a result of the transport, the server side of a hidden service connection is considered to have automatically completed `im.ricochet.auth.hidden-service` authentication, and must be allowed equivalent access. ##### Packet ```protobuf extend OpenChannel { optional bytes client_cookie = 7200; // 16 random bytes } extend ChannelResult { optional bytes server_cookie = 7200; // 16 random bytes } message Packet { optional Proof proof = 1; optional Result result = 2; } ``` The channel is opened by the peer who wishes to authenticate itself. The *OpenChannel* message must contain a *client_cookie* of 16 bytes. A successful *ChannelResult* message must include the *server_cookie* field, with a randomly generated value used to prevent replayed authentication. ##### Proof ```protobuf message Proof { optional bytes public_key = 1; // DER encoded RSA public key optional bytes signature = 2; // RSA signature } ``` The proof is calculated as: ``` // + represents concatenation, and function is HMAC-SHA256(key, message) HMAC-SHA256(client_cookie + server_cookie, client_hostname // base32-encoded client address, without .onion + recipient_hostname // base32-encoded server address, without .onion ) ``` This proof is signed with the hidden service's private key using PKCS #1 v2.0 (as per OpenSSL RSA_sign) to make *signature*. The recipient of this message must: * Reject any message with a public_key field too large or not correctly formed to be a DER-encoded 1024-bit RSA public key * Reject any message with a signature field of an unexpected size * Decode the public_key, and calculate its 'onion' address per [rend-spec][rend-spec] * Build the proof message * Verify that *signature* is a valid signature of the proof by *public_key* ##### Result ```protobuf message Result { required bool accepted = 1; optional bool is_known_contact = 2; } ``` If authentication is successful as a known contact, whose connection will be allowed to remain open without any further purpose, the *is_known_contact* flag must be set as true. If this flag is not set, the authenticating client should assume that it is not authorized (except e.g. to send a contact request). After sending *Result*, the channel should be closed. [rend-spec]: https://gitweb.torproject.org/torspec.git/blob/HEAD:/rend-spec.txt [protobuf]: https://code.google.com/p/protobuf/ ricochet-1.1.4/hardened.pri000066400000000000000000000037641300720305500156100ustar00rootroot00000000000000load(configure) # Define common variables; these are used by config tests _and_ the actual build # Supported in gcc 4.8+ HARDENED_SANITIZE_FLAGS = -fsanitize=address # Supported in gcc 4.9+ HARDENED_SANITIZE_UBSAN_FLAGS = -fsanitize=undefined -fsanitize=integer-divide-by-zero -fsanitize=bounds -fsanitize=alignment -fsanitize=float-divide-by-zero -fsanitize=float-cast-overflow -fno-sanitize-recover # Supported in gcc 5.0+ HARDENED_SANITIZE_UBSAN_MORE_FLAGS = -fsanitize=vptr -fsanitize=object-size # vtable-verify requires some OS support; see https://bugzilla.novell.com/show_bug.cgi?id=877239 HARDENED_VTABLE_VERIFY_FLAGS = -fvtable-verify=std HARDENED_STACK_PROTECTOR_STRONG_FLAGS = -fstack-protector-strong HARDENED_STACK_PROTECTOR_FLAGS = -fstack-protector --param=ssp-buffer-size=4 HARDENED_MINGW_64ASLR_FLAGS = -Wl,--dynamicbase -Wl,--high-entropy-va # Run tests and apply options where possible CONFIG(hardened) { # mingw is always PIC, and complains about the flag !mingw:HARDEN_FLAGS = -fPIC CONFIG(debug,debug|release): qtCompileTest(sanitize):HARDEN_FLAGS += $$HARDENED_SANITIZE_FLAGS qtCompileTest(sanitize-ubsan):HARDEN_FLAGS += $$HARDENED_SANITIZE_UBSAN_FLAGS qtCompileTest(sanitize-ubsan-more):HARDEN_FLAGS += $$HARDENED_SANITIZE_UBSAN_MORE_FLAGS qtCompileTest(vtable-verify):HARDEN_FLAGS += $$HARDENED_VTABLE_VERIFY_FLAGS qtCompileTest(stack-protector-strong) { HARDEN_FLAGS += $$HARDENED_STACK_PROTECTOR_STRONG_FLAGS } else { qtCompileTest(stack-protector):HARDEN_FLAGS += $$HARDENED_STACK_PROTECTOR_FLAGS } mingw { qtCompileTest(mingw-64aslr):QMAKE_LFLAGS *= $$HARDENED_MINGW_64ASLR_FLAGS QMAKE_LFLAGS *= -Wl,--nxcompat -Wl,--dynamicbase } QMAKE_CXXFLAGS *= $$HARDEN_FLAGS QMAKE_LFLAGS *= $$HARDEN_FLAGS # _FORTIFY_SOURCE requires -O, so only use on release builds CONFIG(release,debug|release):QMAKE_CXXFLAGS *= -D_FORTIFY_SOURCE=2 # Linux specific unix:!macx:QMAKE_LFLAGS *= -pie -Wl,-z,relro,-z,now } ricochet-1.1.4/icons/000077500000000000000000000000001300720305500144235ustar00rootroot00000000000000ricochet-1.1.4/icons/LICENSE000066400000000000000000000023331300720305500154310ustar00rootroot00000000000000Ricochet "R" icon Copyright 2014, Lawrence Eastland (lawrence@eyedesign.com.au) This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. ------------------------------------------------------------------------ ricochet_icons.ttf is composed from icons by the following authors: ## Font Awesome Copyright (C) 2012 by Dave Gandy Author: Dave Gandy License: SIL 1.1 (http://scripts.sil.org/OFL) Homepage: http://fortawesome.github.com/Font-Awesome/ ## Web Symbols Copyright (c) 2011 by Just Be Nice studio. All rights reserved. Author: Just Be Nice studio License: SIL 1.1 (http://scripts.sil.org/OFL) Homepage: http://www.justbenicestudio.com/ ## Entypo Copyright (C) 2012 by Daniel Bruce Author: Daniel Bruce License: SIL 1.1 (http://scripts.sil.org/OFL) Homepage: http://www.entypo.com ## Iconic Copyright (C) 2012 by P.J. Onori Author: P.J. Onori License: SIL 1.1 (http://scripts.sil.org/OFL) Homepage: http://somerandomdude.com/work/iconic/ ricochet-1.1.4/icons/Ricochet.icns000066400000000000000000026163561300720305500170640ustar00rootroot00000000000000icns ic078XPNG  IHDR>asRGB pHYs  7IDATx} Gyfu̩Fucc ۀz! Ylkl p#n<@<6Hd]eg~_UuW# ^k,1EҗvH (z[+߲՗w[f_<BN 8|o+:;6][~Kz?n =Nh?+l὞zk gZyȵu!ן=[WjM(Y8>Aی^wF˿M<5%7^$j5* "#hIAomV4h(?avl<]|ml[?!]nJK/9}ko\=c[į9p#;oE. V?i W 5Z ' |,S;W9 yxaU6iC:Lmb Doueg_3o7F?qjBm `' gvt_bY6vgkwѧW4%Pc}b U]bMw.4WW=zÖ́! !,R%@(%"8IL +9Zk YpiWk[9X[}0㧳Uϗ+U׿7,k^za_QzPw :O2WV!!y"ʀR3_`gm[tX?VuŪ?T+5@CeǩKޡs܂o80\y`V-Z޵JP/Jnev}$']@paA˖yEY8KM`~}xybVn_R[YDgK6$8_DT!c60`BĽR|"&UlB llH]$Y ˱mS0sL(IXU"gDb^HUteȀJ7;\9< ڮ Iy}S1JHaP\}(E$2,׷D@_ W}r/<ʲA({F v>pʞB-/6nB%gJmCOebthY)U0b'f"8ˌ<mZ gAvp.|JiQ(fA}Jpelݾּ`j.M"Wր)R&!x B fGՉH"H?cڏCe;D٭|6vW*`Hrpo }_J0j~2qB 8<:&GFBq~9>YW!B(~료su%Ud%@Oi8R-5?NN*:nֱЕq^ <4ݲMmXLiid"N.HCMX<` N6,@$EFGd9PCGH" Wi~D6I 'UT('PYpR+@bAgKlֱp`B)Mí_ Ptd~O}@lԸW FH #[3SszB 5T,q@"+LI.@HXPTkNt*X@eA1S>"PtD]JQ iCW嬭$@2$IH$Tn 4 r7suEg~h}c1܇I#Om~۞g4}v" 5MŔ*2" BW&86FhWڪA"r(M&H&7p: ~.H'Ch1?~XȡV # -Òάx lg!VlŪu J@ O{k8g@8I$<. pPU9isK8>-Ur@1dsE~'.t)LY4:3;@h]:xϦ:݋c7W':**\\iFTdyƱ:6cǽbN$KJtW.C7܆>O6L^?ȌS,s%u :D]ſN_&Z9R%\3BC 6$\hFBKX\ Z3(0V<ԦtQ l^/쇮C- ~weĿMs;2 |RQeܴoTv磩 x`ObaWa $ .+mت\j)n1H8\A`(P0]N܌82@K\9Cݞaq)Atfpj43R=3)Mag@c&8Ԯ4 e}qysb7@D k@DW"AI!TBy@H* Iv8oњvDAGUQ8ȁ\$^>L[ S"E=L;|֡m"o0LD<ȝ`oLKt9av9~p 2pg,XE_88Hsv7ˮrőJPd FD6UJ9͋z.%PӴ9l"KB!pSlFD ]!rDh{F'BD vF1GDw\J9ox i@;R۳@\, NZ ]P; p=&ԇuԹ> 82Aq":` ]d~Fڳb)U|nUW܃;Fgj@Tԧ0 p 8YJ ( DA X'kc8G#bӌ 5.o `QG. |mhv4^Zb/ߴj>?>ޱiJeO[יHY[eڹ}C5T\Ye+{ C)y"+!o"MNj?-%tgAAkbxfnv9(פ1Bbk2whe :(ԤҚV!WxsARLypO,1y)Uh?,dͶdW/! Zyz6}GWF*ݕRi )!*l D./ƮA>Xr(TSˣ]-[{UUD@Mp,IWb4ZR|lFPӉUhzU`oQn=ˮesZ,VW+x \XDs Ek+[hAVnuj2lpcbX6iǼ3yBa |߶Ny (kdc6W-ˆrCQmm61uεyqSO@$-Q[~&|\@\@ΕLࡸ++DJe?ݣn F\ߩ \ xiv k*2^j VjՒYtҎE.@~ʁ}ֶs`A# ,ITm"&)>sڽwVm㍘EX/p·}D;ڡ@j U}Ή #J:GDzvLҿUu w7'ie\,d]"UK-dOh=\ j;[_>v@~"HgTQXKtN4YO,yjIB9\̈gJsO+A03m{`HBt3 L$T 7Ǡ$J `!+1XM"q+D.jkounã:GAӅ2j7`(u0#;afjb X b!ww*8v2|QnViT K ©&JOplflv'H3Pt.Mo:u%M5OGj424)UA (/_z?PBh1 0o NOLd:f!>r_?1Q~}{^>@}^ +U c z. }uO'ؒ}`"1hb2SO1p<+1:XIB@I} $>} e[ӕɬwPx-VJf ";>Bo 9hmg ͏C/̔./a4,TM 1V Z3 2$-~/Itu$HJSDFfknKΚۆ4[@Ow^з32n .AYR4ܠT @(A"@b(3-zʫ 6eRB.,$Y;Lk.!i mOy}gFx ԛ/1V6~z0&E7ZludЦ_4_OfJ訚 k"B.yqU`a:.m  \6m6[1&% O=c =&BT4J ɝJ~8@$ &3͵mj? m$o$ĕKta!YBչ0\S"1s+-9U]iZ ̔*I_AiGuGfZpoYˆP/ZRڏFl @i;Oy}%R%@*aJa1 <P#+Qo1iJӳS~((2b\&ؑŐ_`3!P\$ t,Ԕ*d #Bbaƅ=ŀ<>9AxqM[,#@]$QpHJבe~k2K5W krM@Bl.!8 r9I-Ҧ>!T &G\M: m݈^/ M=KEyIHxu@̛'YIr&%rQtMldHh E?,S|G/~ :$+)*ѽf&;޿in&coרߺ_7oM&R囔ѽTLj 0 4&`HBCk*㶨 7^&Vcpcm؁Z-V"k/L̅Ŵ+GM-,@EksO <~GFCR%@p(Àx%0`aJR1DcYif 4Y97fP?4n4.&H`S)ψ ԿL&}`K܍طbP+h\R!V(wj=Ym.Q H{يE <,~?5ўb#]`~|ٻW2>AA2,H@db ZL0#{z=sV>1V.c( /yFJ Mz$" )T" LEL[.edwǴZqqa_. ilN0Q_PhB^ju}-@jtdUP`1B;U @&vf4hAHۻsҿ9\G0 !^,OJ66U{7~q43KiY,fvڞY'J"^!ԉ ۋb2-AE=x;R%kA2ùiU>+=O#sP[-+1x\gЃ yad +M4ÉWO_zjIu`xȳ$Ab?$å]<b@s߾~Ҷm-I% 'NDDx)S/Ȳ:`3ǵ]jI&Ӹ ]O}_NO! X*|h2Y$ Ph c Zh].|a\XxX8|cx<_m|^DtJy5OXBš 7߲ni<ݜ-T9[>|ɺ0: !rk橳 |ptO+ 5 仅2VNfN$aUfd ":BY Η[HxD(4*NPOZbh?}S44N MW Sc#/*k_+mg~),vGeIRپDli_-M[m82}C?y x~t*3T{=3p2AcF{g4s]RDZS?بgQ]'tXIڭ5A?Du&;]lX%v+ųK;*_>]!ę"V?*.v~_ i0a7@Γ %0-`ife%G,:$n;KQX߅'Ik4kQ}{C0&.z%dWR}>mUݎA)cHcuM! :& h'=y]T>qT @ELdHlb1PkSCQqMb! d3tXw&>  Ԇcz*ĝݗ go_Sõ%ǠȬӪb8{v|⾟'0K -H2Dz~x}): 6K3&)|]"A3R~$t.x(%WΈoԿ~FBn/jL`SֶUt[H^P|Åg}uvJ & sij8'bHY~Tp6 We'T@ vp쳏ߛtc~C1NFgj{3}h>/Sx뮿>~J9 U2MS/ن<,NWxjDtTŌ B4 ȐUat:>XmxֺoVXXqd _L@&B=ak2b:` 4Onx|I@b5)@1(ǵ~]Ֆ2㽸 l=wB{WT @ dM=eTAha?F+%Y:( Pp] A&]t?ͬ:q+e  TRpS z@30aKlxI9H\#_C5-@sDP@]ꄠk}| @hABo[7Mz;*ok_ڏz\-d~ņR%@-4KW`z!x,F#ECCdj O:7Efp1Ԝ7b_G"^Fnn8jF:{eԝCS%_ Bw%ML=@˾]@D."6<ZlŴ0iZsVr' >akX+;M-'|e2ݺiaT6.$2`3Shr9EމbXB<"بLFk;ihq @ .Kb7)NcuK0g_@ lTeN]y:S az* "As@p y BaBSoO6"$`iX57I"yE0FYA7(:|`v сjW݁ciR݁} ~K5\@qm͎i åBP#0K r5<à D8BKpn;sONH/ S*n|ڛyIǓA=1[^%5^N 2(1Yr;N>^-h3`Q[𗴾QԯAѳŨ8D}OI"y˙ՙ_-OS,Uga{1/dЅ}#L_w Yqo<b:gŠhjh!iPg똡2(h?)0,I>POE B*ӦKP8 [kc0GF<Ӄգ\śYW[ G_ ./ Gf2Đ ` | \ >_>4\}z@e`eX^!b4نKYL c>4*Sa4݋"ruhi4d H'(`/͘Y+~="> Io9hy;Fƫ;e 0)1C#ֽ1n*'vN:_1X'[M~lHCA]|6%|Rd?ay] .E|DxrDմ;&|h{tdT .cz^&Zߌu.Uv 6TD L"GZۛ t/1@ph|ct& :ͪ49*{.zXm{wu;nX+#Uᑩ~tIa,:d4(s؀!R/P ]I;tr&1E̡*Yi ,_##U5X=2W09rP} b!\sɗ:.C(:>-7V,x1߄VC^GwudtI%"xZ9Z DE,no,m_ v%Й2ڕ_ӽ1?:S/54xI躁)UYAi9Yf h$LmW//|p$m{<|,fѧnxwA0pEc:=܉WG.)󏗵 h@x?ef&._"DGĹebsbN(펬t Qt~_e 6KG"o_"}Yk+6>ەߊbbi>)MڷS+ cs7iꥫ-X+weW!sG ϊF9{+]%CdPB_gIqj3ϼ/w>|] ZT2ka  LjKbkf6w Ѓ-h GyH 6(Kh?G\|m8ݾRѡ-Nਸ-8&n ʓP"H_/|vzwo;oȭ~bR|¾,\a%ӝYn,۝>& t6 YR\D'3d<$0P/`%NL-~,nemhu+xp}Bf3Q@@ |<"d} O$~!գ>Y5yٜ9w^"T֞5X7WPWU'n;#mZ5hZ)U>j&:5{CL$*IgIlxi`cp\9' g.,O1@`m?Të<<ʕWvf׭YPB;kvbP9YU|tHm7 F; 47M?MU(^ڄN/btCڏ2O`G$%|:T_E`8ng+1ne9!z>'AB,ГN}A . ugEuf.w -6\yuՂU^)N"=2\k a"'+&Gh& PˍB2^h$d(80G @pߍQc6+ox]-=c|9=0TtT_SOu:nz[WI cUЖ/.V [XE5!)h+ڙ o5M" _)xå_tڍB>^*0L< 0"bKb1E.x.eW.6P݌@Ept.Wd rR%\ QO,WMS/T=-dߴX0NГ$=r9y"'> fc%@;r:_~u~(L?oj>%<R%@SSϿ_@~qoJ&&2NDx@n"^MJ$9"2ޯLJs3&KVPK '>g|ɢT S#)ABR'Yop !JZAֿ ,?޸/_hVj/SV*+r>Hr|4 %9!ڮn@ 4JJ4zBvбEwS^cG8!.(D#/_tŏw&#@fs(/j?X$-.~8c#~jФ;UGä@XP k Ԏ7(6 `)8~̶2:7ZJE 5g-!'j ׆0a +2u`Sva^wqL Ǿ-J CBlB(4P6 ()H+'5_ 0WJCvze%" -'Xp|/T#}"i8WA^{YT j{!HR 6LPj%njc7z;G޲B8L]hoLh&C :H@OEP & AtDS%x N| /M;luޢz !J!,ld1J 5\Go>rU&eeQTJ֗;~@GKM'Սt +so0n\~܍HmCůUM! )^@~u|0oyɠd."W4ԣy 6cx|:K+ t^*m'叫Y%@-%s7vKsOص<& /_  j?!@GWHd b|g5q7D A4Ռ[!~m"^Xj?$@,@yOEҵ%@^}?%l\ X6+#ˠ.53V `M7 LS]3cC 'ci v,,j39*C\ϟ[w5\iYF1 Xey?:I$":l,qbȃ-4`dÿ5v6\EuRcTXYΫc fň yE$1Џ}ޕ ~#`'!P 6z'#A>t?()$44"bxiYKx|"1/ ! 2r pBCN Ie5Ўx2٨Ge\e[ʤ|;P/Mr BbI HrT#4^jy@4ID 4H 2`19̯@jA{ JX7Ⳇ12(2(8ZW易/bԝR3 Zz,"+Pi1B@yPjmʝI?/}rpǯW^ @pJfiZ:wXbE5Jp,6O6`r'aeb/`7>sۃpRB#O[wCb'G˟_{q rZn zzdA%Z8aSmvJ;?#O[?v+?|W' ՞OK Z9O8)|k 50?!=~{ 4#c&)7[͉{Tz(._[91Wc|v%38.cS;.+Zx9Z86_7+ؑs`?uCx\*Mĥxc:x9:[ųe7n,o'^6`ЊO>xMGE.}'FM+G, [Yb8DUßﺓ[ _#-6=+)~ {DT/R5=&E5q_w_8|) Pn4"(e &q~Orb5/}͖ MxZQ6K-Mw]9s).ÇJp?H/Ù6-iyJtgCpitxx[=W^~zg`mQO9xMblMXosf.,%GD8vñZD:`AAJt@KI;#xQi:m[Oۻ..}}k ~ ;Zas0VYw:9 B`9ܥIߚywO޳ι<8XDowmɴv~=1x?|gw>)ſGݛ $aIn3r:ݹK%ٹKϼ6يM|S/[l T{m9mJt-vvٮDlba'p0]MܽnU>[үCqI6Ò:E7e @r|hgO=up=}a9~yOٴ~ ߒs6-.ەgu6ag""w)Zr71.| q7܃pQGp+qX:=KtuD7etzbnחZ2t)cSEo߳feC3"߷T9mKݵ+_RgbhV*JĄ^:O ޺Gޝrh;s&w^`XRL?Ben֫19lD>g{vo0_ fh5vЦ%enW~,ӵ'{8'JDWzS^@X4ϡ.q2#CKq%@^RgG]Bw%t:9ݍ{'gwU. z,͚M̍[nGΠώd۵Թt̾P7[Mսt(txy0~:t@7 8^@ в`xer톒-|w1 `]9mZ+3ډֶ:~2M]*2÷yoqЁ.4Uof;oS&Qv0ufXeg"t|td|qrl;%sڇ1[VұKw 53`]Wڽcm# .; soDٌ1z[l\`U/p}ƼU-ױ, GeWk=z|~+f$qv׍DG/6G7w`O~ 0lQ60>7)xy6"?o1sX Gp9ag@Xe 6ՈZ7Iyǚw5";m:2o|<EwF(,3SR(xP txyF~5Q(@wU8QllMr"վxr3vIm+o=f6,ű \|ft'оS|(N s`T_a[8Y5t @OvU)ӊ_ì `Q\/X""Ww\5dQ3R/ F_kmr "4Pc[6@A\౷x?̩^.*Ax6"l,!4!|:lekEWS4ӌ7\rwΌmj1sk9o\l.?>L=` Ao_o8زW_6VVo5V32W "%!5vrqmbr+%-FB`:eܯBsFȫeC#)$^6[i xPۼ8BBwwGz #d;̎te^YlE3Yӌb6`¤gh+vjo|ǵf ٦Wfe2":sye^: F_#0tv2!?tTV*髚[J( -<}xy_hUm_Qi'>szӵtTWWm5{|dYQ.(>sNz 8Խ wg|LuAO N>8*~lrAOGێ/4ンBl~W x6M7S'Ŵ~ǚ5Z2ྤ4}"Bd ~kwd^[blƣ_Yi<Ô `Ҁm+ǒ(l x[ HrU5W vR2G1,x+\#~aO֣a:uk:YkOsuvEM}z "|zaXɠY?T1#](<{/gC(_Nt;id?ԕ8^N!E>z&CNOj/ S n'7<=|?݁g%YWX`4>O2W3; 3=0psf/lE-z>Φs0]o0 ' y8Ձ(i )y_=.ѹ-[e6#tI`Itk}#J'm38xjذc񛁭+/_(Nb\b'7+xO g'IV$Lp^tw,jt|BT$fu8~QQJHdPa7}[4 D!~?e]s VAm//C 6oԀJ|f+:l*~mg'T]v5}~9yYӠ~с ^KVn-P $m:[HǑMfic z3rI59 3%g[$`sv@;^_ǖ:}VTCPXrS{>x3Sߪcx>X MLJ6u`ycu ?/z[ 6, ( `!3o^aYjU!V!`NnE- "`.:*8f3":o3 m˰z'}fyI:611#،&k 3ע ()\s||y#t@3m<̳ ؅t-ǧ "Oǒeԝ nj,GL Yg * f@mr2aZGge͂wNk:}=,O`@HI:TzHPRyR1k4ȃxB=k>nқxN(Nq;A;[l>a}tiwd|;uNp`*#J XР L !삌|-U}7=TSq9/-LT@_b 4ԅJfhi<ِ859W_cιV~=0b3_֞ۚl}tݵ j3;Y[ۄwCDITks9 6X%~&TgpzJ ZhgAi[ ۩}`p KKW7;d|K;W<̋^'>xv'pԵ :nƒ2ߖ I9 &2W/j2ԡ* J͒If6rzxN6A,Gv҆- T}K@vH?QHӝ Xl,u'AhT>\^,}a]^kxK[΂bF>tdJ uf:H;tm;"2.%$,}tmC~ۨ'NJi.R럫 ucbznƖ3& A<3 />`~|GK[9$n3)ƅ y: hLH]vg+CqP0kqv\:g֥[y͵÷a%6@ԫG[ `OUbʵ$w< R`-:< "ѵ0~% ,\M9)i(|\G 5tJZ[ӡ'0tJtvyLiכn 8`_w:xL'.miɧ39JXKPh;|Yv&:Hꥮ,P0}f#;5uR]~'U7h|'UQfuNt٭B'|XޢPG s`7)UD& WfV )yy nO3K~fD R^uX]l^^ Xy3m^TOcB<ȷ/WUzT.NL3%`kkK {Э~:.`5kD:2Im|& j3&m#x v]TjPPKKY[^iV/(az[ 7Ƞ#Y`IoQPl[yW~!:~ ls>}oms~A]۟l?}0mڟ2uGMGjC(_Y{$Rw(xtGfHB/m]Yu2Ը\Azl'.b&w˵:HY6P*zSH}m6̻>?%+:vX6/0ں@a@#1:{}9;L$@B )qNh:]f;:Dq\=R9{.KAM,֕ j(m^6O~U>ȣO,_(R@r.:*y,,PِC*NFJ^MJ ].sцNqJt42|2S }pq׹vj?]Q@,1θ~B$ryA`?mg`ˀWmڼ0~+ɹ9_  m~} ,zH@jLʹ$y_vKo>%A۸6}W&Mff&N||ï`|mT>Cl =.nIbGg}m ptg3Ke' ">7 g;#Y \,s5]3?^ۂt?V+x(*ri_}+R+l& NA E/-v.)pe@ȗl"@CgXξLɀGlDdLJW~0 AtLl ]v4L7 mc  eFJ~ʇuپV z@߾pyuQ_N?kBߧTDӕ/nѣdy*0#zrj֮ f1g'kcx׬ßA\#;pլ '"<)ZQ'38ЈwNRa ::i- A:| AM@ W'vm$s(}돶a!i͋ZT)8t[K~>p@KJ߂^W5i\ n 5_Gg+cari(}x j:6oQ-^ Km IՑLH%΋kĩm P-Clr3ʩPouQKѤ34SԮA}?tCη=lyt]V/sG #ܣ yt'y$%soO_1KEha}&(\*psN)I h: :#,W-  @z! GRA.P(>/Y ƲZBAWJ9Bb}ƳMkCL/O 2l\)=Lg{r+z9n.xD?+Y@RZ9tdE/v`4Y'' p-0fi 6j_]l"yaK%2n> >A:S*.9<M@гiJ;}^Z8//3 hsҼhu~ZSZ0 L>uF:?~P+$H[]?{nPkg}q}w/O N>u ĠVn<4olZcNQY E[ #04Bw32S R(|)Q~XfQྟ*m$o/@%G_} o_@V0|rzF^Ğ-P`q}/Eo,*&8J}x$T L(΢@IQi^(b L@xdHg,(U@!Ll=>Q&OW Aȏ#L~B.Dwgw. 0"?A\(?[Q6BAϧ ʻXe/ }Zߥ&Ǽ8q@&N</or:E2#3> 를 A9l<TٶI}Iozn8#}/H] X xJKʳ1"\"*ץ֠A6(XXW/um_n_?9xzlsL= gݜ װ[( UWo! p.aw:Y .ЗC ~&6y{In閦+o'z`|\Xm3WgP%AኹY6Yk|d&5uN@!o@b2`jBȴ"lKxdۧm#O-E\`y hdʺ/8TAWڃ|zȣvh?%ahvi͙9lZ)iAg7ݽyp=dbt'O&_NZ{+,Ex"_ a# Q<`ZU*iX D<.p"_jxnq Y>>fJ<~:ן~ôWjDiVa hUj׉}p2\~{~kPy.O.va{ f#J%}b^{~{%SW\)}EG%7Qa/\ sSGn xSιn vuy:K盯?svS\f3֊)j4<*x0 A:!E|d.3AoUoM^1x(*p[vo98e:~KVP ]pvDr>O騼sF!|p1zg.6ߡɫ:v6c !bLSZ>Ϙ,4T;fLj;S/u:q2o0WffVpFeXn~ߘZ788whwۀDiN5M fT:@  7 "甡-Rtg(vz.!Us6`DҟyTP$L8FοPyr =n\Mxv?m7[Xc~ǟi݅Yvtt✸P'w@q: ޙ [;@MCݺiHg(jnډO/VOͶvqpK,̛<:A@ OOnZP/tD=Cs|JW MHfަ䵖$WT,-&u& 7/'3x1:ܹ߱o.1 ;xwbupu(*~řpΜv]!98:t\oN9k޵kOI\l}[>ToJ6UKw/梞 pk2&\1fql6u>mPOhDbOt_ƺdg^PO{ fc ~k&1p|}[7!^tHo֢7>ɳ_Z@*V9Eiu櫱h!qZ{MH{:f1\$Ոn_b?OaryVV|!F:P;)^dȃM@`FKt{[OSnJAۖ~#_whh&_>Cl4UO7Y) N20 (ۗjK 0@̚Gl^=fna^mufྥe7*#*V 8N`@g7z :w nY4~xm|lV6>'-my:om6Z~qAIwzTNL(wAЙ:tU`@=lPtP\42PyUnM# ߴ܏|tLO6[}ps$>xf% _2H08zn5ԌҡԤ2v/`0)jw, ?nrz<gahoZ*fN;u6K🎪D U5}@5ׯ7;w$E]-{?+SͲ%a`G[75xשf|^;Zo%~%e`U6:/LQ- \B+,3u #t vͻ !uBʥfvS;X&A#A|`Bm}r,ޭk{^ua7!A}@+n>l.4ԗ$}FߥD^:&+aۨNz*fz|3~ x lȧ 8Lg'{>-^3a\]]xO'TSpwN9H9m 7兀 4yե@ tL)K墦>_;vc=VtyZyKk-#xoZP|yC"pH:R^Ա|+ہc T&- ^d6xTGOv\^WG8v6+4`Jf:V&eȨr2;yu 潓ՁBQiZtm7$<ۍjvtZ+ UX/m|ba^ ^-.R/Vq*vhs|Qpj2t$0t tA t2RZuѫհ *ح a@d72頪gqN'N>7jq_ѶgNdFOAѧDkYZSa@kl\ķN:y+6+vy$pNHlRÙcuSt1bvcG}`)V_z=DW`g6.Q[R$Tܣ*џra9u) F+AKtuңR/}距Y=ҶK V{yz^@UPy߸1nӥ 96d!)x},2> n@U`P8L~ 7; ?>ҙ.~EY9#1j4;0Vz;9:m; 챳iv2ꂏL _p |σM+em ,:_N>xh^_ͩ pmB)IWj>s]s~A*ؒڳAf3xپTFN&՚ Y 3RΙ?S;J  [|'vq!hIvȷU-8lŖ<(Ȩ$~[G`+1WWnȺyԑ$W"a> K2^6Vs*lpI`\(L0tN/N:ɻꦾ\_ԶlW8vF-]~@IDAT:ڍe0Vi,; jڕd/PZ8u: P@HܭEVs*xC Tm`@sβ4 Ꜭ+)KGA޵+M Noɴjo6IჺyeQq</[83<̋ : 4(#Z'3;>pp\ ss@ve k[m2vߍAW@g[/HV bHy*r[|8NZbݫ<;46 ̓wy=Y3Hʡ:@`΀vҫʼ?8M^:jM솼ﯵwb| ?~G]nInB~O p AL`*y)k Mڃ9 ;Dy 0`:Ty:,9zސ̨SqA~;x }O}8p m!; Tpb7,CՕo"+T=Bc,.~WYPOEGvRge͵j!6g)58'e=x8;w7CV|#>ڝ1 ` $遍 K`IX]/ (dCz%e[|DR}V/EΖyffVq*nQe@p $OۑAL_`:ڃk ڨu1{ˊoT&.ׁnHjs|nlRvh L#yݘvrW!X:9'ySᳺ_`Nt+4ƣDsxttN$Sy /m< X*PbSf}pQ_aԼ :ڍv0 wP= }@*@m[dmY1%tf+է:yz>\*5.+-a|_f% q19fo~|曆}Lϟ Tn,VC*84>6HBsn,Uѵ[2A^ "qf(yIY8eۃD5㳰|&? SqbBY|վG7Qg0f\ӉY9 JVu>l]v|IDr-ˬjPz&:}vdV/֑ (/P:v;N:Y1#.U%4O՜ |?=>mn360d2Ht *#(( ei.o~wq:;gt 5pLQtpa,wLJHY?QXQg|!]@z\[`myNmvgP}JK5PI&U M=g_m|1lsl3my;Z4֢  :0 \P<+cz }23uGm𭒙_{YsKy~{v4x+=5,uK8C`-t|J!6rjfgxD촟2N;^ʰ |E96gRi5[޷?xcɩ}SʉZkz{83(7q ~9Jc*}̃WZ 5NAf@Z/n%lT<?6U&YG燪۰~ќJ?c@CAr솦l%ߜ<]X] f;B)X[FmI. ԇ2*(p3#pR\1Tٻshh^ӍSON6k1<M y yˀ`1 q/ nPvI,`T{Ba罾[EókA|g136{-6 xvhyfp']fvdrj}ElZݔU D% `_L-gKo˻`򹁝>" ן.Wu*~_f 8LcZ|nF/<|ӵɗ#gѵp Hq05=񟫂UB U:wmpmTs[cxNV_2GGau" cc5i2́M湾xe][mK -g_I/~ 9맭98t'<U^x-O2hAחB*lHR4Gp@$ilr4tӚ߽FA |Sgo=fߊWw[8Aw ͺ)-̪`W J1ew/2=q"60qH06X}bv 8xa~YGln`ߏ!7<|r]t\7w|(yo׷üPVyn+'0rԃWAD;C}bs^#S l/E;1Ik9>E>xNϟw_| }6IAsfA-A2=qhx)m^۪|d۰53^{Ɔfšdlr m 0pB<0r_"=>At c53v cg+;y vѿ0;RY_)#bF=˵%D΍xB@e9@)NO}}_.8NvvT#[bc:7vsyӕ迫'vȳc!ü."k)x~ >H]zv.dh{ҩ#flyܴLTmt̞צ:Y;82si?`AY5Ks"φO:1鵃Cp?2g2qCPDAC&6FO-7-Vb#|mdIQXg^rVXsojwKǧ+CL/:P? K5zA:VRv;p8աux1qSOzbƖz3cqB[MSi̘,.ASy~šg@~{|3;gWG:`.%]srV&17t|ͫMhg|혮 P:fr d@@+34*zw29X;~Yh>qgYtX꜊yuP T,8U~ARC}6 UՎ-6h[ytc\&"nLmԪNrft7?k^R$ @dty:0Z^?eQlۖcƖ 8t/Hlvޖ`_@;q/N"~?#̓f 8 T'is*nN頋` J!q88gǾXzJRy<$wTg.dzb#n׉nzhnf=jz?a 䶳WVRYSelXfgQj+7^& l4'UFheKՕؘ~gY_2~yx`C|n\V@61Jh~l_ަy(NW4Lcm;q| h+Zhg-޿S3OH=|u49-O XtIgG&4W"汵י׆U:}(a)Hڞ<2S;~v,R_z ϶P֭O-hVJ\;ab^&=bBD֟GWȉ˟[rc2 Mu:N^ؚ̗NQ1Pli,y?.ҙ׀;&`$gw¥?%v~X<:E9ϟOS@0 E'@NA_ʱ*IGRm{FDy~5ܯd_U"!ڳ<' З;]ۈ~ 4礍o_UuRxidxAd ]p\~A]:mC^R 6, r* 0V@Atz7:јWU 6{uY | Q+)/ךA^[:/%fANC`ȇkIKqg93P̖AcךيnҶQ ,9_ l7;l,6-7A?WGaSd "F .{YP'w*Q.!)^_|}}{N!s8>xN;zPL9(J3 gG~fR<$^3J"|^+kudJWl{9^iQE^Ϫ ~?9Qi[6-o nw"%jcdvS`>@4p@l~ze/-g# 3.fj7#@ ^Ny]Mq 67Ll>r/ը 3+4ά,A9kdX Cк\?mʕ\> z@Ek" kKz8泓ׁnXG g;o&0O{]Yy{  "N^XEBÇ8x//|5. YVOoЁ×rDY Ru%,&^:tIW:DTXv똡i~ '[/ | {m Qg``^6/eR w֙T^z}G<9\t,WƠ.tDlYUDD&tv+W^9Q*rG (I zIS߰~,f_MV.绱_a>~ ?^q 6Lfj:gG`J gA-V⌟(&:d\$s%Qn-= TJŬ<9 蚵/>Y2o|Sv`EY_O1OW7_ܿs{-P`-;Ss/=ZfSGBΚk $2Z:i,@g<\IK-/^=/4sOϢʂ?Ö s}#9r~ZzҴ@q[]35 ٛ/ \_ه{x֌a,]c^o>m} Iܮ$"‡. fE{bX_oJשr2W(zKCA u~o_Aa* Ђ?G:;? f񳂥Mh*QZPۆI4 ڱ\KHKiȑsu+6"3/koCcatZh0 $:fJb6[浳f~qʬkM=f֩h,ȡ;U%2VWZyqՇ+/OUYus-O>bH$ 5:.U ~{Klua2W^dKCn>Y43̴鯟5+fSX3ʾj@ UF3O!6Sy2?"=yN_~N dbW5i:2=?rVJ  g@J Y!k3sQSZ?:W3Κhh&ξ|u ,Y=\,rr/?kr+6h`Oѥiʧ4;*9[I^;?A),n"f\^VŔ҄z,> ~j\5 Zy̤;kFq۰oڌ(Gj @?B8><|65 R/fM˻s,T 6P`Nry=~* ۃFD#8GkkG;W" B'Z Wl_5+]!f5'N ?nL#yz Lퟴui6?f)8ʂW]E/ M@퀌?pޕ}\d!& O& 2ӆ`wV`K<-@UЗ 8|jQ2֍#Nq9|/L%6 us1yت϶M<Ϝ /KWeOe_w->b7u:p6$HGp|$1ʑ剌Nz*=u׼%i/km(Hʸ@u ->~**ظ|33}ѣO=hy:D3LW*#V%{E@1g|v@< vQ䪫y`E%~M`/,rZqN^#, #<_س 7^{`3` LjOu[}\)V2!r߆Q2~{a>~gZz9ߎ]^:_ 882a&zƅ>pv-וALvro ; _N`z^3w|XGH d?q{;K%]ىm =R9ެUy;S -PV&MOtHBNW/bw  VrB-t t;xt5 CϬ+jVsO?PFP ߷{ -_8/2JªC\WM?te"n~W!]'$GLMx%)a9ѧQѯWuQ;X#8GϜU7\R+G=I]μ3x_΀,F(B^G4W&tG;mǷck[*`z;r/h=td'O~߭j6$@?$GvV@+/~Ar `)w^ ) p7tW2;lANyVe#+ /4+0'"sd2h~a7sW =kس+JP߿=Z &`|vpCycglPl3{Zǀ/ae= > Ct_`co4o|aցZIg)ϣQOn~zoWb0Njj׮7?79 ~Q+\ t|ڃ#KN З/HP|.(`Zkg]ȲRgvZ- ecu]4 i/tbE{Hfapv3/аJ)YBrnCtY¼kedWCS*k4Wh&2GNOϛͯ. 1"Mg?id/w.g&0Imx]fFYWan Ky^$h/q_PEY:3+i+}GG@t%t~wbYW'<x|ՏrvoujH G g܊Itno A4  dz <`t~+OGzAz`P_PY [fzOp*[NgT u!?nL"#8;X`044p~lVn `)c`yǀ;l 7DkPLVhԐ6;y .UtBSgx/ t/r`,\mcdg\otƞ8zcύ֎MwQbԠOd |-iU aA9$q h4|AKA-jDN5_ Zڜ U UjD /J=3R;45uVKOi\㳘^ WS3@/-Θ_'72Ëp9Eٿ}tzOF&o;іUL'nbQ` Aa`(~{0u-)f+%?Jׅ\z i}Աs}1yti&3Su`xЧ;iإB&夿c |#ƒ;?Yp6. #>:cDr`ˬm8,hX,:*o^7q8O7dz#Ṷo b@(ȬliS&+8W.r.4,>P ϏC<噜@6Q`p^:Y nD\xYzخOoh&M+&窀xh#!]ī d/x9D A篦u,Ldּp^&xO.P$rH&+:M|7<2`b`O"<Y=tU ۆԶC,Xj{\ Ԇ~i>}աX1hõW9ylz)_`JR Sςg ^&ΏA[6]-QU~0$ :iĽ| 6' gJؾ 3<"t&Kxj < l8gljsв~鸐afhGYi@Xy\ÞpfXd&/A[_@{f*g>Tj y?>` Q]Hs3upPHYH+ +6iW:|{;.< q)_6MS%VLLg^ۧ< b3'?.;k~ gp*:Æ,M{lv@YxPtݿ6Os9(pۀ`預$-H ZS@{j 4*㟂G??a翀ו½?ԢD9g&UuNu/Jꌮas碴:=68Ӑ|2ds.4379.G/ZS:5oX!mCA |#b?_3t ZKM\TH7nwgjQlyi k&&vnߝ]t±cՓAn 4te\ڂpR 8S)eJ 87|ÿ|W\uUg &ysӉҴ^f@? M>v_sغ3r?u|vm.] I)Y)?t;ѩCv~ze/XV 3n/*{G#{ײ % B3;^| 1.;ED^Z*gCZ+[TKr@}CSxe,^43uNb?g/>^P&fwV ,4 uu aD3B8Owk3ΜN˖ZSoy~UsO7%6K?/ze\Ǫ᧎L'&|ZүheK~_$ڈ}*ċ]D'BF.+LsR!3;pIt h_VrS|"ۻlߺ}k']5CΞlwvkls p hu@ y1-egx0\,wIuba.SMx䡧Nl_mT{ƊUǬ_&aaXV3Xk<85v$Y3Fzqr%r̩98&KY(gNʈ<@C99Y P?ݹ0{qAl>s`w|lOvclj|ޥ{l$Hu GB[£14F3HI8W@KH6OjSC{Wuk/\,~wij2]> 7 +֗lE71Yƫ[/dqNfm!TN??(Y"1dYnYrMLnjFdY!w@s]7'kxScӫLwDvӻ> ggM肑`/)W:!ѩ N:A۠DtCKm@1|j9(vH5r:u_0Taq)[. NӡxDlp-<\ -|@$6l]}uol! 3(]< ROprﱛSjAqF*URp+Y&=4:Xy㺁o^_&wh=hǎM7ñV2)0^XމvwyE׳qv}!Xnfzyxpx# m ._Q l XZmm,mÑ2)^Sp@b<׬@gMg?oHÝƿusEX+p~39d5mxq)_kоypԿ~A1DNW\_PڝC?>zA3''ZPͩՑQYi2+}Uun k0?sc%t 0I\pT@ӧA/#5o.c7+z 9[y ȱk<N#< Za?N g󢝩 Ϊ񗶨.\XI܁zoHaHa*P C(Gڝ0:nPjLtrL*J?:rA9.rqҜ M6^A:8&Kppjl%x¸̎8ĩ3zXˢ|§_m$Y^Qа+ "tKbʢ|$с@rF`%pT.~3C2r`Ҷy9ř2DY;1 { 22 5UeVЄy'7Uh!ǣMzq]'2KKwqm*93 ܑ٘APԕJrg`CYT Kݍi x/u%9< NuP1im5sHC34|-:~Sp+_&nh@8/yӃrxc6e9/qÜa`(55^N}y1šjmʔ)]~a9i-!9NgbO`t@`]5#=Ӈ[9e BM}PR]v{TMO}6=s]y=a6Ĝ f2; e$0Y &KRmg8UTjefxuj~ۅ4Ȣs\^|⭅NZI3+DcU][[ @=i fOzy3\5.q X;3Yp~d~L%gg[%6ĢNkWXq&W)~riP(R n?(ɵL A$1[ ,pb^5Nk`Fr(ό t/'N4@he"r_̂D؍ܽ0>zcǣc!bt\>acxL>t$r8lVm:6" NxM.ᚥ(JA0w/2ECGEYJFHe:ng.V!&W,r @>̯+ Q9=д=@o(L,ʀ~s;/LP+#A. 4 ,Z=lZd0s\u%;;;,U517lp03 8^ J `h@q>uv3`k e:!uVC_)GQyv}u[ +ubv Rئ\d{EKc9]/긨.WX.2b;tvPU +2eD#XT0M@h5p3:)*t=:/@FY^51Bɧ1ADNvZk6k!koyRSq0P%zfN WA:kӗP3A seq1nC'3;xeLTL /$>mDS^Sm::aѩa4_@s2 c8Q*{4*_E]xlBlh,Iw:f_#Nu e8UwC9n|Q>H-R(YEݝLEA:a"4F3K~iRCUt463кD5wTI\bݑk㝀"T5P&gh paau@CJŞ$jP׆>[JE p z@[Lgf0uI`y{~QIVch_UT],;kKBRԅxfecYsy`#ft(QMaERv%ҳ@_e(M*R'`_0;@aT.w,IlxEA8pssʡ11ʚ㭅] _֑wrpm?h'_Un d7x Igr>`uؘ'<pO1ro&0d ne$O)B J(HOLS2^ mW<~_n+TOd+m e ȊmtgX?l0 )8!$:I_TA0pmk1|,}1B?Ǚd> Kb! ?dG[i+ĨLf)6GYG?p#^FQUI PGY ʬԧbY'oˊ d2}ʙ \ "P=ГRjj<M=Cfo@Ua tVP=p0pH"=cWf~=#_mX|ɱx"5՘K`Ey>/+6 )uVGUpXGx R),Rde LP!PTPPT##k~s?!mf.=L ^.E, JFT z=_[zIk0$Pނ\37 Ht nUu>IO^o%_*K8<6~ kwԲrHC&Rp@9eR96 Vy E,-P#\/$B-@$3qpQZ`I@7g,['+AsP:ܡ2h,Q*Ds&pM3_eE&O /jΪP{FNvdw@sSQ`WWf)4H .F>Kׁ:j`c # d:~l_`k$u$i@e^z OP !r5`h}Ukr)$1Cw23zteӃrx]ҧȲ0H[ o! L<mXrspzFRN >/GvgΑ`G20܀&CN3gOdՎew~/.PX! Ϋҩ([ui($xzRg H vs\J`q#Α5Ca0؆|`0"3IokN%@ӛѡd>@W攐Cb`(b2{P?8/|,9;qV&EgWru&HYip9*uK4Fdƍ;ټ['Gp=y+ 64afY|3:w_iTS ZU/3V (Ԝ8qg?țÖ.@ ABʧ [:pFϯԎ/QйK;w!Êk]rPsF:C%zЕW1T=_PWc;u6l^A&tI^$٧ I@cqO ,ƞqW ɟi_D>x&eg b\]Z5HWW9I GLt̄}&͔ M\=6ւx⌯mTYq3yڗ>"Yr 6w|$?뚖@]GF[9],::q 3; "0YdPPԩg <2̉ENMvy}?>}Zo 73> :c N 8NٕY >: 9$.yپ[3H';^YO.0-n#@C p@'UԤjW?9zg=:b+B:,lY9;}>a|+M8K,ZJi9,Z1$3tGZX:pc L? .AC鳧A18_%xZKp^&v%|-3'ttf5hK 8:qR8JK; #3GCgjC+50`IlI: *'ғ-WK=->Cg֭$=zmf/V7$/ַIu$N*:9:5_/c}LlGWgr}),plIϳ3x5Ce}[% x [݄4fjW>]'.b}y|8gr`Gyg7:&Acv%l9-k`O݆@nm6,a}WEUe̩A#]INd5 e2[`Y po>+r8+y-6ޡڗ%WI׀ޮcY>D|S)K$ tBn% X  8ٜgsV ӱ[>x:U+foN\W;Y@saP(} ǃ=gZg׷_ >|Khn"_lQʁS_xc#/ ?#.?Y_-3eܼ9/knTx ^Kp /e 7qrǞ|>u|C'_G,+u5P[ lCk۝cm\qv8UB>8+5@ Gknu5t4po9e*#7_c6pz;qoȣ?킙~EA [ѵA7aps%tS@ΏP u =fO.O\鱝OϚWA҆qIENDB`ic094PNG  IHDRxsRGB pHYs  @IDATx eUx_%Ukɲ_? -_Lw tFztFgN#I N McpC$=Bh:$#Y,ֿ{sε{}^I[笽\s9{w߭vt ,YأYf]QA_s%iUUUU EPأ߆߁8)|{f|ڱUUUUB@\W?mX)}Zu6qOtGn]ǹVVVV mp\ 1}ѽX??W0G8<֧ gY> 3!靂{mOZ@@@KPx ~|Ǐ~3a~=W{[Ud!+{-]ej \hmo?󉣯*Xk.Zqy>RZa砸7 [n;ll@Oݍ2U,?-/@WL9!8ovZZZZ^xfx' `_qO>\G+ݼvqnFVVVV*6+RވG1?oqA`7 )!(Fqv*VVVVhŹM?wѝX0cp xozo¿?s)^6VVVRx!ջ|),ȯcsoKx*6|?r \hGâ.P6qw#}s!>S#I/K; \hK2Gy}eqX,;ŸFy#]+[khh$+6e_;,6.Z ~Z@gndž>T?\}} VVVKmp)\b GBh₮EVv_l~Яƿ~w ~=aZkhh+6U?yaq1o"n+y^#T7F\n >Swޠbo?G97wھb-V Ha.&?yWb!lwf\I,_{rV"E]w5#bSpE~cމqx3^&F[71( \ h*҃Gw1 = qHZ|e>n yl**ԧСi!M̲_0>H쇐_Fc1.s=_{k&FVmp]H>uOcf L7S~}~Ģ#-,a̺gT/!~K6;F>?^Ρ8$9pm,g~-Ch2B>)i {urmb2"^ `d2y}{ǦtxǏDԿ8{jRةQ8S0ks3M(Fk/e६6̥? xq -qA1cQ ]eP/3Adg.ҷqo'~Q5#jE|pH!澉M^vjx*6/V!/0Y$\#&4r,@F}McɅ4 >\X8]睄__2oE"%67g'z_//+%?Ő{my,c 5SgW$;> ?M?71ZڱUTmKY_?{豽;,ߏ9 &Ssbrɧw\HN|~wo7g7dx0ۗ;y>XAs?̠o S^a}ّzZ@Umέn+=JCvE[tvGgކE41:)j؟OՖ%_r#O`C_̍G~I|E{ZU%\g5x{q>_@x3nM/~{(M,K9]W Gqoy{{_tȁ*bTm^*y]~7@2{\⏛%u~ ̗wKٜ,sD{NGd?g M? [^ yQ_Iȅ>'<]mz7&L_q4s}Szk|ɿ/RyyьzGz'tCc6O3^ P7\۽sd[f|,Cyr';Y]mz7&L_q4s}Szk|ɿ/RyF#a:Oҩ|_\ggo+GYV} zW xӏAeNr_[{`#y#oc}`s|K4*pTmJ`s1<7qFh[O҅$-͓MUcɅUVqWy%ʾdKIb| _4~a{cy';z| r᎒}k;o=NkyA}Iy,c,͹xR)7w}෰{0[:W Bcy|&|pvI݂~[hj%@Ï%}74î\ض># ۋaS\ls`VqV`8*y/'= n*;k9zϞe}vkW]tiCg^P_b*6P|?'zs=:KYpqe>\?"0xt=(U9߲|MH =;y_ۘ̾J_2a|+ދOܷk 'S w{aT3:; lػs4? @נq[?vdvw_>})}Mowr?|i:~v\wPlHO%=xX߸I=Qlݳcon /qkr=vjx{-iNh_v;Mq9Vsq,}r:×Cuxo!X6mBh\|833:Ϻ[1[ָC@ۻ'w="Ǘ^_m. H?vp a)߇)x~XVnY!} *plcz=~3ͷ?F@lbu߉;OZ>o&'צ;;t_ymlVVUXx'y%7R/?;)xlx5w K\OwL_P)e~omᣓq/ǬJdhovl wvjQ'S- 8l6l"8xb b?)ڋXϞ~ xl.Mq/M+d#뛷axqS,YFh*_E:-|>`ɥMy Dg*69⺇x/~ɫ;L81~5s~n̶\LsÌR8MΦS\ mоE8׆>}OX3{`*5{wuݝ/߯g.M*wޘ -nt_F14lSX_ wg>Ēy-m @lw.v-6_w_w= XNpGSMe>ea3tݟ`o9jeZOONn_|4]}$Cps[mB1LXg<|@% }J[a1a eRq%9z7¿+;lŦ`Mˁ{ tJ˼ڜ}t3ϭoea?Zg7xWV*/ 3NNgo{\8S~wkmBq pt'g,(xjɥ>/y .Gݻv&bn¦8r {5~?>vc'gu2=:;z~*CS\&=mL߂!3:v ],~pn.zi6+ԝ?sx|HzpHlLE+B;v+@;s׎݇w j)xh}sf \I穳{&UY(4ڪ&A_uN|~lZk |n2Ol={(!IP[Jr[k8hG+_i3N<|ĸGsxqu,m>kt'>C|_$;_؜}~}2[û{a>oZTpYg|&3/I wR >ac28Чq7ӯ D;g'S|v`2(`v>t|MرZaqUͻ Z߻^TZr~ɑק_Ty s-ɨV!6>ڲHTkmBYL\ص8mW\g' ә6O[o37 GW-c>9>,tș8xU;L(^\{|}_&|$EK)Ѕ`3&(!pW 'ɾ3 $Y-}fz'-ikU`+6+T4'1XG61qc!RL h?~Eog &߹suxgPn>sf2;Ma /Y[Ύk}>Ͽ`XF;^~{fw`״{ h<5yOSjIјw|S,H; uW)}<|>p<+qAO6Gͺʑ9#':ѷw9CnB|&kGv.OnL?ϟY;uv:Y§PN|7+}~ /ϸ=f +vu]:xl2{Omtٻ9*>$ɰ:fl3{޶I)xZmB-b"\->pXDfx{W@8L  1ƕb3^^KӟPX6};^y~F;w:>=w knq:sk~̧Gz\}:Za#سkgwy8OLn"zCpvU,~'7>Ą?!k1\g by.Y/M.:T*h Ǒ/>:darPm3 hٟ*p=ɹl3 >0 m.8͎o<<R2+=l96{l@oqNOcaVcR Lv>bv+lO쎟t{ !|v2v*0LހIגuS%Bmٸ;Q_lmv\_ OMfgמWa2h䍖dqDK/U;;&dS'WWW68<˷vWxߐ'f/q?ȪK_XXr/`1kZ(Ca#v/6C\MEK3DlP&<! l cK6G>Ν yõX;1o|i|vq;l$[D)9f#/i3q@1x݇ƫ;ONN: ̺k^ G'?1tm.lzKơ)I֋77>CkB6U8+_bƃ̉&/-tr39Gù(;QqC\E\mY[/F C9m1yolU*8gE^ bXK!-lHvC‡c}.a=}l;>w]_&xjsopIN\C0yt!hOĬ䛸D5W,jlKp -Wes.F1XI J52:+$eA{f˼sKG\bS7^ɍ 37^q_][[PYwOn|s\H BaM,{,BΝROҲ_'ǶX*p>*6+T5 SM8,g8s`/`s;/pv./D o<:m8R}`j.D ':OlP|?X +z (K4Ve02' /t9ӷ P|6Î3k'|'W՚WkݓnD- d6]$CQ$g8\orJ m>'XK}7U`*6+R??|xI:& >sx0gs<[l,6ٜt)^ &]փ㍾dj K}G}י\SH:}J_@2M dgzo A_:=ds6^)evxvsvWm/f1eűSTD"1Qd=Ѵy@o}Ɵy~>u_Ć F|}Y>yy kd޷j[P} 2Vk^D7͟Wz>[$]*y҇3~56&i[c3J\)&+6 hJr7\pKO. UO9[mB-9"S2R'l%EpM0:'.gg$,<}3uKÿ` P<ĕpWg ъI=96dJ OƢ>5D$O|͐8$m#t8cσc7mkKǩ2q3w6ņ5|-%~Ѵc'~ߜ"jIdX -TpɹSw91V)lԵ*h p`yB< TĻ8q/džc&-h$"sC\6 !]}`5?Yǁq˗CX>k@r?e(u#WTqzvf ־yeJ8Ww9kb/cщ癭hV@Pf]-WIunydt m1zND|Q/6aU-t>,õ0@5]}$ɳe T-+/Qc= H8Pb!ᝏ#uz gaިv\k\'`Sx|s3ýGCY)5$Úd$zxbrɱAQx:'ڡUTmV(+jT3|̥ @^҅ 7\f@~?gK: ɩEm_?F<+56ZήOqFB1C 4duM>gap~f ٤#DR?_ c==߻ww&DxQUǣ6fS|ٗ<=2B$CHq&zjlz{-euvF4,ahVm@Pat''E z !&X͖'[l 9l'~\͖O~)Ji~j40v[cYZ_5c"h<)'`uf_6%|,P$mwLUS)\\\~8%sUxϋ32} 3n=oǭS8?|r}v9Ӷ}'ݓG>tzr'|PdI Ëԏ/۽⢧|ES4HcVm@PP>1ד-`sBlPC'O s Qg,xP1̋X~4`~?^6t~Ais [m3u;=:vdh e0RxrQBǮ' e3" ѿtP a.NCTqrt+ϻ"s!q]uŞor/YŽ_| x`7~p;`OXLv`$CH2Lte ˲}a2OלVoXT˪AY5L$٪iU:|ܓ dt8$녃2 [wb_6)f1RLڴMx 'l p ?'00㵄hD\$ӡlsL_9ӳ_CW/NdIŀ'"6#EqcnL>\Iy_suF 2xO\>|b}܈w ~'OlL[jC$$ClIrzs ~-9?8zz꜠qbhVRX$򁞳aƤcL¸O/ }m 1 q'q溰AOLAOoyHSl ~^۪EF@Fm`>βn rFXh\Q@'y؇bur.SF2 c{sxeoA17]s`M{lmɵٕ pCp frx#0"Z#z5ZMƱ)Iz5N IkOOpu8MQOL*}hjsg9J׳=nq\@$^99'06$.-?\X [W/2"?ׇspu^8xEKb\rB:⡐٨@I68$ $q l"NU*}2vaxNĂrLo*x7%w_9<NdǾ6}Od)+k&f}=[7O^8כ\t fNN&S!f q&ۜ,*h?CQ SA,H\.+vM|86SٜK|q&~|:٠>y?^q&.É}(6"F]5T y0]'׏G93$r&*"q~}T>ʼ J"{и L9Fxf.wxyو)1nل4( V>9l==ua:6zj[k8hGxhfylW68IG_L- _OīrE^sFg[ HG ȫٌ+r8TsY<c+ q!ܦ* ~bru~Rp$v=s|6hCa\|oXȱ<G3Wԯw]Qy% a\rxp :%W#} I!9wJ=cZ>9'!DŽd+ E=}+m ',pE~3&~  OO،tCI}g hAtD\9"s1/f.Zl,Oh{|͘RXBG}xlVH8de_cj9wΕsKyy?1^KzzyriŮAP<1j zqLzj)Iz)^2vk8xM}of3y8miU`+6+Ti?E:9٧#/y30Os.r+٠y-A|[pUzi+cCyG6_d`!.?%'Sc2K?ѕyYn6(Juy= OB¹OF@@J\#\"8A?Ԓ<}|S`x#hu,p){[OOpIh}f9}SvmRUmV(}8'2<1IIד'mPU$aY \9~?/i>7!1ٌ1͍♭XdJA%j0Ff _I5-r|\\ϑXMsϲuK1R[׍_ q$q+/NenAF׳̫rǁ⡒C[Cё 9J.:K.6Nw)6w-1lc+l; l{`+tmR} !|CI0ӅFa08= Գl9clGc8ě(qhH6)gE3XY nPWXt \Fl4.=![ .ߒk$.\rǾtq:>Łr\k \/2C\oȖr|Fv4d5Mk>~"/4Ӫ XόMj؞ uwbkZ >}ЄǙz' >c6apy3 [u+dK\z⹞KZi+'nSpW`\2h4ga {կ:bu̻kؤ#.nF٬n\O!5ENx\[n8CH8HH`Ó{^& yQ%06^$?bjZțz6m9= R$Cpse; lk`r+OxJG>ÐdhF%pWqp S>o͸o 0 Jӱ|q&|:٠7.~r-)Ǔ>IOgod3&}7zF2d9&lmQ^+6D{>>Ŧ z$6Sn8C|p^SKd\Vu?zra6?OXd+WǕxRlϩ2&>COCW-+JA9CF׍ \93P&B` :l fvq@؊KvGP\o6N$Lun3|uC2%B)yX$CgjjU? uM <R7L_OWq#n՞+p*cŽu7Cy@PT>|!?%c"|1&}8\pQIl=|I/[lo3h^1:6Aa@$xHR,MNrLEC/E~[5VA2oJݖd腇~fnҁh/G(/._x+2}2~C08{jtnI}~ymK=r_d&녓mv  荱[mBIӇ 'b2  >Ȥ-o>Մe¸O" 4'P$ GX/\љv8:KxrWxk=u9PB.ľ)H^8C0XrĻ>z0%/UA"!x㚩 ̀O1>f.&ǁ‹FhϜ@K3^6 &YE6e9+oKXBI=ƅxGcZEh)PL8R6eAVyVN8>Ij6_7uP!.†lj"ec,RNs:'98esōXumV@Pph"ܟl̋1Y"b1GɆ zrcu 6>jCaɧM6b6DL C/6>q2sAD|Na˛*Wኼ2WS/Xp2yNA;h? X!p"4 .Ӛ^L|+25-g >AGŅ)i9V6ҕ.VzNә R,[ [Q>02_08}8gԋ3#NL2À& sVኼ{oI88c\qlı?3MN,Fky&͓2v-gέmBUa>>|c8iqpq-3~/)/bBsی4?86lc\^b]'7CZGr9Su?RD~1G}<)*nq$\E% 0{;Gq^^\^Z0vxGşG%8wHB\iyl^j^oXc67眈X\/BfV@PpDL>i`@bO6 C҅ zţ eȑԈX<cAI}m%ǩbyfaȫh+lRr ͡S>[mzx@1S@3&'qp<>y񰼄g0g aZһ^| ܜH)oٍwzOg}t k~Q7066(30 WH\:YŦn{jQ[E"x]2YWG}[ -ñfqɡQ>$hIk; lo`z;zzm`sc69- LIЋsҕ6\ُ|gx"Nx毼d.3+b#"^a+ơי-MP0^4a_aqZK8_˳-RuS.`qmcM8a 8DNV;$ qImKas6f@~n \LD<BfuJlRr#t|Yeo8|DN?=S77g%=rMh <3:I{@0\,8 D,9)^r cxZ~/{\$J3e Z+2=/F{bH :EbP]R= p)bR?wrFB.qql U\#W>a!y CqD`z=d¸Tz@R,z?`?{u^Wuǡvua뛳 8$/7':/ۦ*4 n 뉱\IxlR0mKkݠfgrn'wڞ];[9]={j=yraoZ^h` ?'fQ%C3s\ڪ 6qBW))& mʣWLrf+&b܇qO^rAr̀GlR͹z,xSUUv/jwk_SmNmW xUTmV4 fS% N 9 (';s&||6qpPu}2~e+Ǩ R$:6k9z}ʯ) \pIb'[Q7*6w0Wg>|odSq'qA.lzf &%^ rՋ?t1C1m!2Hzayp kuoOZ4)mǐ'!;ÏZ@mЯȂ>'pN&'Sge/9<}O\o̦Ȉ+OĉB&+]i#+b.lPy  7 :=әf$FkMI2Qk@&.ǠO0}kxҢ }&GY2gF66U!W]E\Soy-\YI~6ۈCfM~2lj; J[wzewI8>Bv_1;x m]h?`KOxKri@: 'Fٖ7Ŭ26C <1T\cQ6q`)#>We66o%1hjlJrE$zÍ?*.i3tA ;ҍJgogbAùf܆X+߃en jIr4DW ]F Yb_d+h쎄d2bPf{{?wfga\!hh@pr>D'OL>INvt>iC'Mr1@\Ħ8Kqx&S(Ob?jȋ> (l˛*O6:(_pAT]7Pni L&bx>>+ <i xbΡn 6[\e8(yWoOpq1vּKx&z/%3f+pFz)pU{wMmW`\wC?ɏ<5*Z<*6+\gN1yRD2ҹ>Q|&EXlKg㲟l 9WxڜK9. r8qE|HΞ0nJyQd;ӝCP/_E͒ O?/% ʐuAg~p݃0 U\1z9&&m|lz1W$CplIz6|wF. Kdz߇&w`XR(Vb+q>)24Sy9qOb|q'F8S.@#w}!9._dUO}<#߾-&aa9Ŷxq# <>U':—Cٯj =G!ul=s%^-ŕmbuW8/ɕjZA5zo0FƸbQ#y 6U3 Cׁ!Kz)}h_~U+Qn GeОκ]mC@ eLɋ&}*6Oad6( c,ɖ>(m\>˶`"FjWx >DIV98:+[9UxbI'@)/ }zZه-P)&^#6r. :?/SYsG7!Wi'? ɖq}i%b4Ǝf)J́8OMrSwo>n8Ln5%~ħNCnގćۆm8}S|8E 1/f ။ .5dc!<<K~\ Gg@WQ{y%YK:2fJƁQPE$a_ 9sx/Yto|bC'sa|-to}`4ɮIy&yXOP7D.,C݁;O7zɷ'~`mr3/nYزD_Ù_if'L:CAr8kLĶ0*\12_~e˙00R \\\3r$ͱ}W|Xm=(]?Feq|r݈Ι -c PV>0嘅\>nz&^%˫⨸r,11! q7_ogZX˥=?3'Ofo\ƹu`%& 81QdSMPOnEAޘa*|[EF1|/HxO$?_^XI i30LjI.<9ţ&kIUHsolF<~m,Zd(ASCO.R.lu.ل7C\)6]HD|_v}AzPAHseAH\3i!M~솞+C _p̗K\{?3ޅ1rs ru9IW\/;df@Ot /'@Υm'}09s"N% x-NAXp"/ah8Ixb :z c=&b1^.C S)a:6pÿċAg縊Źgs%AX=.t`r)_Uc/l,x(1v uo}wC~?ݔ.ec񿡽)_6X~|`u!}7NO=6ZF+67aD'j ỳ%&Ϲ؈&by"EzE\Ul3_s\>6Ŷ%ȫ\rM\,jST I3NY83wrLlyP)ɁfndΉ h%^$i>/ On˸ [/ZCX,(q2C6>o|{O=ͦ ~ pKa~3{XCl%zsXmB5Cs8b2lE0)sDxt)0S;G'޹ۉm7B`ؿ[mNNNx`/#Zl/'&_[/s6+\ɐRPFl8E)/mW \>qЙЇyz-DW,}*6c3_s\^K)m%"q9),ya '*}u#Xj[Ǣ1Qz\e¸p%\7_g)3^gxg>^8 oV,RCq0UFy. /}O eb8勌b{;u!(jD2?1V{^yR+Vb3^%sMLleS^"q9O\Vf t}#. XryP;'@|^-u`Gğ1]>7nMj|`'繝Ξ'6xr2{'B9d+6+\`M >TN6VLOkgzm+pq=8Ƹqa\INR&=T ߂狸?O g ?PĻ?Am,cŵW^{z ?KC=~SkF`M**6+/zMnx$!gub":ӽE pi"e3`*xK@1SIx[84%M_I ᙯOqyn3;9:6rh`!&=%]aIYcq'q@,|=l̀/H\Agqشyyl/ʋsU+ńZ&f@^^7x0~7":˥(q{Q{%VRXT4QᑴPimL6鋗,rs't&V$!QmЇm|NM-\@Z<D;=gnqXgtW-sI1B׍t\~]`6qCfC1qYrS%?8+y==˓N8k`buEyyg (0կ\_-PFүno_IܧNmBmIq"r"NOPI DgG٠k7Cq3~&SG]jMƕC $LG`69>*?4t]G"'}O u`I .Ƹ8ydA4V:x'3EY(R(۲e˶h[4i;A M)(A#A6RpI(ڦj"O%KLQ%|;sH⧭%3!m0F6CLh.TB5,G85a歱Z>ǹ\A ƒ,ĪÐ6( !GlKxr: }o2PqEwmWH(ri5&d`ȃ<DtOߠ6;@犾;"~VFuC8j.FMyWl݆c#XoUcyl&Zl9 6c؀#`0׹=pz`>Li>|q)]OaXȎ"VX/Xü=Bv<]ATsq{H lUc\dpmaM05u1wݡHmP.l\?y;"of/O %1l*?f\1y8E=.=D?1uHd2n|;6u7u  D7^…,}?wG& ec.mX^?r x(6/ʦ8)We޼N|a\qi#@=BY\B~z~Cɩ8U>I gBQh#$mdp #!orlyﴰ Y 3 E6|V6~w[g Y:1_x{`>L k.!t(B8 ;bJ=i -XC\9_,$>mU^ O! 9;>bS’HY< 3sTPdRvJ]/q7fxO>U&@\yʫOټms:"/ս-r_Nj \$# SeIa=|yNY#}3~wmޤ:sx@B>FE&<|rau|ABQ_å?l80/Α'&6׋{ؒI IԛJ2yLUHyyg Oebߨ+z>p֝ub˫p~Lb+\ ݉=a6sB\0Iv A)POt(J%@b>QAE!?\6l'osX`|ye`Bo/;x]XCQ>{ qtYBE8xա(>-W,Ķ-&62tm@}A'^6\tzE?ZTz1UL6cQMC}7yHEw h"qye,Gj-؜QM 968'BWq1|AXdS\ƿHl|Aw&]l8HLߎqЌȹwzsx e7.߀7.CQ+xϿ7r~ʦ<>,\QC( 1csb r{S xoccy1~}*N+ VN625WTamCm8/F }]\I_c`!Y0-:x cXe}JڠO m1 021>;?Dž:!37m0D>?r7s9( Nƭc"ϓ+s{p}/ߵxo/qUwMK/0"Hwh-6d8ܡ~w.6&XɩO[Wc>1hdʈ\se䬸J gMk?McR}e()9KSIm 0Ǣ={.>ҁ@-qn|<8Q~qUx\l'ڸlC$^vT\͘jcmS FFXd0:y~GƆmR7w0nԏ1<>|έ|ns{|n^_Kp?קµ -N|18pb_B_ X'0_x{`>Lw˯[\n߰-w+xqBJ?njrJ!(s7sMx+#6. VeYqǫn+?l({0T}̗Nt<2Q`0Ě:o > !lC\=[g.E۠7r`^\ċ#P8Q 46M&;75dg{f}, M9i+{g//Ao8{X,V{~` cuB1x[X #qou; ޹W i8klj$xkj Բ)pi Ax <YmE1VG?`K^ x`(u$>*/jqa:PR/[WcUrjlZYvrWQ`>&^6So6j'/a~ȃ`'Z?$00e`BWa=rGn!a~KAwao>#}؀ &!Aྲ/bDwXw.'e$vట .ɸNCJ^qa#Am"J^ bQ_9؊]z6(RG{uXACF.:6[f|7@W^eCy<̖).qࢻ#6q2N%y›~ 86pI;x-1AG nm̺11<#4ca\冀}/^9x O=~aoW9Xе +;XI9 ``wWlnbm+,;9 .Wy[[M> n^\eCPC}zq솔w8;^Hݹz6(Sth8? sČ!Uy@ mp1pm»-6 HHQWtOY.i>>` ؞ۣoS|ϵb0 Jm_<_xA,4sN0acZ~P/b0޹?}>v`yK᥽[.'.vڀy`bㄿK/"rQbvFap|J|[ǗÀ$:\4\Nqb}k%G9ᢻי8붪>]܈7tr Ӕ\`~4@O!lC\=['[x6ͷ]xr/6hm~ %@.Ǻ9| tV'͕3wxY>}eϼ߿ʻxwz+{`eXxg`w Z aÀr6@PIm` moĸºt*}ؚ@cTws{qb߽z̓z9^eB` 3Qyx7{g+xGU̿=|Cܸ.F:6? 6<(&s}sSgn1Fz˻|z_~p=r Z(zӯ[AkH6aoyF+%!mȹz`>L1$Շ˙-͒saGY޺zcw=ŽWw^ݓ]/VwW,\xyл<CuMH;%N\fW\p2ed^9Wǘl`PV)O(yŸJ\ot+(:K\x|5.qz{\m^#kR[6i mr)?e;Ka\n䧮3vwnc0G'¼HOYϼ0oƇ YXظjNYQp}l@>*g<;g߿{| WvB_5ZcZl#x >jCg &p1l$KU'ʪk(e{m'Dx[J(_\tH[ژ~`Q~3.Z,6T3~iGxg">3=e'{a8ELa/:?wPأ>= `,5`젟}9ٮoȇ3gF@qɛ^s=0&xbr^@lCĂ:.ESkN_jս_xm+8\zˇ@IDATr!gd}l*>6`o)rK:rT/*K{u`c6軇W6YnR|ԥ v\}:˷n~q\29# cFRA.Usyw-c27WiUZ[W⫼?@/#k=V7c<[5\l 2W !7hU6qlny _މ&95?g )|4_?ιc  Mlb>caO|g\?t׉ ]g_y^xik]oIc>征Ew !AY-WqvK狩a06mSp]~pY7^?{ʏ0ںY(q x2' )/\ p]2Nn6O?tQqEf^BRIl0Oa> } Wҗd }hu2o?'Gc~{;}bU*>i_zj16?ko3& )8·<]PNul˅ϐ:8?|Zs{e{u{$q Zh N]IJ+g!a o2Xöxk< \SwC O|zbsyWl6k= IN,vqGN,H_ikό\TD" q/p AqBþ?@ ]}bH>նA.NRAr95ƾl7ۆls#e?ʦ_|B6 /SWՃα!~Ӥ: }#2whq3aP]*c~XR \C1+ONm=zc/Ͼra5ƫ{Ȁ,0p~l*c}!Pt3V%R6K7{Կujw.~>l.[V&,w=X//&p6X C딣VL\큞˗J ֶSWv }x_[}>X]anPS/X~.ڲ  /(bg!N6Jّ_ 0]#Cʫ#|'/UQ␤(\"fyjI%Ǚa@C*XWwc)F5 UH:Ox z_Pp$^(t璿un!_ۧAX+#1"/]_Y|-7ʕ;@څͭcGpLr09q6̋[|W߃F*+W.x=)l8$0_&g= _M?p?Sˏً?68udVf=|l-N~7"apt6vJDRs6/UI3g#[|l 'ɸC0 P[K^q쐞:PqSCԉK7~ԏDC)ݱ3wm,N1+O-G?q-v]DŽX9qK, Ol,AibWyAcH5Gmon>rlj~`x7?_xiً ד/խ-=0O&'~ݏv }k+Uذc(`s{{q=6z>ѻ؞PsIczxy) {G.IbRቫhl7J\r5%2DAGs+q e؀nm  Ϗy[\KuSծ7?`n.u['SW/_[%?V1|R5m^8 8< e=xp '[1wx|Uy1|.yYݧ7?|u ~iowWc V !ٓy3ow=N_8}XoCΙӋӧr!R L/|q&i/\nhb3}<0 ]хxr8QqQXX_N ^oe^K/_{6|:uUe5vp >6m2]cx k|,cM֤ )s?9 _>"8FvaxG| P\P`cmw<ϑ=d/pi\wbK;mk/O L@Ǧg\!F~|~] ϛ - d!eXk9p=0&z,$sYƉ0zEFapѝ<jn!'Nyy;l`:b3{ ,:wz^^_;l+/}KVuۀ n{?6bu*Ylz:s, [3^Iy\h(\J|ϴaЈi>~NC_͗,>r#qZM7[:X SW^]Hḏb@HVhUr9}fXz`>LvY3]͟2XX(0._w|9* Fcb eGjcr@Yؙ'@\cK9w\y/<=Ι [{,G-q719[opř/&N:sꩯ&1T+Y#WvAl,Gl9Uzq@g3pT2P]rtЗc&GlMؤ۠כ}P 9>~vU]7}v~>CX(;zPh˱|z3Wx(} >yw[aqO[+!66H'ja'.|خ*T!}b\o;(Ip{O~ _}}ᅭ/N_zU?ʩKWkq߇~(7׸.JYb^DzlqP ٤ H} $EEU1fo]U>X̛}5M|ſ7wm߇5H߳m)CH^r=0&~zXY NHcs=}ŵ6gw!6r/K.F/˫pzo9#0c+G;N_o<w&K;~ᅫO~WzuK^̗7+E6{.W It;< dOcdoc={U6'X6G1j@=/ ;epɈ| q&rXb1}ʿǧf}_ pS|O ;on=C^O'^uc_r,ē7CsX\0la0/~j^q) .pI[zmaxGȑ>` zn!q30I0\._'_/˯]^k٫3,ss_cc1ƒJ/a93w̥.W̽P/M\L%l$PY.ΜX<;wc1}m5]wu xכzw&}VaS~~F?ϧ/}?}8$i2fWh˱||q z_5{rq > n^pD}r?K -&U#]< ߉H 8 =Cy.7S/߲rx-ޱ_drm+닗Ӛ3Dp4vq=l@KKOo}|2i\*7J,[p`BBy]oᑰ |D@z> 5^X\ qwl$6g8%BpQ^6\tgUlG\#brā|m0聿~:oƋ*έ/\_S\/_B P<ǻ1f'\t66ng~R #lGe.ęl>O/>ӿ=zͽ{Z,%+x=}~w9?sn;' Q3;$~vvco_K?~o7-2@)^~".h^Dч}~cE4\(#ġRPPq6O la#N#gWO7䋅sd.Sǩ%b3@s~oge54!8z]ͧ=\QxSdfc9L08wFx)rq޹eA;sg}q/^>my]Əzobofbg!CpCg]o_{vr1u60¶wmO/:`}qk r5Θ<(6w,nG_# ޻= !WpH`ڱۦ_wC_CÚtݹ/p\oymon<'_|)C,Wc p+rl=0&t}<ѕ ]/mA0lx΅s;<YqU -| }J^k%.g\U<`_.#Ni h.{sw}mݷ}|aL}\^9+_~ѽN%}nc2x5}y;#xu/@26Ν;Zelv/ܤ߹5~L!ιJ }xO_절w<}׫cLyJ>eRYq |?󸻈&Ӝ\.#Bfg}Dƅ;3^_mH8qŸ!eLv!Am<= 75&gX}6kpw[g瞿yr{OŃ "7 =:vrsm"ʹ_/Tnc8ΕX\۬j\a=1spS2&'W҅'^܊Wo7Z0O/^5XlIw>t=xv+a|l^8/ ]wءEom.~h|o=ůo2QTAb&CH&2f}s. s9?Q>0yI: $B' 3\g8/P-Yԓ2ɞc>TuZOIg t8pri6V9l . eqÀ N\ȫ;Y7PaE%s8W0fUc̹L[-Ȝ+w0&]. 6yGOg.l/.G>=?y ?2aeP`\< rf/8K7.w0ys̭l08"u]OlGO6!劫>$@]r&uR[~r+c\68U%:5k.{ .\՟H%;Fwjbŗ ֵվvpxWgu; d7[m_ocͶ ؟sR2,Y6~8^*rV?ױd[ ]oƢOh>Vm`xͭz lЋc\^<= 4Og¥˥zrympH .pI Ñ&6뮏 O9Ew{\cA`u/cޑ2lFcJ}LX.[-.Xy鍵?_;| !x `b.f~Ovޅ%1"C<08iM6G7 4o榋s?±͍/3yQF<@Grq !cSl.wq O H?vLH0 5=ĘdW;t8YxԻc< tjb3&+;vBĽri ]Yٵ_e|vrgs+~p>)C,cZoOp7@,ʕڹ_ }υsYH}c`Ap0 s%^+q+,{p9/~/*Wl/C X_yaǿ$ɮ1enV?Lv4qᝯX,pQcRvQfujgڒ|hզkf&/yk$ \xF|*.6pi2\@|E 7pL8Wrz#/c_s}a}V\X$U|lplIprz}x{_޾_x<\2,)C,VvmE\*uOx0tR8_Q|m|`G@K.Ó66OnKW2_DGpS6*6\j1ƹ\@̓2oLfMIY"0C6=q$ ՕsgO|, "`ع{E ^7𷿵lJR2z9e(jrы& ,}_' gf|֜<%sK; ݽitLFlӍMߵ)-?l0(m 8'o<뵭E3{9ӇL{R:*'I='b>%*Ǟ36h>xݧkk27Om>z q 楝>hx~i0{՗v>;v>hy+l: 1[gkɸ e@,Oԫ>b<|t/`tFf[Hy Xbx;jf a`/WڒdCpԐ";j`u8:}`Z.߆#pt%<@ }b @\sxyTGp趽kI߈^:CLKkvl l$B⡍"˴pI#p 0ӵe` *$Cm̘U…'%^b[_F݁8r/z ~W_O!cbns1ѹ{(t͙!QJI`0 Xk+y/E]]l];?xĦt eO]A$AsS)Qz?c8\orp:%?iplVQfz`>LXD9c䔎 ͻmRa@|vr!c`>I3(\Cy56TXg.MNqY.8DLۻPP;#ywɡ\ؐc|ҝil,uTw3/.-l#c^o; O^PW?+/m,>Ɯ0Gk˖r!rf#Uj=;j7 \Y?ćP<ۥ#'r.AAl@H$B _qf܆XUC͘8iK.`!lGj^rF8=..q2=+]C7cm$1.ߜ1b/8|.ӖqmxoOr Bu4x.Cɍ=?G >*x{0\O/y,8et~$eXRYD^86 cbzm|(mN`V< 3jL1or Ģ iɭ;PR/J2бPI}*pUxtKam']sum !X j%| |pqEL}|5<&u%zpI u1f>>q# pi o<+VrE|t'b6כC\+ z.\X*|ب\>.˴U~,c2d~hL@fS_Enp_yJԱr|O^b{ۼl}]p΅`|3tXBV I& 2 W2QEnRpjD#f`BǏi_onˆKb` & qIGW.M Q-L6ϋ6% =\q.'.5Q͗ =tǤwƥ1!ubqmӈyGs7N/e֛36c\}0\Tw˸ç>c>b Qܧt|Rӷ~dk}rn㪦\>KD*uI6S˱|PrNQ2e|YO[dQu~^0Y*!M?CAN.`@k|$0ս6yˤr1{= "Nds }B=l륫m˯Dž\9\sUwRڼ|r  pq CuWOUj2? |C]ob *r͉xjSk*>s9@O= |T[cC%u,t $.*o] swprnQ oTcS7/&J\ū>][yWyUԊ\D.lhamKwB s9:Fg=o|yEN:PHu%AH^r WWB؈ƜeW\/6ث!b}|u;SDŽiaq$wƗV7(bU|2/rKZ"+c>U5s5ж %ID1L֊a$mN;yͷ09ķE[ F('8\$^\tzڠ6IY6#>xc:K3>].١Lpѝuݤeam\5=9ɾ+Ac3ճ&|M92W7Fl F0cA4kkxqZY!UD2atY8ƌr`Xl͉xj-h{Xo ƭr\!6K[HmYCZn9{x;ڧ%[!@>by 3vFriC9̅xo'JC1Ƶjlu HXou@}}6|U>1WŁ8>t[ @xʕ"ߔ}g.**TُۜUjQ|,|=z>Q>Ţ |{g]F76TX.N% $d)yQI}ڔ\C%̌.E}26nkjlmP:z QDŽ!GXYr|%?K(q;=kG̫z~e\Pa|a)Ad^#@kDF=c @9b6c[~nkb3=z`>Lz> \HKMy×dȓGr8%>J^"EYx{\U|>y-mHu.{2uL4995'z<׈?#JczL=0&t<!I, k#SۨrE2XHXkeZ7/'Jjd=qe5ĕ+.?sMp|6lFjP1- @9 W~>428:q ޶YA:6ek.X/\}6@5=C% 4Eorj>=s|) S^Hwɟ6TXwqJڠO bu8VIձ(fZOʹk ('vl| IG";ugqbYqɯs(Q+.@+(:߸κ#_ݡ'"m26,SD5_&x|L0tEwۼ&[9 $Bj4xƤf6ozun3|\L>toSJ:YAk=RAE4٘4LtNj9^6x Ƿ\)1%kz`>Lx{O֜(HEwڪalc%!X,c@ń*k rM\t -]P!76=n01C_*q = 7u >S\Ș=.ulOF&Dl|IUrόr== pt7077P#0b!sF|;3| rubq >_#S>\<3^lŶ?"P,^)ЏUg^B/,+\8V(LQ\cZoyn%'f:n.9zC0|rNs1a#ƇB:\cbǢ\6Cq Dn7.̸46TXw̘PrY,m |Ɇa:6> (,QOo6^2ؗ}LlDŽ>;HYoPwb)mC\YRI*P/^6\8d#sYMz/EwoHrU p>!APn3"/)ya8 kH^67 !7As9wdF\U1Cf2KǹHCM6o'zT {̯ *L.2f6o2|t(g^?& \i1<% d+p1fs$9E{`>Lx]wHʢ3\)6=#OO1b,atkb;WƄ SۄxŒl7X:Mx*f]:\6ߣTBPaF|(: Dki0G߇\+p,W ߏ>n.9ѷmͷc09Gr0)R !13d30 ס6dLe%ڸ8tm Wql!qqeZ4 c1Gj󡲱|Vl|q*PeۦqX6my -*zCpo ,dܮr>(M:HCM6o[UEHszoWreL.%[ sf4ݒG|n P.=|;yzE& RχP!^(cIAxr\8c6q ,ftkb.`Ŧki-"NhLʲ9W\N |}.z c^ƘxUxf26Qx_X?s9UgdMpkaD5cV;~(p.SBUy A=rl=0&t=[z;':Xa=[ XOY,1Bn@)˱a 1ĉ= ס6q9ǁ{fo"̭b}`==na'xfcx]<9 gK.rq,\yF^MlƤ3| %ni)8 x.˜ nSȦR]Pf3Ezj;:ov6Vѷ R3ߏ~?ccyYlKG!ΟOKv .=*'b12lXMlճA:ګ8u;X ~Gds9r4^+DcI#ϣf>¹؜sx6z[rOpdlq|#//aJƬm 6e]P mYzmi.eVr 73qPPVֈk^5:4}~{`>Lr{O瑓F`A!yO(\XrR\xxqgh LcM\XyA;yсzU4(\pu@| 1Lĸpp5qW%W6jm`lcpQm'1qas0!^qAlp g.]+;1 ;_dlls.:8^!R/.q>%[6_9uNIok7.+J/\C{f~0Uun%yBI}kC<(ǞpI1°#jDpIyg&S3f1~(~XwPOԘ906Mp>@|6ȆKbu})!XT%cBHb'op1nP|2<gCRs@324WcOF )Cr,Ðx +q\T)\930c56hO+3].oϘM\G_,'eəxn^F\txe?Ios@s =r!Vra~\8$Gɘ.ņp;ȅxU|̗]n0!,K]3똏]CfR.zF(6~%WZr-Ȝ<ߎ)Pl౐!Å q|sFp-$c*3ü"~/OqKr01#<SaN~0&2cҌ?kcXsq"P/[e-Xak jwd)6N~$Ϙ$❯*/p!x1_#j=yȭ"vs,ڸF8ܵ8##'" qt|+_ηc0㧼CwKVC%< -WV2],#/8GbD1eC<*M:6O*پoOޔ~w8G1 6AȱC ʲaB Db1Qm =l5J\e˶%UʁUæ=c)i.WqݒmXOC&\Ӕa̶qE6jֻܷ|;:rKױ%z Jrԍ3x {X,\;9mWUQD^ qƸQ+3'O*:cf@JleE-r`!Õs“zm N6̷1*b>28E<tdK([e?:Rܜ5 Ip%F\>gWij%ϳѼKm$<'lW\=<1\G0O~3:Tgy[i7ZO~j[ yq˘ mr_Xx;40mԁ 0pr9^0Jgpzx2e\&\tI[>m\6bm)yUC3\Z' Uҿ/hՃo*WsG5.ٷ]\1CsE8tg< pʘ ݹ6e~t^u/0@jÅ⿖-̧֕xb)tb.*)C07<' %[oc)zT0PL/,/M,>{`>LNc4!ӡeu%q-2L#_VC }s .>J^poXBF.ڧqUx'讼l) Eۿ`_u㯠|]-'BR}U1ǐ2C]I}. ֥Ci!L wrN&pIģ_Zzx%Y"kJJ?M^y B6.oj"om=TZ{+ğד H%~l 72&C\[ko|m@cO0}= Sb5f8qnlrE?vm8E;qgr/\n-ƿcs͡6{f*!tl+myq`)kD8O<)PGOu lm_|4ز:;oy$b"h%!E@JJrX)T~\#[-ۑ$XYIȉ])GIIv,ʦ H@=~=={M4@}^{o}k}{^t@3FT&f\oLo2Cy=,Kj]Npf,F:e1%$./ȑ.eKxoOk&ҏhzIU_9D*<,*nk# OI'.\o2Jy%<%3_h!ٜ ~ >CqxCM73WG'3Fq=!'cCdK,xDڑǸO9Wt (/~^@HlPpOx3nKx_iz2'a ~' zû=l%}أ̕703٦"A[o-]_p^ϝ~PA_z9'us^ w_+El:e^K)$0CbUl0$((SѹO|eRg?T06py3Ѕ lH&JKOUǡZ(ᬿO;g5?P>/[sF\63=/a>xCĂ6ҵl c*H8uC\y37g'|".[*pSf; !xT.. |}'cLE7bY]x‡0.m-̿L/Gyj|ׇ_A#hS츾mPM*NĦ?j\ond+*[UYpAt.K\$[x 4VY;d"C6Q8R&Wfw>8-@_3Rl0/9'k?`1/Fñ&C"/d(y+tM>d)Q'_vƃc>ёҗt\98Ń`r=YF^HEp۵OOxƱ^pNҹ+ aɅx`mpu.8 \@!beȪqP }“J+m!%~#Z`ٳWIIͥ^]Oxu yai&˅Y1 els2vz#XxF`02*]F~s'^켼ysM2iӝcʆF즭Wي P?Ε73'䂫Hd6b3/jC\%<~u'i(*\|xbUOqNί|{*<:?}*bSW^Z|QU\_'|%ʈܿ\wp{[\ yYȣmq9oq}f@ɲM d.Gcxqdi}+<[}Hn\y,)ȋk}V/kYtʉ9_w_Oli'tmzR: }|3):v8$򜕗-r>wXoF9Yo#̩ˇjr[uF93fK91Q0+>`S~5O=of8h*..3WopBŏy^ݞ"?ן=x}cmz]9b\.fcm|N|]8҅ } Pr;q['&>Kn#yM2]eq*xu\O!Gmכ̣mfD[r>C}EF`0yC.yޜZ*FOK^:AU!cɿO[9E^%Jk:.SW-6#zԿku/&vXa?n|Djm]׸Zrznm$.86C.q.Ks."8Ǹ [˚Yt=K+vE+8h>hԸY5Lh3<UB +8`dz p'pQ-J^1xK&c8$56LJOdi20A0,Ne+p'C< QC,^0Dy,}t \B\6^bCDDžOoZ{kq1X\cw9H9ࠚ:8[=lJ8 ^. "LZ17o $At\vd87jAb%N(U ! URslCOq,sKm.?TxX{$ZYOM֓9)@IlL"+l)'8^g[zkJrB8'*`C}eF`:9Wuw6s#`JBZqɇɎM8(U4V ]%>/О~}&R#9D$ȅ7>7w/|_{7"&5t`36*M64wNO";m뻋K=\L58DjW^p8 .* N6(٣dC$f~3>kbL#VoTf> cxq,'f\Or| ǣҧ<UΉ~6.aN6+3;,`W&mď!K/nXM[ Ul 9[=›So ,  .lKbcm% .pHF%q\O'k`~ _w~~}Kşk Ǿ DŽqONj1ƓZC!_WE ctݲ_o}W26{mc[" a0;m>On华 a766X<>K݄v#Un*ޢ>Թ7bN,-Vf Le(V^/86:c6ǀ>eo/8n ̅ǰ1=+E v9!:;|Kos>I lU`-lqmW`2WKc 6@tyl}rDr'*pN2!ɥ2 (3eoЖEȹQmR~\x'7pnc~w}ߎ?MO2tzQckոsB)/,rVv\c_sـ8 ~'Li @3p}-<}RnjѬOulL\lMjec$Ke+b[,W8mQI.ˣe.sj9Qr7S3+;`Oy?Ësm9_~qec׷FF aj(o-m0c&a؀,Ulu`.tk,FN̕٦E%-ҧ͕l@l5^x&~<'XWk}v_S9lNyNt 8 U?ub3`YGTv틳7]\MKLj{.ℍ(} |?J$"zBzO>yxO^sOrF`0(^km?tk읁խn~3g@+_Ô kyV60~o^-q՛rXyC>vr迸<7֜1*W/~\鳕zd(ZD':s3)a5x\63>>fdf{~{,}g.n>GW6_Yۼ4CPmol=p2Es9~xNnp3zR<][y %~\B֛}xL*x^|6,&m8{_:qw4ݪǗԬÝb_kyi<}%cU3g!OwNp !]S-.eAdoqYJR^y3@} ]~-UqdǁW5u$N̄Pt^C8mƭ<6"D%?NJ/Ad,7T n@mM96x~_2+0Y ,!>*.[^&(K偋A7\p۾oGo0Ǐxgo]W B g.a7hVm6fJ\-s A5H@0>Yʯ?+M[ptY _?,={ݧFu/=4>˨hnņ/9^pzu\n9I8 GHmXK. ?JqKG0dw~S!Gf@ oW9.€\ t/zeXesx!–_W ~oht񉳛_z'n;K.s[x`c_;o.UnU|*VxawW J'L}ب—Y(w}~ciN[ӳƇ4 l~}ȉ#c=_osP|KoicG<:]ty-ʈ c5L4mTRl ]esl:ڋ!ɼ>+畹|q {Flx)<}63#<ݪ%|abKԢT2Dczɧױ}P):Hշ槮y j  NG|Ay&%l#|&-wZziŭ:/.^u6ǥ~`b3G|`%l Ƥ<7sU8DTG$hOʁ1aA6(Y_J!ǁkz[+~LdFGO j`v0(;q3{;ܸxcx͎G977z[soVW8̺;cSc:wp':Lɨ!;l'WaL6\o-.ƈ˱=6Ry/L*(l~͔6辆 ~_m6X PۨYp ':)bS Z6*O6~^06 ~#O>trS6>/0,l+0 b31>r:MKMG>.& \Y _?̗[=x{^ڞkޟ&V_ho\xN';q XbaoiB>|S5橥j3ȮGxq/ǁC17\m=d *|N6hŜ eA3W'{ pFP 7gZ.<"r^Dnw_^ Z Wq^*ɰ@f!$SQvɹo^8RRv,'ڭul~ƿeg NLzwnrFrizB UZ% ];w{xDOC'^]'׶nC1ƥmUm ncC`30څ7p!#ea2eaeof^?jqDJkx݋@d Ͽ|ReYd'$ԣ-H6p6 y^|KzG8|:ƪU`h9Yx_M"j[h3|b|'}Y6ZW7][ç Xyt|?Շɟ;v!TZ20g=0aܧm# 0>.o5 `xZQ> 3|سږiCSgS^78j#d_VPĩM@m+}d+ȫc[^ĴT,[N/42pxW`PM6D~usaa11곹$%pE|(//kgq`e}68TO>fwx=L7 !fM^1@)D>4 kɲ5ʹ3Lx̏xL=7#/6p"%0[>}ls`\q{5!wHv *5 $G\x[}^5ݐǦ ed͑OcoT:4FmPh]ډˮwt|4r{m9 [{nx=qmSP)_-SZߗzt:Vױl쫸a3?Bt]Ҿ2'(~3v bYO^DRr!dy2wws_{=>Mx(b$b$^fa!oR!XǹE|ȩf:+>3GIhc{A~^77~O7?~f/fB)ۥLSKuRW!b)fu/`)مC`<6J5L˕e;.Xe@- Řc' Qh0l/> @EmnL Hv36#s|32bPY6ৰ uCyc@ޘ鿶Yڣ`bFcwl9RMv7^ #'xrO>sf-㓄R(&4¾lf`{7>Ըk0o.]+8ά5;1,;sd-rǚ_>zZ#y%z;!$!I: F.һqL6m|ӳcjfii1+fOI\xc~cxu25 qR~M&ڊ$<pLZzYXW ANCy]Nn!X-FmIo@ ަl8#qVG؂ƛ1_بO1́]Wu^>ړ<کO9v8?x43sg׆`{Bz.dQXSځ-'/'| u%n-̽|}_Ej~|Lڨhٝ y<@ gNĂkG/dW]pYؚc[s;<~{ (X]wtsEWvD\'QsQW@*?X-rf)6qlHWڜ6d|3b3Ndb TlD3l|_XBQ2K'6}~?KP#0lf8M6AFU'xxʆkd20¡jjMt1I>kann|}ۀ֗/l~yem7 ^<Ɨ.^h.>X 7 ",#.g|kLl/}70Xkf3'?ԩ6 Mx3=%_%{;NbN GF2s@ eֶm*kؤr=5.maCV106>ǹb\y !|J.r$ C y%ˑAL8S:"ؘ@%Vz "cy2^* niU\ }6.0L3xSʷ %['flnde6 t\\ !5W?ᜭqלGWN 8xs~ pgP[yq:T%c`JF&(㲛:p1@: !N,cxz =s=}uxv,4[ϮlQM5 ։vו/"ee)IG6ͨLO>!x\)&|P/[ŕQj"/:ecϱV֒\GyR-z3d$YOc{cCVa0Ù߄lar@7p8$a(ta+93qmi$VxqO$s%,}ye.I vwp)0m'^\o2^HhKSY/'%CU>AGo63\S|prӻ6O_(xQo6+ 8%1c|9Sƞc Gb{:{y[?|m5k_z_=wf߾]۽koՔO7~⏛wrq|IԳعI:iE/Rۈt|e0I7]Ba鍋gȒmV5!#Bv\j3\6/bSc1.ǾyW5,6x1.8[i%N[妵ԓogT"\){>z~7~|g(o63xE?QU7;ow(+`Ll0ݜ\0t.S1?k:ds6WWE+}1o e{qanﻮ2zqek^£Wnްr?{qI|mm~Ŀi~c6`0 ut)dx=@eΕh xM$.G,Kr[XP+Rzfmcdɚqy6 Q0 \lW>x#/(C,\P!AoB}uy!0 லIvLY1S),_6fe@IDATa0Ù潓b7j]M\P& $\TOR[q|-.OqlOE8:yBvk|&~C˹S_:/\8̩Å{rGv;6b"O˟i}CG%-Iâ@#iJt |qr8 b-{K=x#"qXׂ%J /ϤN6;tGҮ+3]S#,f d3<fG?dC_2W^fRlbByxq_ҸI=/ 9T$;AK^ RK ,oH֛߿sOU\gӠ:pt@9*bzpVety~\+oгcxh{?wnsD \5>CWĥg(Lٲ!|/L!7.:A $Yv$  ﺪoo>vw4Fil΅/wm}\mq}6]Vuȿׅ&Jߵ\)x mMДlj-F[*?'n a3e[KX_xCN5PRL k2Fg}N\ |x@|rE}LxIc>qxV$ad l +(Ci张NԘm'|+bs᣺1z,\pSG^?~旗9)Xɖm*FaRzoGE hxAlU1ࢁЖeƹLoT A$pKzx0El;&+x/ﻩ|<筢"oGlS>w{L%kx/>(|dCGCt_]\lE^B[!z"ف-r"lczBr D <`oxqFa&F@@ލ%^m(k٨4|L ؊mŁj"hG_ĉ1.}{_j޵Fr|ZTqYJ|e8(2XbHO4$+Aklq޽;.>&Q>2WڬOA+T_Y٨.&pu}6.YRL~+ǣC~#%>p:3!h2UIRo(pm\ ?PPH#0lP,.obɸT7(D3V%.‡dq1nd3,6xݧm" MHႺ#g&"^#X7/,~b6y|^$_ rF!`%>d8juD*Ɍt'"\3=BH!o<Ƹ +;.rW$"Ǧy+H~6z%%vM(Gwi1 K (*IG- 9Ka6C$, kTaM鏆-.} @6Q嬧"xlZ殥 ~aZ#륥=#sC>bbTPKSl.T:Ɔ"aJ|a+RiK~91]X|liŸUpc\oyƁSl<` P*Q$z;!|U\:ER^5W;<%[2xAz6n ǫ?u^4_Qc's 97q{%@__tHm k-,TfHh/B;k8#P@ b@S ZB @´ok8O8PIV<oK>޷>.NNBz76W-? |3^ʀ}!Xer{Ao?UD1P@-1.)xq̑WV*VF!I 3 !?\xmЁ7J5!X%e:zWp,l %6a8fd'y>+5`=~ UaZ#Ŗ~hvO׏Pu-[%:q\sf.6N8̉ZZkgw|f.~E3e ^ YDKpvJ&]kQ VS.+T奎6Mzq>,Řٲ# eiF@4৲ F.ɉcF[a" T|IzcXa1s÷l/eҵh&-<|]?Gu>>?AR(}s”xUq1d9y;I2{Fjt\ٷe%u T*܎W$z;G͋Q"eGl-0|ۤ맲vv|^Iơԥ\e䗸}6 ϶^ cs@q˲zxŭ˨?z(L?qMO&F$6~ k8hs%F.e)0lx U:\DpR8ιtг0θ }s CO6_7o]~H8pX\wc@H2\J<-H>>{.;eo2"vVOjP' ?(t'0a }r՛p86rF]߸yK5ܒtLmK=v$ΎnKI%\9IwZ_d*qe0.8HH8=V”lU)6Gz̓w}<6"Q\tPgBjUIvC{ʱ/?dycZ'\5zo.=A썬1D3uMNZ芳3W8G*fͱetJ!NECRKQ'LnO0IαʇO{Q0 fѐ{P%c¼F[ZrR?x@5ܿ;qlЁ, L +,M]1]iz>Nۥ [~._M8. _oY?Pxu#WG8&qdwSUwK*:_{| K2'syl^_n>O77y- R;4 e;]Rip\χ) dJ\&K!{xnɀ"7vl~[-n| $s GO<0k.o$0wyP߅zZ>$ ]9-P҅}-m\-=_~x}bgu0cڽ\? zV.p7CH%مs6W“r3l.J`.[#L 7.e*خtEhWмOjۼ хN ؒɒ}MmZe/k.մxK xbw䒢TP_MDڽ0wnéUR'jyk*?39/OߊkN\oDNFN=lkڂ~]/MPݨT?e\ȻHJyRkaL } [; >6C ^kN\?b6QXc˶+QprC/iWŝrdT@tH2=B f,T?Io4x += ['s#wҤ4b`lRߝ2%wE3Jm܃;Υ~,=3S#T+&Evax>ۃ<}N+PwD4a++4 B0[ %$[W\+ ~:H] e?`1M >i=qH&O7Lbj1. 2.l3`x+tɆ>ɝ\02%s͇yYB…?gXTJLN$<,T`4 D?Hx/e WО3e%S\#-=rR&orI_Hh\ 8JAJrt~ߴ޹\پ8߉ƭin#]-\jǏ9Fp 酶F c3@}GJA=`EPɦ1|7`a>`!ޞ b"V) \Ux%.I[]Jhq &^O,mx/rzM4SAuL-<:bfer|t6NLd:_ d=1rO  ƀxPM*VHX cG˹D\|t!#r\#A&oX RhV ?J@rQ>ۡ^Q~GnĐrc^Ȼ8 ^Z2k^{ d 's9CqS?K5&a+ I nnԥ zM BfFmF.G" -'oG K.q\\ᝋ&5<-т?sE!YA)8.fQ# {JssR3~Q[#/;'}ǏOKr|䓧wx-kHc^s0@zq C<t0 FOx#9 0 l6).o9Z\c޼/qٴT>O1oB4q+@9"-]{6jeCF5a0p˹\77БA0g4\fZt[\|iR!7 xN?oQت%k>:L?{C+_hvmk\DHT,Mf꽣D1.w<{Xp"bx0ʫG,bK.cNέs9N*uqmS0׼L5.a(+E.qy¯)B0 |4orM88c2P jaezM\$:FyZwqxCgnrRX 32v_giU:T g.\@~祋{q oKXWCFauc27/O{ EP•6(B`J pܹ=7w 12a0H-`r6zCB taRY @}osFpy3Ek5'JkAdC#a h>psxT=?OI (qB@,Z`dَ%B5ҢMjp"MT-w0`/crwnUk S8X@ &Ã"m{<(m̱GD%P!'|Ou$?cJPxL'z?|ViT2ͦdp0 L/?(?us՛PA}%A8Ag.ɵ{n>?jvYZ['O6{{q0" mX_b:M1gOFqѐrPt逡 <ڂ2$^ xT%ەK!lO#}08`!abhm)i΅6*Or>B¹Ϙ ڢlG+.lg%\gOً5,7g𭦵3S͑0cFM }[be%:wxPv1L1ڨcz@f\}>hTZrZ h|~Zah #0@ Q+1_jIA5|4\ʹ]sf+'ۄre |#h.F" qlQsti9H< íŭQs z`qDĠ*GƱX(%=Q.ˁxLk 3 "_}'E)qEK)xp)r~wG.>icm?-RINn00Ӎnҷ&IJ^։ޅUXP2=eJ朮Oq $\ 9ӇbV{f.<'1? D ų2Z\h|44k?zY_]iv'{NGaQmHP6>"EpO,E +푺IM۹"UFKAGA\rxWu6=1F3ڜ~᧣t>kܮaa?6cc17V'ya9]qSl(m\ݛ*N;&"(&[V^ Z9SHe[S`92叱_(c0ً +ͣyel8q,,\ J "&6H[ :dLGMrяl6ҏ)amϢRnsY= #0#0lv HN&}t=6N N. pm<=9Ff-a8Hoxj!P'XL8xT6wux5/OmVw̟mGNhLZ&7'h LXmU .b!{ld2A*0կ=0|$$3Gܹ…T7a4`l[9qԦs9ŜLpPfͅUxqy,qb\4$[(³n裾%.lA=ZbIDTCfl*nӗ:aq쵻k~ pW!9|`yt\|aٻܼ}0&P <-B~F\p>{}@NF\'&lZ$SE^wrd/{љ'nG"*>d؏})Wůai N#T95d3&/P@>8/څA5c\Ol=W\3|6ƅZ#y + }be/8ȧX[L,9=k9z :tj6Jsr o2,`0hxHp4XیctQWTģd02v9Uv::7?}WD%$wé]|gSaa72 $N|chÁ08pPmG\8\K\8 C⧭!٠L]͟iF`й'6B6yU|AO\0-.f׳gɷ -Lyji͵-KR(#=Wqq3mOGUN ;1Axs>7Z-Z;O 0L5`a2\5qBtJ!| ]eM#,P ES강Kl)aJ{D_tVuȸlI3J[o۱JSo j@jXur6kol5_;ܜ=:ܶ+_As-TxI-z5\:-.sq/@$Xۏă6E֚Nj.5gv4Kr1-cTq XJe}ˮ({{}<~hn0lݎPlճkO;OBSis$ #0@ Kޚ3`N1WsZ'eλswf@hzhS,qy M$b+2+* ѭž{3U!"2Ȩl3wG|`0l)Qق ].myyW\4[svĩ͙=-KgËxۀ>~ {İ,qi/x8:^al>j!BN MII"NOS9qCF`#0l&OmgϚ\5jnJ ͷv%!g ԫj&+%^yy\W[k xDv\Su`9zoj;<\=NXfP ŹGB"ӧN5mv_:r<̓68ql+KNj ǷSwXeVr>忲<{dzTsp?Rl{?<'KIbqBumomgO=^>Ð8BP][>ճ>T1ݧ ea&8</Θ/ys ۢ Od/h՝m3u/XɘM8Vmk*{tDzm_Tu:}D.Ĕ>lg;"N7Oڽ|M|۠iASͩ~F|%F0&*Ŋ0:#[޽i $."2X{Tqz)>}H 0La0itZ6΍\?|y)i,&abf55F.ꥣ A } ([ +sѦXVɁĤ=Sij$H#;@ewh C&fJX|eފ }Juy֛;%Ɨ m>D慓'SU#Y?cncSˣb.Vzv01T W-S()#QqD8F` IӲCY9暴LƂ af FPCHIQ[Zgqz^'eicO\9d=\ iWD'3=m2Zk6L[F/v6E %;s/筯vðXYZo޹5"t0Ee)vi'T/ʭ}(qL #0#0lv Ça%:aL]z_ uEs8c\L]\~\nx"" nPU;a')d-Km_ x Db9jX"*}KyVt][}R^cX (;M[pS⪅JYt̿3ufF`6;PaWEv.pnҽ E<ΰv}!Dĕ ='PA̺³MkH:e2@Ģ {QQD(eMhK&lS7tOlpWi6S,ͅjaFY9?y9#Ǧ\zb"d}pvO֡qf'i r{{72c 9>9~Z I:]9JN2 7#lT5uua?XvI\|TZȔ%lb $CThכǔ|IJ1IXTvemj1Swg+;|=1CD L )R8]9_iRvL [?\<8 #6(O\8ОDl\Sh#f;. vOmd;v.Ϧ[X _`Yc K24Ult]m[hM#`3--K=^>C;!UL)&û Ew2Zַ#bO{枯^a(F`p8Iq2s/ i0MPlS|q-E|=m\8Wl!6ek%s["CMEXIvZpK,ի0`Xm>oa;b(-|{^[Ď̀%y`-}Bhrؕ6!'šЇ߂I 8:~va^63 u |8@w(pHӧmx)&]O_^\7:Šv^lk`gqac"{ړl̺3eO#Ձ8N\-68qK=\Ul2)|qQ<ą6CKxNdo[,$|;D:6[ ЗX.)V;v70Z=y2LlzLRJ%GE%Ϣ_?&An{!n']_a(\63> gm*Jl\>S6 U(_8j(6蠐18T0.0lAt͸o[dc['F@ԻֳSEU|ć B\e6l~6’(˒SU l ڨ(0ꢟh;0ĕr] 9Rئ_NCޛ_v\aـ;Ap R@P+DXDb)Il,ڒ-ITeUr\.KV$RʱH؉C,$o}ӷ.o{9Yo>޽7f;v'.mp!ߍ&7>Kw߉0o 9::,:\wKjђ! D9_2!.K:!c2~J4[lɘKG t$s|DKFT 3!&il,}lDZ69X&a/VlѕJoX 6ԏBZ^4'Jʳ8U';N/o`^8~0̓ܟUmw`(^#0lY&DO\ԋΗ B5.mq#E\-l> 1:zJ4 {XIXi;>~OS6a^ 9FIw&Ŭy$^S)HxP"mlmK2e1?dtpGb="բeBA:zjҒ5u , ] 2~v;舅Þ X8[aעdt4%dA S3á]_r'BF77,]Hȩyl-N/TVɡ/o 9ڞy[ZQNnJaX{ K$.+!M~-%)a [?7ZeԩmQ 1tcnf (:6aW{oE&\ 'u,ŷq˰T~8XݮSm?潎jy\\GV.}c.ןЉO_ǫ_*n8Lwų7{a 3]׸c@#%i.)'xõJm3aBeq \$.mquofE=LiVO -[id HVVR'(7onqؖű(^ё4%v.^#[ _W?f|aV-=/s 86s "\|Ī6R]']'ϰed' H-(x*dnKPmS6b* \x2[2D"Zw۰J5ԥH“Fb!MOmK2e+udذB2Syd'/@Z\IˑЙpۑ0O ٬Yn7wv _Xɳ8i?m&&Ն^#̀Ǜp,Q[l>iC'>:y?9V5~6*^S/.~qz7$ S=6s\c@\QXiIģ, 0!p +lNSF`i@ Y-l̀rY G B!닫e)O<dRRt280pZ 2b_U/hS=hv`C)n Nf4FyHS 9_ccfБuXͥwTx__M;2~av߳75^uapչ6`+µkuZA$|َ|T<9 'ۘhfNVllM8#pq5$[,ԅ"S2jOʯ> 3mIv躭6 LDzW~V`GW+"=unpWSp))U~kwT/..!icign%3CF a0PGЉu;Ỽ!Sΰ̖Xam)_ns3d,E%<?aHO%% I!jrxQ `ȏ$+3. mm-_ua2kg;2= Xo:c8(L: -,4 Ydts[3)&ꎿTru /X=ubzx+-W^lе!_xѰE2S,I[ZY+Cr'=$?\G|ָ"Z1$GSӡ\V\S$7{LIx^7N]W\WvuہW_:{p}7o{ǹR _~NtǛ{ ן &7|*GE۶lOdr ؞Ֆ4vx}ӛ@u_Hߺn:8#0lQBğr=cjVG44vbS5 3Qc %6M%h|LRM?SvhV Aфne䝾;e(%Fk'_'#čSK ;٤LwNooQy8+xا7 ="0,6Mhm{V=+i#ɍX}W>~/HG}ޱCFa0 i,U`%]$|45bf lIܥ ߔ, y'Ƙm {;>pjASg7}8| N0Y ~o_sg}ڀ;k#0lz<X'>*y_ 9Q-lM4Y4e`R\n囶{l)R#r3&OJvfPuOI*P@C3o˪52`ԱH@]d"3v?VIum<lUPA1d$](`4)d LVoϟۋ`8tJ|_SLlnOo xwxz/Z{c~KZ7}'^bWO\(P8G5j|NX> a=~IHzx&D_1h_ӑtO}2[ BɋCYϦkokP7V=lsԮ} Qk8}{?0;jo,5Vv$,Rg#NYM c&X>z7OЙ&'r}ڷrfLө:fۗ <6`}s|Wۧ?q6oO>r7~ #pތx53{:TYƋ0/}-c?habJ[ɃP2X'!3L#^`[aΓ6gp{p WUM\Ɵ+q37HpSɊˀjIx )D#m[1sH nwBo3 7AIOM, K^n:{cq]|gӘRÍGS'SEp9N?? :hI<>gyD\:^P8OG` 77^%ױnP^W26 7k&!Z@SOG$-q ̖ht?eBd؂(2m6= Xo$m98+}4 &EGؽ͸ `2[`bo+45F} xԩFyDm膍Cawl[KȞ&`Ļ {Z y hl650 y>ʵź/GUl)i-JT vնpL pA[ ռ//7zG _8 o48mB:W.m`YM /fYhb{pN-G)=&`ZƧɟ$8tjm:M/ifXuͰ71 ;b-Ta:3n[Ng|̈ۤun{pEQ9N3S6-Hv:C'kҶm F[>CF|a0ǕlZmNl|{x%h?va:# xlΕ[@jn ⥜4$h\Ff)څ-T\Xj {]{–-ۙDp&%:(xˁi]x;}Nt.i1 Xwս81(F ь~,Q(4_cfIFly٨h٬u?7IBP8G`qt߹8#{y(S6ƁXۺ9LwC\G!c_4 |l 78F#\(b _4N) Oǡ#^.`Hn3@gD%7΂ #Eӟz[AN\nW+Fm_Y&` cN3e כ\g&:|vDZUrR}=w0f}_@I=Dz_1 #p+A59xh֝K|%CM_?{olxPi~B~x!/z=g8S %&2;,hU-)-.e[o˺he 庭,X`~a_O/PR"fgЙ'O>O&l9Rp,Cƣư/.2y9`J^*?Dm$XOF޷g;ǿ~x/pz:ؿ~pp:#*p4^e^c+/e)vlxlmUhMLL4LɊͼ77 X+6%k6;m ^lL]XL3B[3|[n%vV&/oAPoO"ܮv|ZoHģzz'6]>1C9oG`qКuӱ`/ow.z~zO}'cd3+Mj|\FF`(9Se*4G<4 CkZ<Ko,nUXv$o7 tcb~`BMz=:J}=:d%@ $^'hXNa'.01a6s\XPsQὯu5ē  A$(q@qTX4 䎥^p6]n)/f M^i1d H/cvC@XQO_ }>̕9pcGv9u L- h^*خԉSTa9*aS˗c.G#ga7E?yՐ5EXM$wѦ1L7gбYn-i3yk4g\Jޞ34Dc6dblF7CWLw|Ͽ 1ßECyB-?Bm\z4׆M,+tRB35>dnia]V}+R?|)|CVG>q4J{`Z%oZ"Ld밚Mrr=v LkeA~!uCLÖc 8afX3`Ze;n#%>\oe}4CxP둤#ǽ`icZ'ą]&lb pOH=KwCG`qQr=Ze@#( <1%ɖ٠VlL9f%_]{<[<_>}/BhKu$9 &)*  DbKɻMmf}e`\NNyXŕaqm*#b6,Dze[:/s uod;qxPxԋ[W6;Nx)hgr]٢(DDOfhՃی٦_",NUgnfO l3NVM)d.ǐh4ЫowG 76HeҞ}ЧچBݶگ阍rX~=E?~n?*m@s 9.,P\ c!hK׳OC; Wgɓ:GsX%> Y-,AOAt7P?A|o~1 rvG`1\ys!(!!€vਓڒyr 0]#|VK[qa2#y\]DĨubB) X(2m:/2"J^؛a[ԾRԉ؊~4Y-f5ڌ}2~ՉVLf!:H C]|Zcay%"+1З ^d}|ja<O2[ P >7>)Cy#F`1qΚNMLxVlْ):|>Rve ,'1h hv~߶hvŋdmkm#|-`Ӱ71nvn"&bh{;]a73+v7/ SW\6 M;3fԟjK39tw |L@~6kn&7-gpzj ^ofn/܃:%(7`MÛz\$TL5?p oO A+Ā! "0⡑ ;".WL\ܓf̀,G]ڈVwKd[,a a)# ZRX`i\G7t4GMR ]:ė>ƿ] .gp58K1nom8zo~1M/=~ .c/.ުPm. 9Ԗ77mgyaK:>VKFg(| vSBG&HU&;OoŅ8m.ZF`}7卾P6bcRj`;!tݎk&y~ų1,*^| ggV1t )~D\(ƘC++x|%y {,񥯜61 Qv{y?OGIPfa08 5"KXqq&  ENOA(#]f{N5;t G)m͑~[f,/6¸i#nkbeE!SOy{zb7҉cڶs6]?猭7yޮuc$aG ~!nԘ&?~_ZiYK~~y>S^|oxF= }aŰcDb .Vd$?(C#څ,O8ǣV,ق/٪}Ҩʖ} RZf> _2X;54RhZ Hz9Il{D/`:V[t=N `&g\S}.&uK~gH]#g]BZ۶jh?JI%q ͡]$uSdomY8oExce Zzn,/~Á˅kI5|#0l/OpQ$Sf8M)iN{rNJE=|7-ڨݲeL)z`~EHGYR̀3nL+nAtO{L+Ln30@o1Elؖo*VN3KO 77OLý߿/gjCjy픎F~cyE l61XZƈ?t_c#osZb&6^;9Gt+J5W?{坸]I[hxTr:xC=q7D^)ǃIh0ȫeNxp [Salk">!?#?0/Y&ZH<"OI`bRl3Pk &=آM& :`"w`#Rwl"tv^Kd1 9-ݖ=c 2+ 0U;v>oß%_Sō5$˵:yTȭE}37 𱿙miF63.ur3p(G۱{>6\  i1u !z&oK#X|qK \2%&1Df u?Η,b 'g)01+K\mHTAel I mF v"qz_cf}O]I< YF1_<8U$ģ-YiBMm,3%aKy౹TOŘÖ'/:hkaQ]qZ|C .Z;%2)Y3Xo_ `@YҸǁ2<+AG5,ğ.,'}e-3Yϋy:Nw[~†` xiھQDEޘ Ɯ>~/>x'nrvk&ښ91DqZ2+ F`0HI/'JjI&_xb6CIL-ـjhg$.[Ka VakxEV/\}W_%8huىZ|`2-^qq@ ~!e팦o\} 5 9fgo;8]) ,(:nbp Y;n{.rӛ#Z}ǟ>L(pF` ?Iu-O i2 "Pr(4h4Cw #ﶧʉ[l09ozx8kn߰EmLJ P^qYm ~ R* |+.BicumA>//^_=pk,wc_|vߩ{Ol2FّD H,ʢts$`wsKrI7(qў_q6[kNoހkDC}~=v._IINDKLӚf'BmN  Ӓ'9gs@wn&%CJV[@IDATn 'LjtJ–a ulSqoӰt7ݖ|'[aosj!hɄ1K{'}gzS4L㧫;OU._S06}D.tUk"jd8]D-ލӔMј~ET{,K7]|MoOZ?ppuWn->itF͗? FSNl5jMr(sp1X?Ds3-t 챭O Y|f`CGWfӴex2 ɲh×z1d _D\57_i !ꫣa{z†z [۸j2HWt&3&~j ~E6J>FCEf ;xN>zdz뻎m ޾:/ݍOw ~7, .~q|o j[¡ex #a0ǸisL(xDD@ Yl :M?oXd\f7&&.Ox⨌"ͱM%v,|˟kߔX'tuy|y*{GVƫȸ}޾/r򒷝[Yp~ Y&Pc#\3\$.<1K d}8['7ڷ<6z[68K }8a~gF%n`׽%_rT6S0Irzi)h8i1Ibr`,۶BG 0pP )xzЩ̖ӴEL.$IgLmy tf,+Bv,Ǫտ^v.z{-mȀkÒ1}NG3 mun\0,/M3,!e/[)Q/n%~B:pWLET(8EH28d6/犺G;5;z-?M_rrc6'Z>8cѭJ!t@$Dx4k$躑i4otc6F(7dEZ|85 XJxs[ W؂0,INm /y-hJvO}0gam~ ƙxn#(ߐOmM&t`9TMmǒ%[?D*aOoFfx(2SlnRgqVl.=w]K ~m!ƸZ{\{GַXۼC{ Տ7ߋ+1MAW6BpsW`$>}O@M.2JP4rk?Oa( ryo' rrY-B|&VW|P_T)<#{b\|[@Օ[WVbC$˅{mQn1&2%ߋq0:Lj;+9ߍQww  Qt0XΣ޶c40ab#BWKodcs>e_GC&E?CQߣMcˁq0w=(~)i|PGL6pJ>A]X 7ZzM8~ik[/~_;NoPB-[Gg}x4qPI4DCueF My2Y|3:/K`i cU'o[t<9O(G"$svHp؍-[ynʄOqM~l.*7b eà,f,# r3iSz[-5kLqyPFX(6 a4!_^;ow7-C| 7__!Pӵ5ymv<&V8Yh:/y{\HOׂ^-agə݀2 d jQz4e#`0v.tDݰE~߳pw]Mӏ߸/zƎk[.e|@9xox‹] R;l{|qvZmit5@M|2|uNF`LL9i$[@ēQjA$Zxo;%-`,/I~e~1iu5=CR/tx}N[9_62[4*Y\#ڧ%YEþѐkf(}I0j-MIc_R=yIxeWZoYm >;@_4$P!נζR73Hh^i zRjq>NyK'l]=v|Sol/_==ϝ޼f} Je]|8{sqt+bG*Ef'Bm}5r%騈Mxi63U tb-R AӸ5n+0m[+[馣Mۂ+ӖQ [K(Akwcm5lQq[GN}oY$AC.Ҹv!+ ڎ*mq|@w'mݛ:zfꛂ wnv*¢O@ϯ_ߌ)RĐ"qՑL 'Fu2\mΰ _`6dxb| u?}?x5}`ol_9~{oĿ ĹSZߓxW=M4ѕLtΧ:e|s9 1UzM|%_y <6B<>t-q22Mv[Xh#=o.'ia_Fen 2[&Ű\p#I$qY2a!.*0d99scɑ?64cSUk١q EI E,b:&g%xll~oآ3l=2e 70C7 d@-DG>*rsSrc\Z$M P-ZXƌ1㋪?‹\B<X6w/>o)gKG'ÿyT#d* @")"k/vHчN2xt67e+|Qn 1/m}+G6'$Sg}7]1@k*٨JAK旾lH>?Z2QdxaL#L"9@3ӘGs2G`Ъ% $jA$Zxo;%궄I~e~M-4}ҖxhՋ>c䛶#xZlӆJmi<$Lja$7< ,Vxx\NӼڮx3qM [Z/U_ۼA7.G<3M/#7p,81i k1r ISa+ױSǕdF~$k`Vv~\]pp5m>C>qѡ1=pZxü&xԃDCht͗1F׍vI:o@M>YeLIA0F`4LT] 2Y4ч ̖pĂ7=:".#N )p C.5lYl~m5ñ ƓAP>'x8dD\{1 ֘6"mU4BF0d m[kg~]KW/wgvV5g u[]3Lt/Ȗ'𥋞 X3ţ9B„y)~-K/:9_}hv/=i?}j/׎n?_M9O_F`WQԉh&f#o'ˆT0 mN1('G1i̛d6obqsѡ'nbH>l!d8)a#~/\)m~ o(RE}]'fJFR [|H@/ „q 8,3/Im7@6ʙF9 '[HCdYl-*Pm L%[4A쓦M C=S Qۘԡ<-Ĩmr~@$_6R!rKYਝDWQ*o6] ٦1hP\)0Z Yt9_2)9&@[I 'llL*=z|_ĕ&E? ?F{g_)\{xc^&#<Ї;p%M{/u1_9 sHWD<do? 6S0%.fhLfIx778eG0$؊Z~H,.ਖ՛2O,}4_.| F("r CHN I0x7Q`3gݶWeq6117U(KˁƵTdM嬞KLɻEmB,mxyd@Cm{Ir,pHϚ&#%ߐ%?{oh?dxp=- ?xqZ$-*Iߜv<`vM9%]p8r:ucf|e NeGN i 'NEN䴰 x  Ocb :ܖ/ْ܏ly<ӗ05[J'# [6.~ܿA'쪇WAĥG3]`K.'MOD8S3G3!39*䟀+myEnF1QpnEHZ9ѕ|^t%^˖'gyIQ--rY97i|%? uB D<8 O\|͎d,SkV{o}p}XrF"\/5rsNG`FpZz]7O*8t'̎ɌQBS1i i8xpVib҅٠-lxaqJ8I_|O'=xō)lQ}}u[lVg .5lm— 2:C9^ ~m}Ἀ4NSf 1TEr95MצGcT^X-⸲]%f4]n+0O'\ qQ%6%lRԔїJH4$t5Almzoc>nNHk22~#0l&O!ont\!Jh:-2tg ':kFJxsM <˖N SGAD ).3uԷdf)@0dprc&Ņx_ЧJPLmj8ۮI:6R_]N)f6΁±T#GA<8&N)YȜ/N ܔA%٧myTiqnŘ(V2 lٴ*v#`JF|q/!J^t8,fhe'Qg~ ԵAPfa0HȃO5fj(CC5 Mx#'\n 6>BGdGJ~W:dȆ43?' + KlF6V9Y31a/]G6pRO5F!WJ4 r2دUt&Y Lr=TF`L9-&}2ܸs B$[Ѳ i.x9l9mc|gNBx6lٸ-+#:L6xs1%ÝxLb PcʦbI'1JjЉm1F˩KNJ qg}k:2^κNYvIc_ԑ^˖ ټ$Pz8_6e smsم<(넌E<8]Թ5/+*XA8N\hP.]u# 1 /,JbNmqmp&觉 #0lf&'5^ Il:5>$d I TmP迶Xll|3ҷ/FPC9-/#؀%bXa40Q2")@FJvWer1K31Xg~-?b΂a๞K0]V!

n6VD2@b0t*lZdضdnoG'Įˬ"[KuC5 @<4QYA^\~ Y E$ɾݖ~2[o[7?(B%p!s[Cl?x\x/Lo"/[8?aMLK3emY17dOE>&04gj XJD+D N6"t9wJ>{nU޷M&Mmlb07Ei6%$D խp%RT7*r/?HӪ>>RI*%j.'"DJ`SHqmmw3Ƙ>}S]s11ǚk}ke˹|ܧ>dUFc.R9u;֦9ztihg&kb{&~^8@˺4YB8nV8pktF [1Yθtr(NcCTQd).mP/| LCҎWcK])i8 <6rx5k_Q<Z.s_zslٴQ?JݶIƎwY1 \pSN֕#G &h|cvNи%$Obz)Z-3OIS(iI)2`?pq52mɅb<XWrƏM$lw΋{)>&'2ljm_o($]N?Ճ'sr(C߁OZ~ᶮ*^.h4Yj\yFXlKhNʒ C ƴnӦdxjƙˌyI% 9Nf0> r\ ui?@@C 'N/燱t\o.㲪zuaGÁi2Nyߙ29FkJJ_ickrᅫ k=,j=R$æ>u-pkCWE ;rT{ts3W˜xs n2 >yn'^+롱Ԫnh%Ue8FbIZhlP`adWa h0h6=W<eS)99y}.UqS}“eǍ+p3W>OllpF?6Wr-/}㘺VV!oHiy{4ꍋOl59bz|>-:SFPg?,M>>XB JE2ʸDjزO|lӥaxF[HܔlS; W|G.҅GrS*<0gelw6\Er/.8 ^LuMÓ 1wPز^[] ʌA9'&;'HSis)""=\VZ)WN9uqIג*bipxspvWO6K yA&\#%c~sọmA^HO:Sse|2s\F2A>w܎MPoOꮨzHa\ :'.pe3o)^*v؛|8b]w.Ko u26^VB5jCJN_y"kcOt:tRiLAڇK۸s1$CX.?l93 ׬tRN8)_sx+׊kNYyVJ^,9j87~ܜ\G[m3Rt$ul-P;fy>g;Kc!מ{NGBo0  Yk^hض)& Ji 1N/bes>qFS=&=۟{n&<6$ @b؛@[I1 &%`DĴ_b`Ο ml1.)aD)Xm  \PΙ)h^L-m L6Cݕ:kg>Rm9'rXB c]˖Ko%̕~PK%h!/p.Ib\K&,'e'WmW<ͯl  yyÅن|Qziway‘\ ;s&x:ƽK@Ty$8u`/@>ӑprV Zυ9t<ּbOLO#W=.x<{Tu>MztUwB6]٤_ϖ~v$6mmm<οOF1'r"V/ƃbkHot+t9*vrh~"3FzڝpW>3|؛h̳oⲜMvte"h '29mSlK4ϱ޸ vr F"L/7[amG)M!5nTw!h\M>⪍m5.3>_tWnU9c+ɘD #ўOcT.NXl EG><07(981}Y|w}*=@*|ZǶZvo%Sncuo~) 5Vmg|rHxϒ䢳 W> 忷̱ngb<]l=PsHe1:mѝ*8xὮ*^.UhwX,>>Qf 3Ro3W|P 84|r}b<ӸjC'7fN\-ȅRMic7V.<; J6<4q"e%r#qJ ӎp1_bv.??]d]ta|5COɘ[lNΥ3٤/۸b=Tѥx m /zs \(j?1`Ͳ~![zzҵˡA#$P|dswIǢ<¹v褕K c۝'$ޟ{n`N]m I)DaLwm-|V`,iً%?5yt!NW,WO{[!NRl*ߞW<ѽpc=.r\Bt9װ7myD%qqdG9!wʶmkm2ծzRD`Lu$nc)6 sLx$Kd<\Q6?sr-6Ri~Wa'ցg,r8Js t"_ (cdo< Er @8[J#7$0-38) s'x kW4ᣯf^:E\ՏtL}Ih6=R5\-qECZn1z6\h- \WSad'˳,-c9~<.xX/3YPXOvncDhNɓMa's N'}`CɅw;05\9.spM9B<ʫ \1&AQܥ3bDi$tE! 3sI~s{ܕG*S|'sIaln{%IU܇U FstqXK c][ˌwlQ1M2Fm{\dz-*`skG MdII5㑍ԌMOYě@rdekYϱR<VI7ԫ]zPsbQ>ʈT8|pWs3Wl!1xEy-[ʹYsk!_|1>׀;ʹ+eNIqj ~@z`Ɂt#nXDfLP\J FF1'e)UɼRiLǧ4~^_&*Y~.fJU}s&kG[mK}ڽܞiu\q㾘Sbvu_٫&<ssnj)>mBa==Rac:u.P T9$A9f3?]F-ͣiNA8Ѭvrd0-Kp|pjXg ϕqD4rx9 kr%'`K)[s\ɡ-WΩlsӅ z,V[5HHY5~Ӑ9WP +~y??OT-LVo{Ʃ/A6K~$o݇sir xp6Aal[#^g?{}]`+S ]CmDќ FzƏ6Co.|O.,6gЇ\"+EȌg=xa\[Pyu/iN VJ ׾eغifv4jOÅ eÃ0-񶉬siLuKǍ5\91- Lso䲯bUŁrɇ( ԬD}a;bz.Դc^,˜'IիᤰNzMʆP=t_k&/p6 Bq< ؽ)W+.M]-! l_-Ɉ5۾Q-Ϡ 5U`UfG_  "7 Ȳ4^&X6g5\⨼+6\m:Hډ}•s4x@ܚM+?$j2".l_&HpJKsȍ&>5nEI?;?oԫeM>&lè8M>isTf?Ǔm׼q~)@Nl+|\.HN pO ^L6pksGbw9gl1⩗e6zT/rJ 5̍56y<豭vU Ue k6 䣍bzAu?٬MP y1J>G\8ԟumrۖzqn}[p9/W`\QCwX ŚF6]' nV{=.#|*fuG78WqeR W%6ȫ -iM|6\`Ymj.!5q17j~ e%.W9ܺVw??~?5[upKM\ۦKPIo6ռɶx l#> 7ڳeqqCKOf,}pin?W%ْ u)0E.<&ZȡEgcTyskl D|'9m_f9#ٺu(<`g} g z KUoh qd񩇻la:d)Kv$Ge]c0\ips) Y6 >}+96^K&+rF^vtdžNƯjظe߾1yir0R޾kRȭ5U>)E%+kC74h:lSzgZq5b W^Mr8rѳ x_qϖM8xz:I0Mp9Il}nCCIte92D>9.W`\QCW:G"h,nӚe`< ,Mxlc@HZwUۼsn|^9'%5չ @fSnyv=Zfm&B+HֹRkyWρ-^w۞s)uq8y s94^wl/g--49$[%ٶ&o8ٖKÕ6\FV^Q=%r4׺TD>n>'ql0ao;=pzP>=g!xjwh5#}%g%vl; =7ر%ubsz5[sc Ot2-PznXZN=z<mM"e1vgG\XM>7zLҺ{>jC7~^@zgYmƌd ;0pʈslrImw5N&[>6>+7|+j9n Ȼ/NE7:@L $j cnKݲȫ~~WguqTK+ CI0j'6)Bۄ}u sOq+\D E>]Qam2?+,#%mMsl9AIQqX/k?hW(o7iѽIGѿJkCpG^ů}UCmRukz Uz^<9A$'uHqxy l|I5R_7Z$)s?{E2laͲ6ٶYOOzﭯb|} <*wk62dylba [ml'xYm"\Æ$%p5+mz\5K2~{؍+7 M-9Eʶ06 [b縸̱j[˱3Fz$4}Q-03hi4{?ꁟa_| ܑ>Zx{oRQ ߤ=Qɿe[lH,n ldV;g}~j=3.Uy UrlR6ŸlIR@؁S4n"j%O[0+h F{l[6 ֋_s6#`'e%p0{Im.N{\ո1>s8JsZ-Njq?Dd.RqB<2<,e\qC&7s0od՛Ә8d73[//U`\Wxc|o~n}^5:-Gz1xcYǂMP뤯 a*g2y×6p`ʫ6\-G&&96܆ mBc|ҮaăFp:L8b^>! 3:iu*о }b4YZ()diPuk\'ۖggykzӼY!M5mKpu>\>a}nY28cO+$9r8qX=m+^3<qѦCR_>β7$x Y]9:װ!W)6qxHۜjp\cv9yШ61I^A go,Wy.^PSS8b-`{?/<,k2%L39>2q/#e@;|xX/k#HA_[F-oAցC2nH2maR?4`l2 2?O)aU=$EJ:L ;/ ./i??.Wݵ/ry.kv|o?g e[^žRY@)W{I5fk+%Aʖc c,}ࣷ2`n㭖]ztp54z'1XɷO1`xA< ǃj+^箰~Cg?/~kJ]3^R46)x۸ ْ|Wsxx_r>|Қ]V֥2kjWVj uar_a\crp/-qbp۶"MnɠWKl%s%&8%܊6եi,=ͯkMR6Ɋd259lӰ11lط~+^箴~s@i|~$_[SO\ у(xj3XăetnzFzدAnyhj6t &!CIRD0y+4t](j./0Mԥ g\+p.8g2(3<ɷxA4[>A:c99Qz3N>K}ycwS2c L4V':zhV;_p>so~#J_E_G55zYNoQaXbۼm0i&'s߉xӹ"iV-ɋOl<l^~Ytoܿ_0׺< xz{CH}uqXzm0h3W,p5mqIu%e9q%}Nv,gOs"ZG<زli(ܾ汛칿_~Mz{S1MmHǾ.{ɦĂ;!i(??@by~6[?wQC$L~`օNwb/;:k(d{}еKq1A➍xyRDD2QԲROzGYo1Ɯ`<"!Z"CN)/mZ,vbg;??ˬ^M#o+7΁k6~agOL[l C%4b?Лˌ|¿mu}6moyl `>0DO |ůuVk&WM͏&/NlR4vTi&L _ .IY9O SC˺4YBm 7?Zp9. 9'0z\>mb`+^.|៸} ?PwmPwCc8o:bpk o7GS44^'{#N"cߪ:L >&+d`Dg Mw)Jmko-ܧ1bTu)Wq+'σ|4\U2GDscV2 nN7 gϜɇ+f~06Ӭc3W`e 5'n&n~6B^ n_omT'6XFOs#>cqSN?۾`|U+c{ zk:d}kE>`m;rLp6W;pllJ&=3j|mKqJ$6F5We],]lRo&+&<26 Mɡo].JQiSsBT`\S5}>#exn{6xWI^u؆~ v3.~16}"ɘ"xL[ُ|__ú<#X lܾ3GOݣ[\GkEC#Pԏ]Ėm7e Fw&P*/fUu)[^1_&6F8)g|w=#۬o*n;Zѓc xQGl ,G=?77 3]e4=<<<,9ll7.K]o2ƙ6kq?Kz*nO㮌ۈa"/is 91dSh;X"gC2m>Hnspu+Ȼ}NHu='"vm Z =z7 6^yyPMa<21Oml͆10v]_WڳPXݓN;cYIo 3K` ^r޶+j-8?lʹ9ĵo:lxJcp@e@A(4vR7ʶ I>rM 1?z(d1ea rdȷHWlyT=/_ N~Ǎr+ݟࡗcۀ5__+V{*~P!wwOÈl+g+#y2>;W٤hv>x֧R߹z\ˀ"s.< us= SPc-G=NMg589jcCo[G96us~gD ʴ@Oz!8ym}Jmm~yǗye oſ|ZW_=DdY=Nzƴfa.WƲM7\Ĩmi>e:WljUY:F*{Wr v׈8ʸ6TS$&[qm HRH>a 7&mIpɹX/볬Pn~#5G_͇I~)!/n_@rn_ݲ| ?sTݬڀ'/>Ό+d҇o @n\Rf&Eao9K!W\p\\=nr p`6-!=k;8ǃ4 n̯j"dx.V`\,<>rKqo>_8?KMlڼ~X/?+g<حٰf2p}OGC "8\Kt+}2ٌznۼ/_x8\w'֒c9rta=Bsrg7y$Pcei}H#uPp@TM/o濑?՞ ieý9P&F^˫^Cx`~Hsu[셣X2e|pw&=3\w0u-}:Wk5.ϯe` sT166[< !p.+/7N>׀"N̏Wo6^>c^x#%wy`Qc&w#ۨsN8Seic9|ފjU`\WZX8@!@٧X5nX&my#2pl&}ÉC|&9;28x(59RL6spׄ'dz\99Fg~5[6F6黜l]#޷ݾ󗀭v} ZUU ᰨ X'6O/@aOʆ!<6ᓫt1/|ydM~$<'Bľ:aD]קRT|VVZC7=9e J^[[c8h+<2dž+x5x\ίlH9r |BZ%hz:yP bxl}̑\!VO>cwЙo1ǒWT?/@ӫzxz[ޫ|8اǯXҙS<K޶ mq⇼>sl@`_5f>89ʹ9vK(emUeHRsO z||8lB;9KYퟖSX]KV w@9Ǘζ|/mN`;WrrXAR\y ;\8F)W7.sҸHTs3Wƃ+`Y -3 -8%˩P7r_ q|1=){VU`|jV a#wKOsr>"ƥ%c&}`pO>S|F[m\97t8!?qz-w1B#J{M>wp=KqMm7?HVU`|JV 3@ٲwŁ_X~.bK$՞ guU@'Mvdpr'XmrwQrz|~fC.nƍreg݃_?o?,{+^.UuH`p+<z>{[>9 2/xe8+ X f|\Wee0,=>ۗQI5[ÇuoʹsK.gJ!םs(hRǫqSܴc(|$rN SeUྭ@ @?t ap0ҔgL cL8tlr, #l kp&}EGJa*L.A~&k޸ć5ypf ?ׁW`<5_W ֯-g L m@C 2 /B&bSoza6uͪllwojG?8Fxrop.zx"6E՞ +=_sXpx|/0C|ܷW^e)vrì^8NBȊ 1p^BEm X@\ƕss-̟1+z 0'!QGX@fzPQ$Zu Vu*^rX+~Ps Ca<87qUMBv|>gsJu?LK+]JkU`U^{o=_˫:/oo ~~JowN~Pewo|VfK[['tx5_&o)AI>`ᡍ d3a$WMWv@Vk~8W%g/^z] /J/uj^:R_ xn}6a?\xМ_嗁NVnZq&]΍@j9  O| ȓ?\x)kzgaɭ=[vWV }^0mP/|J=w-rÖJѮd0w%W')Ixøa[n @/-OK.QMtR_ޯ@[/T VV EmdC'jz vm :%ڞ6_ KOR\e|/^n2Ɨ'/iSeut_LfMbU`U`Urml=~ި|]mvO|G'](^B9Io\m^<>v>Wۯ~o jWj_ZYXX C۵wH_˘k<vY]BwdpjIFNlR4/_F ?m9p~+{WJ+PkݪVeWAmw Mb({ 8ꀡGeۂl $/owP|gm~YOdot;T25UUUV}#|EE/t ж+"}zgw7~N |ff***4+n@'m_ Ў~mO)ufM^8^d |: zz0gf***U}֦Z:_@6pr+~Pз{hlo|Z!~R?KW{+M}U`U`U+>p"6߸_%L/w &K}=R?O1V[X/k K\a ԚG]i _xߨ#໴1Y^2R^T| X/c5***W@/B@n6ڰ^x xB?Y :S\ <oK_R~7~U8VU`β ܅o~zC,>}KB”WJwaȬ&[ XIENDB`ic10#PNG  IHDR+sRGB pHYs%%IR$@IDATx $qXoMs 8SD]Dk[}k($KnHQzZ[ҒV(KH nb0q9pcy#2232"UGDFUzf d^E<^c'J?- [3c0c0YǺ1E쿪(b|QۏNЂ.{~[X3c0c03` 2LW㣘ܯ~Ib"I8݃N #or, 40c0c0̀-̙: 4Om}*~gcu/wny*Id1` ƀ1` ƀ1` ̀-M9@70&[&{?ziƀ1` ƀ1` @@ƀ1Нٿ7N@_> [h1` ƀ1` ƀ1` d @Fƀ1yh1눦;6 S$x jXޱ%"ƀ1` ƀ1` [Xd܆k ̗L/C_{h&Ϭ|)_/ceY_G߹n8h0c0cXT :6Xc` |o1)q&(;kpKbj|)_g/ 3:Z,C*wu[n0c0cl`lj 0=z,;)HpW_N^M>/ 3:ZY*tsA. O\u\ƀ1` ƀ1` Ȁ-Qs8&{E&09)Δ 59.fyS4MP#D#U9e1 <܂~/mc0c0`F X dzx"'[(QwRН $$9H̐SK/c'TR%> ]w"'oylo ƀ1` ƀ1` Mllm}#x&q}ijMwpj|I@!ϊ]$gnJ6kƀ1` ƀ1` g8ց10z |1 0]+L&P#*]&|)_gⳢ+s{-SƭR:kƀ1` ƀ1` a3jg@Y%Tjd֏`ĝuy>9N[@8KG|Vt%~x„_(ƀ1` ƀ1` bEo<|L.oe[;q'EN pj|I@!ϊW9%/ wo_ f ƀ1` ƀ1` Ā- D9&kQ'0RJԝu$KЄW@8KG|Vt%~ʑwo bcD֌c0c0*P,a n *E8$:;)Hp '&pʗdORJ0p#4 Xm7)Ձ{~m0c0cl d `h'A!CCw pj|I@!ϊW9N)NjׂA<§ pƀ1` ƀ1` ,^l`{d79 ch,p% 1,] %NKysIl5>/ 3:ZYѕa*Gi )P`|?}S{~mm1` ƀ1` `aA.06Ә,0  $ "E=ǹW_|s$/3d*Ԓj}gEW⇁i~V@)&\Ob)#̚1` ƀ1` dqQ-"0ߊ~aɟA'-M8K:% %1P.ބ3:ZT R"!RF%DIW>{G_rp {[ ]|˶1` ƀ1` `aA,F~+0`د,AOa'wRsuЄKZ,J[CI'=sTLȩpn)2laAr˶ih[c0c0qcY}ނ:ѫdO)9R49P=I24gBN#p~uWoF֌c0c0ƀ[d%K1UL8 ;)9*j%s%-ѡ$z9d-h΄ Gք|T<}Mkn됛5c0c0bFxX5@-/oLٗqlEdzN "E=]Mu΢$1%:Dq3L͙S>ܚprO8pwY~A_m;pК1` ƀ1` 9f&0 >-/5R"qb&qIuY+dp-pI&,!U q2*<$Jٓ>*ğ#Bqqnƃh m;$5c0c0̀-e­;c_~./aϰUVeR".BН{5廊 Y$D9Nz搩 ͙S>ܚprOV1{~- f ƀ1` ƀ1p3ׅe6AG,_#?982* A"ބ?|HQqA.i(I l%s!Sm@s&T8&5>GU${v)b_y=۷mOwX3c01b`Y };1=vKѵE13$4G?3d-C8r*#҄|8]?-D$c0cl`հhGL_ƒ&V*[5I\{rEIQqԄW@8KG|Vt%~ʑwoHq Tc؄'b飊'JML zYB|p]{+ǐxL/D/ xbԔab8aAI~;2D &gp aPңMxQL߇|~A`v+! ƀ1` ƀ1p3Nu` `౬ZȉXWF%F$K廊>=vK\Ca!I&Ije{s|Vt%~ʑwoHq Tc؄'b飊'JM!>IS ]:pow}O0'$GJ0菼cKkƀ1` ƀ10l`(4Zc?~߇ _>"e]!})uGn)kq138>ބS7I@M!soϊW9N)NjğUL>5}TR);$dzAJ5ixmV(XԻٿ|<}(BIt8%zNI@#ѽ'9;LBf ƀ1` C`@0^ rL]$+a]qcθN:$3Qk58IL@8KG|Vt%~ʑwoHq Tc؄'b飊'JML z9H /;kybư8/Rd 2o!lrr現sA֌c0c` Hc_O#x `JHQIp;)p'{S3N,?C]r[R՘6?G.}$9k≥RS1'?D2*,P v!Ǯz>K0_~sx&:DG7d}\hf߹ ƀ1` @@\;)\n9lHP}P 2FƫJd6QQӢ=c=uLoDhކ}]ƩKpENG52YtuGs̸8^ *=~Q`}󎶯l{ u<t.&:DO7d}YF3L5ЍD䮟nKЭƀ1`  [XЇw?x..|-O9D',ATȩpnM89k|STq5a3pT{CYpŎOAku&{Zyq?`']Ka%M8'Y|_x %S{4҅'J?i0cX0 96&߇..ofS /pm9o r17%T%:hG焫ix+@S `sG*X*5edx6ȴ2* zG{?uzG|xyL7=c MHsx _Iq8N zwT(LαG51djs2q뷞$ؚ1` ƀ10 ,­nc\13\`x0x}GM/ǪsgrL)2~I7˃{}v [Nƀ1` dɦZ O@mpƀ10^|egۂ=fdo_&~>xuX`itatpۥ{A]]݋cv&1` ƀ10 (ad_~3B0c8 t:գS7bҿ5=~߃>MH/_&">?Q%go֌c0s€-ڭQg??q0Q3sKOāc3]&W3ݗ7Rؽ5vO `m f ƀ1` ilL3lNJ?x.%<Xc8 Lʹ3#x#?eV##x,ڼGQ˷˃ʚ]=mx| f ƀ1` [6ol'^|5c03O?;_<|:o\'ݣ`4"G:y?k><+;iA?Ӷ6a֌c00` abƀof/ cL0ʱ}}߱eg_IGlC4km*=j'>sӈ>)p/ܴ8֌c0AA2&̾x d Ce`z҃Oyzrz4G{X.z } &:?s)Y7o{kƀ1` @/l;-X'of/أk3>~w|u &Wݏ9.6joF؝^Ďf0c gLa?f86Hc`~ vbf玮lwzg´7%zS?zeߕf %<(vG>ǹR[1 גgףz/ё|enSN f;c0Ȁ-,jCr קofcb @/ykvyk/u_?pAf`3:VyP_ʾ=؇O ڭ`0c`a0Pb3?f6.i*qUa6-XT%~θt2Maȁcr[/ =m{K1` 1mƬn+emofW.2C򕤶iw|=>a J D',AO-G|Vt%~ʑwoHq TlV1]HrQK V#O|꥓|!@,'} h] ^yeu6rG`BYSb4+2*1$ q's Jwp*#K}yDWh撩}P&>ǝg!]u݄K]㇁#;fH{p(ݟ}QΚ1` 3` >çGqafvdqI.>Ohٗє7n ck8KfORJ0p#4  O9Zt#YGO,4}5?)%rgYl[&T1?eM]dOI:]bIq5lKj BH5sRjƒwA!Tsu6/Rd 2_HjN褠tήӄK1]㇁xKBuh)Ȼ>}'1` 1` #v@,ʮ/L*zfT6-wk c+D',AO-G|Vt%~ʑwoHq TlV1]HrQK iuʓ?w'gN:xNr]. ;oٺ퍛 Z5QW0DD&"]*.%:l%s!SOF3LmCJ;j?<ATM5~8rc{R,XD7܉}曶OKc0!`]sb>Hk^NE_gLm2A93c6P9. ¡'[{}4CqL+U4x+@S`sG*X*5u4ck{}q裲(cˆS8AdAvI r#+esq)>ZsBO :PN1h%?K}yDZK6 ?ge D:E z.U0p;D)vmZ1~<§>M_c0ui=c>DƅFL-£gLڦe_.`mxLx%T%ϊW9N)NCMx*KIΚ>xbԔj'Z~eqO Cq;ٍK\x.HѠD.#APǁJ`/-(`+>IAஃKDisO3L#иF3Lm2 %?<A&\? 91D)mZn? ?%>~c{c03À93-10t0I?KBJñgLmӲ.`mxLx%T%ϊW9N)NCMx*KIΚ>xbԔ~oy̻1a,,H|=oT]Ye J8I Pb4+ceTbUwRE:G'2/9 \2W&2+yYPĝtxpk0pp,XiY<9wA҂ݟ{>c0À- Grg]_EfZp[J\3Criq~@<&BIAqd*Ԓj}gEW⇁i~V@!&nC8^ΟAo9t}3m'gv;λ҉:5?Zv%^Q.K'[P\c}dDu•'"ezL2O*ӲJJq']sQr Gyif.\+IuE%Ub<Ah5&\? 91D)mZfγL#N_n0cp-jeO9tNGrPEw&8dL}݇-БIx}Tk~^ R" կJr>\6J< &< U]HrBItHtK oN~ H 'Gh5߼ذ}ܿ+jJdփ`B>]P!\pJ%><"%SP|e"T "xp)k0pp,XiY<9ς2Wiq/w޷ƀ1` UuM21pn_HjN褠tήӄK1]㇁xKbڦeh< x+W c_>ۑkƀ1` ,J5΢ zg?GQT\rƆu'E9'zOz搩܁iܚrmHLx%T%ϊW9N)NCMx*KIΚ>xbԔ9ᓳə6ן[Jcᄈe?5[PG? ̏z8&Μ>8_4SȒV猻L!AwDpk0ppKbڦeh< xk /_C n,H w'ƀ1(8r6aO=6?􍺨"JԝuD%˒2w@G&JuLRy*H4W2*<$px+*ğ+T}v# %ё#O,)N)<>]L]r#{1~ZOń#ysi ~T5[; r(q>ǝyl ٥Lm~eI+s]O8ބK?^;6-G3YP6XC^=Rq(|J߿$c00gц6 `,?+ť`ĝu8& Lpr ZQѻ[# %:&i`D_?|ly~Mx>䄒ȑ''̞\);7N&TuW/->՗fTjaQF%FeTbIRYs(#.B9:)yȩ(Oed*sKZ#K6@ %ƚQ "UM8çk0p;D:@A۴|" y@R/_?%vЯX3c`1` πg}?N/᪡TNz8 <w S~TiٗҔ7n ck8KfORJ0p#4  O9Zt#YGO,&V;Tp}t'$ºCYIJFV|9[O+Q2*1$)@ѬJ9QW}DIA!ʼpWPNv,ki&<$rA{qE&\ y0pp^;6-y+@'`~;;'ƀ10 +U?F ` ʽoJNP;BeLM.cs} %?KSK]r[RğUL>5}TR);$]qdiyΧL n ǁTgu' }+o/XyEŨa*Ye]qhVF%ʨĈ>"tNJce^8rC:k´ڐ߄gI3wן2*"{3tVi¥i3d*/M>Љvpc_~?' +ƍ7^Tn5 p%x*#'!Is4᪏ꤠtΞp^8r6ruײWYP6א%wB>#P7RGaY%J`mӲx4sejs5eeh};Tҝ)ƀ1` 8FX+o|Cx\1ʍcl00.Á@}3.tC+sbŲu%c`2p,}i o 085|6๮7g;}_[*`up߲yE%InWb VQl:m}hw f 9e) *GqbQW.ܑȌc` z?t1` ,@l`QvW.-v~Fg` (3WxӭpQD= ac>(n_늵ްX|I{Ūc(/ʹ?Y70bF`,RN'>tS\oOXRbXn8_jc11ᩝUE׭ C4o\Q7m,ho0^ ?3_mzݹ~y{{30QbFh,ZH'K͔5kNʉbZ/c`2pxB+7m]W`n 1` 2p]>gZkpˢ⧮N$rKƀ10 q.͟œe|" Oo \0]OL/( |xD >;v8sf vYꋓw?5{#^˟^ە<*@o^v5c0z1!W3wJ:TWN 3c`D9 c|X h>" V9-"?>!p |b~i Z3S'fʩK:vWE=2µˊ]x+{CĆ"fo{fw>4OB$KTLj1` ʀ-S",-ʐ`t"|2Aߖv1H8~I|ep+pkƀ1p&8>9ׇN<չ.nM\=/m_Wܶmm>o0 <)tklQ=zr",iĠBAf1c`4<._Nk/8<::?H! u~q2mx'Ƌm|1,-7Llg sd7˓99s=xO7U_뵁ۛ/ZU|6Vk21` _M~>_AXKOѮbM2c`?FcYar^Jŀg00=SȈ3SK"5/naEݣ w]xㅸ'^}hc0q?lx%}Rk/%bb؍jc0ƋjLyr5Dײ8IARC?9à|J, aY`I19Q`Au+L5c lu^ '=>qb^/xqz2|}GIf @/^O ~bfz߉J lRQSq&ƀ10& q+ΓtTdtBX3i NwBļ=vX@Xb ފby-b!<@_N5c`12vzzǧ߁O{_=^OkpϦxÍc`Q3 i=|s30pԻh811ЈIT&cil`ܜA'M2z1\/ktSy;&&&vTkI|\n4n :3>5{7{7M{:q'vETb*vQ)]2&ƀ10 (1-9%j%e$C^'R+ D2$;]!NHc9xKeqx,6x֌aO]{lx+A4 &qx57(%kqj\6~L9yb+.%bbPn1*ƀ1` 0Gi kP:&:k|sԟ]D NwBļ=˜t2m8o PrQ_ be8z䠯 ޚ10 U7)=ǨwĮ$ԟb.Q.&ƀ106 1+'H>Gv0*L٭*x1XG\hʟK 51>'!a;UX>! q<;|S+bIeLҵ'Njپqy}oP\NC@=ީnts#&[U@vG\"&]bNZ8ƀ10& ؕטq+3?! x& @u}?Pl߄GE jb'L.ªEK O LSe|Q+a_f OKӝfJL3._R|҅HT0ƚ[{TboM}bo0AVQB7&ƀ10 q-~@IDAT#lwYV:Dc]pW\D%BĪ}16 wcYO W*ڝ`]XxdNy/_9ӭ$r_N":bKOқ.[]|'&k;ctq41` ,>wq$5 F{O1YS.C.({ֳƀ1` <0h< F:Vd]A6\I~C:Nˌ!& 0NڃҾ[qi#nJ CaXضli<#r O) lN_X#]׎}d𼺭S-@Ĭ՘ǖˋbۆcM6@(f:a!xjUo]SK*1b K}c1` -q*I'\Ya-$5y9S]C/uG8شtĻ`VS>X n:mc Wg:|SݞqZGS_3?ޱ5\m~?M53׾<ڈsHsdKy?T!"!u!ψA춥Kc`:\cT,N\3(DdDp Y +O,8Shpr;?HOz-&\Ɖ?-[dxBy,Ã.Bޜ6 ٲO?g0^ wr_ӭ`a27$Ѯ2KĠB#&qƨ\1L4c`><[\駣BӉ #crβ$a6Ҵ}cH."\4csKZSkX6AWXQb%/lO75:y>WsDҵˊj}qզ\ 11` ,zTν3kqxjFގ"&4Ƿ8'HI qv1fY3c`\q9RcV;7ݓD-ɘY983I 8-3 K>\@\*nF*K<~8V} k-x;B'‚ 3b:pܶۏ/Gf;/7(jx'<ϧmm+?yi"a`/OVbޤޏ F"E\"&]bNI}OG!;01` cӝVi[swpXH4Lf6RHɳqx_~fC[dJzh+3lǯ>pչ޿Pra_RbFO k+dmkobu`1` UGl9)n:A}'I?(AٓZ`1q<&ƀ10 xRS$'<~1 Fj- tt~9UC"c2 Y7h#MVdZ|uyKqk[֬زǸ<ŀ'(pq ƌ& 엏L|T:W M='(~u$95c`30]lrvϋ_wRi\01T)v\_ҾTk߁cl` 8ϓ='_;u88,FJ~$=kspNjb_ڌ"z4 OG6n,l2:|c&̛'O7,Ym ëHCc`2Pދ=վw AFLQ(cfX_*1` #̀-jΗq臥tcMFhb#`>'s?HQ.TL.҇+`ytjbȵ[\/.B郓cW/_Z^A۝E,<{USbz@r<8._W>QNRWu.i#{ե?l@6ϗ=#h#My[wnqXCՇ\]a\K]1A. 㘜i_lG0 W̠m* S-fvɓSA%ֺj#|otMqզœZfng7z$l &'t\_7?+YQ.r&q =Ctfw`mk 0` sƮR>OɒN~1eĺ>b@tU@ #4G+f,k[]?$r16,֌c`2;OwCޟ wq*vUuI1h*aideX;a֌cUl`T̘ץωtbe=7fc ~bg]b$5Ss:B hAdiSS!\11B؇uJ>?Mfr5.+z/NMFbb҉!u`y7~l9 6Z씇_WMðsP1?kA&vL%%v+ԩ/P7Nj.\+$YuxHQ.TL.5},ΘD&8"2pKyuk_xdr| pX4uλO|#infq c}+Jݫ..muPՇ;u*fr|m\&pxLy8 W!߈ Ssps3yVV#'T*P%x/ 6&[>7)6+S֌c`t8)֋';M^&. DrQN6;c]tn9Jo3c`t=6c_Yr tVjiH{ՂX=I!;GC%?Ђ Ҧ.C.cc?~\JK?hi=i=ȾAFl*%ke1xW]'6ĉE|J`)~p B9F`_yrvW^Z7*=.U6(%vVka8@ F/l͆UOf:QO /‚o1` [`7דOtn@%xTyRqThd|!FSZ{&Đ5cWl`\܈׭ϳtRz`M!`^V}Z X_Y5y1%_uxHQ.TL.& T}ru\"?d.Y]KV.q~4'g:`Q`8O ? 𵁩V8b\Бe[c,1L/=.Dy$ԟ+1abP$>Xˇ,WX Nkƀ1` 0~l,*N:{ ׺tZGzhH9UB0 :}:E*uW*1.#u~ZgYYUrS#[u3?k4ma85B>_z_UJPi>%EW-h M c‚, <ܓtd}2|WivG'~nosHZn$JH$~PN_SS{e];8Vemk ax|ON>ԻDtlk9P|O]cjrq..dc0Ƅ[5neDR_Stz U@CYŘO*[W.jƒtN?iC]}TH]r_ȟVp7 vc:J&U[ ).Kx^yOv͉w[0 2zQ0A;!b+`,t;鸵:)I|]b5UvsAO퍁fdkw"”t%ėTh$z2tðCL.vJmAY3c`Q>:c\K ɳ2{#ur.;Gtqg2KǐKAZa1@ tAZ ~/pr?Gu1=Ub}K']zbۅof?S>yls : K[gӑ#ofחWSꛜ"SD5W̞kߛ>F,F%yڀKZũ6>PX8`0xz;5E^X5du@NL"C^'KQPq;9׼]{1` À-ϱJsR'nxtR A1;&~;6byYQ_NVrRGy1%_. ]I*Hǐu -v븜c't@c:wO gn~1y%FU'qMWNk#=6UO !=7=Oߍ:wIЇY%Pbc`՜>5/Y|Bolzv8O 0YOw <=7S?>9k_G_N}%S1ДĠjŸ|rZ DR 5c`,WIcY= -0~'^zrch9Y3iHG'ԁsk]\"%Fe*9P^q=mkQ:p1u9$ EAA4ǔe2^7[+DZm~ekM\f5ʏ bc%-\tdה=m.ѓ ((2$;]rB;%l5zJ~Kw{|lqkkW,61˩L߆ҽH※,k0xbFǼ6a0T Y}?d!X;Vsn1c`,8LW$N\4mKuQt7D/ ~[mɻW\@ t]+*O*U1J췮Pt}X&B'к9}/- 5WCywۥZ_JK}u[ BNYS}?n@r%uTSu^zs )Cg1An; HJ3>pAIr9tίe?wy//B|m@8$_EzX3*g;2|: ?;*/+ DrQZ{?:9ym,{tζ5c`<8NcW@JNz1 :1I*8e8r{Kq(9Je[?0m+:˪.Ji{]u\KE_փN9ȈMj0P\bk"oN%,tM&F6`O¨Mk?Yv9~УǶ82p,G^jώe=&>LxD)b,b")qzryT2.j11` π-1 I'H>Ub21i>NϏJޢ\u%!&Y ˪t'@SjJvY]:r=!@i?`ץ$)% ~Ⱥ ?Ui\s+ӹI 7P!1Z?Eu1&b`fyOu!%!i]On\144kO()sk]\}b6*~^̔C%?gōeHc+jcȜG嚈Xt~-sNl\1ڀ'ǍM+WA|r>}>Ƅ>:O2$;]!NϝA!Wq`S%ju5X0ؼ =F[_Lğ@ _ެ,!ט>Ũ?mo 3` ~ƴnE:2&5bd|nc%?}$Z``q'Q.f,&' NK g-cK]`Jݫ.} s]29 *^9ڟ+]5>MʡĦ(p~:NY>X4m~T>怄~y}ui2֮ZZ÷6~#Ӹ`X{ nV==? L?=>Vn^+F rQZ{?:9Ey:.I9. ƀ1` 60glH'n!t.II'Mm ؏ϰn ʝȄS PZw1B]NX}Mt=c'fD |^ 7/8 )/P]dh2LWH)zRK b>'dL!İ.%4~R~AD}@M\q ihJ*MVpOlٸnfc?z.|F=A|ڏ MCc?|u1eJ1aR5ob~keg6*~^UcH./ĄO XPsboEעe}LX+ ꄣ\9[c MT.%(HZ]'QYq*RVXC]"k!;WC]jRzl6v:J\v.f6 xb}qxme1Ȏ <bT.;c]&@unǥ"%Oc{NTƀ1` "$1YMc̀?5҉40Q8 f)s^ga>I*['sA}l <~]_Z"Tλ5ܷJc^/u],F\Q'\Ue9so{NǤÁ1ڠ~ZgYLi>#2}%Bĺ89Tҗ~KLiy !,<-,\ +0~;d7p <0վNv 84yEL #ItkQ1=`5v }/o2 tO>ZI50QfFqm]?!/Τt2mCЦ{q>.+-Y ~bg]b򺜋K✣>u ~$d-`^SWu%&],$W|?oS.z7cdC;>Ob<^ uUȥ"wIQ}W]0J˖- );gq,DZx=6m5}k: J+xp\>W>pQ5eH.'D,b,t Clj)t\αKd83 dI7(%X@_&Й8-ҙ>F_'\(- z$jbYMCc?O3Z`eɠ.$^/.'~mh??1{3f-)/3ȿh9I=w&e'$5d.5vƺ)E4%{飂uw'1` ɀ-qy1:C^'ڷV@hX3وМꮶ.ЏލT]W0:jt 㲾}89~ZgYYuq b)1%+JXt-Z2a[6L׌PjeڑK:: R^UbEWc>))(CEP1 Xr׋c-^x.iSX8?}ީuu >{TiGN"=6hEy\>3{jSOv@9lS : NS5%vvƀ1` :0GhL#yeИYGp!=!:]9,v|=cD/)'cAP2U1JB-!ݥZX&But]?_/- 5WyywG]R8/%X?'s\:qCA'ݪ,Q/> \I/jmfoyOuDaYbj(W]އoHNH+F$1(LՅ\._6qer'q@p% f1dh<ft 廧jPΦYL{b%(o vDH{xh%~M6c`D=0 nh:i8E!'v ĐuL{]_H1-Ǿ!~ 8J߷K9|?P*9]`#!kvCוieUN},d|{]uGR?=Q 6 2 d[$&9VWaclX{_:TWWWLvBb QBH~?@"!!!$ !؉?ڱ.w}1U>{}|1箽9)/.sy1NP}Q{wዒGѫG]ASp^?xWNaƛ`i?oo.N;q-^/Prr}3"%5]}?DTKN/")͚][j1Yc^s-R#6cQ/ ^R!@1xGg{!#x3Hm3~9sg~Ύ-q5OwX汭0(ϕ<>ޡ˲\Ol#BO%Cz!ÓĹ'ʕ>Qd*z|6S8ysNHg ^qO}qC_ܧ)MM&^65bkf<oE"r,s~PFgE0qs_jkEBΗ"54͸/"<|Zjr\O~{a۩A5D_yݿ.(|w w_=x>SSXo!WWꯋ}k,{y. %J"rĩX86.MopYrQO-Js0ǽ5qM}8~G7/$ +5# gVXDA"y.%v 4L/ҖbVH"6ԟ$ڊzk-Դa E)9Q8+kN&};~᧾wÏ^?w^> },cL_xo22K/G(ixxGU-¿-x"4a !e_!6p^xDn'ҖUuN9;.ҽxtoͳOnJo8.7! X; Gq8ۀs9]5qPNk\Vm`$11Ef9kp3+Qy35<`z`Ģ}jܒk<U 8','p>x^nrM ^inÁ֜pą<>m˲\Ol#BO%Cz!wG(W@DmʒyӇ gm,?TSIR3ԗĥg՛μk`i]ȣL+%YKNm"kp*·\x; kWjkEBΗ"E$(_̋Xcg~c̽>۠~L^5y G~_w^<>9!淬9+ͅCy'o3&x%Mhɹk,94^U(;&8'pN3^{o6'cO{ac^ph"}1fۙkDŝL M78' g_F#jX5'km|? >s0ǽ5qM}8~G75y|9ڱ5s{4ȼo$5,ԇO* eŇƨ&1 θH/8L8kPXii_]ly1.t\}E[aˡp,s;IQ? @_ɟ;~R>$?S;o_|կqۯ&CWK9g#|ޒ> ~I %iG s}N957rͶ[٬62B4j!(!N Y&X޿vZ8ۓ\]ǥ_,qŸy^Lj̼ʾ̨\礆b^ĮY)؏E[R(;~dM??1q,8&q>9RBm(_w#V‡qHnij4=!sa{@5-m`=(.D$5x_@sPx^нC\+l3|軏߮\t=mMDQ-*Sf]ܒZN\YƋy>'pN3_{m/ 'nt/-!iN܉Y$.TcÁ볭9D_Z<>6/rU< $ j/Wk|W c>jq Q#y8'{kdp_. g ps% ZW*. <7y<'pN=M|pOWz(nwZζIJH5hBܰ3 Nj59h9&Y'Iv游@+%D3NLJ|I8ƻl;C07Kjk,O?kI|N9..57E9[t5 82MbA'3G q67ˮ'S6\9G:yMi/5ևQg-C̿ Ͻ8>/>zo˯~ .cOW}oǿ|FKYguHůpH Ɠgn㕳#{u$^"Mx/sKBI0r=ʹ 8'pw8%qč4 $IfvKkТILt#ҹ$}-Hy~dn}YUqiJY}13n8R֗pR k0j/}q䠨,"x汭‰.y/ NL¨>&2;k/\ lDUAHj8/̅M5kE $h:F_q>K ؐw@IDAT%3l/7T8 c_$cqޜ1h೔$8ڰ3t+Yp|w~ǿ?F_&_z៾z_pO7?zu=kj99;$iHn/7xSB[}Ml^ް)ygŎ!'ɲ35$1scl#NC,cr|6S?fx 9g /M:!kO?K+YXuُ}mnm}T}(_#>e?o ՠ\% }i U}[4qB'߼ G=z4+Kqٔ CDu<6B/}m`[mv k?~3W?}6>/Om;[/~ ?_ܬwAy@&o3ÿ>B^;ů<,qT6 r'58|9ssk|_7 oUq2FVhq752'FA7jF|b=Ix<.a;PgL5[m)α bNHQ}_PVډSJ?ˊ }xY~̹1yZpW|I_>kpvWp'unXyj/+,{|=׋ߖ? r_#_>x74/}j{'ڒ KOOB_B#FQl;%ExzWƒӸ%[zZ\bP^/id4 8'p8U㶈h|Pwc{EM4M8G00 [4T3J!µ/疂1Szr‰ܬU X.88a6ԦW17o4b} W_ hW /E,:Fg-Vl$L:_((0kE .2x+qW9/ ԰(4]&Xc_m̽3K%72/$4q}\~aE\84$"}束/g?xou+/s wsQq#ۤP[ﴶ/.6lLSM_[źo\ck*<T ;c>5giJR5kx)}q䠨x汭‰.y/ n#QŠrq{Ag1/BVI<}=8g.lE \4k@Ҥ-83T/ /7T8 o#'d挙lg)Ipag.芇H\fs-9є`w~!7~=W/?_?>=Il)tpnqo8-XKlY{ĩʱ l㵸]__%:K ZX*. <7kϫ`q x86\$}(fiA8pPajkb[u <Ͻ>੿nV_[ȉ@jg|SyElY&o6'7= n;$$A|\j]yXQ+snz4Cʫk\  =ǼFZq~37P5MSVHLSGgij#\{^ci8'pN&p>kvǭ7ڰhwmNIH}'M.v Iڇ5Akk܈I=Ƒm}y@8\ck8TR'}kθDG[_ZA5*b/(|Sꇃ0 ؈ 8 F5ܣ$V1S+,N#ŭDyUpbkӪѿ$ĉ 9Ͼ(Um/3*w%OXl F7taQv;٨<Eu3& ŁM64 gheM\s18 -9_z%G=+ eGx$+4* u]>3xN99.w1}3}ijnsLk690Qq~Sœ9w⦯\&}7h-jtaFa$U"xV(~ q iQCO_;FhFdyJ,? VEQ.5R4Gowr$=I1\FRCwj:}@8+㢄pwx1m_ڎ?/q"!gK@96KCSOጭc;jv^ˢI-}mp@WR,ZKg~yc+O&ABHdf?_ԮM<8'pNn&p>Ku_ηjمHowpq~㘹Ys +YҊc^wrIQBDO鋑\ 9.a;PgL5SqPsl~ a 7jOd;Jc\P'1} ]X_Q}\(YHPS@zX0YrTCT4H}e77{JMOhk ^?0{ _kks/+ju0@5(I 9[ \:W8'?#n99nĕsQс5P4ۈWGSxq2C몽u@YuN!N9sC7ݹ6'Fb-1$dMEH~в؂g܄/q?Ə74_sYΫ N9ssy]+'>y :p#w> 5G.vԀ$3Qۋ m8GBM_[+1GC3Fan)0O2N}~ڭqDpށ£j+9 Y y@Ų,З8`Y3$nI_ .r}Q闆Xٗ[{%Ŀ`3>78Eo t\mz[΁QǁƟƱZZ^߆-U_G# |$+;W|g$n} @Ʒ3xN9g=<\@~{U5 sڄ0/mR n gjXվ>ef_b|#ą<ڻn^8Db#)J-8c8g.jH?=2m(GhFa^K8>{4%9'/$yXq fr WM]W1=Y&_oh9E{4@v}Um25Mయz`qۓ/Qo:U}[4Fi/$+ͧQQC<η&zK_q݀dą ns8(]pēk ›˥EOq*fU>CW29!! GsOC5:>GU9  zMe7a:lUN7$5 8p581/<5#DA4x\xfHq?@ NZWJ\_H[ըm|hHJ5z=UpXrbϚm( Bɲ\ULl#BO%d!x9 TCH"x$s5|&Y~k:x 9f':}-8ɫ Dm#NIp3Gv G=^ciܒ-7zZsOPhGFe[u{l}SF6NU< }mu5^XqMn#Xm1 $%rAs̃—^;JslWmB[sf_F#jP58Sx(+s5qM}8r1 `BY̴{MO *=@*~)_r^w{HVm$sEUr[d|ŏ8'pN.&p>tM= %P7ϪAch~ܫM(GZf}&K`朳Ź/3yNnS@'ę3f\ڭ/TUxP@׭}emVSԞhy<ܯm~D9~9 Gq8+ }qS?3Nw }Ze6D.:Mj_ .r}Q+ b^nTG~ٵ .l9oΘy|!^;qnsqknAB>#R CG\qpPgW$n} ٣|N9g<38rK&? .ou,Bw`32GxR%΅pgQq~ÙֳDf2G{//CAڋ۪ɲ M}.WaB]L*dR q.e &!bO/FW6uhF?|j 5Ɲ$֜Ԉ2 4Z%O4/.6Iy5<ׯv uom7аsqd W[~9ݼ#Ea6Mlþ|I&FZg^ڱAd憝DX#/0h#H83ߊg.ӜW@pn<̾y/V6{x{S&idXܺq֖6q)f8#|jIe-8 [N8Jfjx 77vp$ᦪ3N|.25D#nfNbl'!g Q:i >m,?TSIRt$T^' >8+9K d۹M\8ZJ̴{0[nZ lFjz>УZ-[rG##{ge4F qc\z࿖ y>'pN^n DDȾyCq GR7hCF=HI_%6Ϧ5#q9 'EXm!#_j%ŹԜŌj(Fjx/Q$ca5}Ik>ʭ{tyf\=x5z؎X;Vђ{h\諘@20|X HrjM8nj}QmV`_̋X5<3قR,) }@"vE>%/5^S* %:@s==5W3#:si:-|N9~]uK1pUK \a\@y_yYE]l꧿)zJ˩/L[5/~œub[g3ĒVuG(W@Dm2pX[ĶM>1(uQ:Զl]^s\*sN3ɚ[ۉ (W(5Dm !~Bu֎bhKV_O{TQQKDh pkG<8'pN>'p>j6wBݾ vA8I7cۚp^yR|Vv8˜H'8xrG<$Cee.Tg87q 9MuN\ƾ/0b8Byܥtn4er[8p "RkyA"aGE[H&0A<3IF9%DGY2k-aJؚk(9s5먎?r2m>e5n*{m*b 8-.&nr?vXO?UYh-qKh#|dGǹ. 3.pD4!(9s0=\;1Yݶ!̉WEơ(@Վl7Ϊ΅N_+D9Mi$? !iيC|GL&a`:~}ysƜ+ưDXq8sT{CRm^ץ~Z_sӗ0Ϙk>*gkdO˫Ɔ8(oA^U(s+hND1]rNi(s x8Kq䅼jiV+[ˌD`_̋X1DXĈ[N Q_]g]yXQj*sʪ=!x9zw9O@"s wrM Tۗq#Vߩӗ8o8P^~ho_8\YLah_ YaJ:KKb9>Єće zlͳX|-x[|%`Cm~A?8B>D|Ml}[EEr7}o|c_pƯY miAq!"iQ}ďT(ިt0m-,#ɴ%g#Z6?_ۊZ^R0'rԾw.'nzOe`S( F@U{Εl-(xlYr+Cơ8˟9syoq_č48᮪]!gXxOxU7jr No&/Ybiw2\⾘CDֈB+yi;|I&\ļػ؀96q4i΋a qPsl~ a9qiP a#ةEji\ܹ+EL"O56-HZ 97乐zFTS\p)T-%(X\ԐpS$6G(- GY2H[id5J'. 6'm\8 Is%+@OLp]KL#Rڳ7]yrDZ.m9Cf89dy>'pNL|p/W{#nac \jMlfD@s~W8jp.|M#ї'8v.Qr[/Vǘq^VgF@X; *I}<8'pNO|/=FnW򄜶u*sƳT}&~}z$;$e\U Hin!B =s8d1'j6UY#o}!98(T4iG }vTr =9Bd^iRs2G9@̅IyF}5₣}s/GeRMt'8ֽ>NJP'šav4"XO10[O8gЩ#m6\|k_UK;{gzhۭO5uy i[u@%hQ٢>n%}n;Lz>sO#RG!36(F|9swcz_#A;4 I%+sb o @@5Nh4{8'22/5g0S 8%azMHLMS愣6rݘH5%86G_s8,`cuzxWz}XGۺƌca悐gt{xɲ}DŽהb,g;P8ɲ35$3'1V?8D9l<ʒ9~f |MWdSIR3ԗĥ/$A=؈2/%G+ eWx$+4jϫ`n@GdrΣ(@%+s wqɸoyˇVJ=97ZwE8As0i+X3=Ϧ#mI ^@m|MlMm5LDm/UنS5`-8g؟3xmA>!Nj8K3WUhu[" :N gf!O*I8ȜZo8GO鋑v\ <.9_;Yj}89+HQ}_`FaMH^M}q}qOL, }O}G_mץ>RKFvߋCrќow=tކ^K^Yk8> x8< t=7Ͱg`Ʌ't 8m7w=z9x>М8s_V󓰶pvpŎԗI}"BLm>"ՙ<Lh)a32]]H}QPP'͹Y'j425V mJ=`6%-$57W,%5?@U έ/N<U ^E_HaC}&ޗp5y־JCˌzu]]OCHW2sJP2l}ӳL^ .?k4ޓ{}%4e2FG)ql'L'gqsi8'pNN||]w!IR.j9" !o&v# !s0\ijo , ^ATx&66D+qNye.t܆CD⃴ p:/&ΩG qvӇ3Sӥ9ᨍ7\7&R 1CB)idgH{B2~yC܄BKSFvVi 5ȇ>%jkn곯E$'taFa$U"xV(~ q iQCPyl'/8rFԦ,:&Y~'  *IO\O2󉻋#!޷t}0CPI %Zsz|ϭ%18_YOWP[2k?  ZU(Wo6;(Tv87 &o˻G-9Oj{_jpH+0$M!bmn/לlJz@pʟyxB_Frir6⌉cθ|@kRm^췾>Rw/\󀁣;4"kq}1n s28GBԇG49ML_0FX5'k%Ծ@5W{wcĢK>ȣ>hDkҖ+&l wuϖs׌Yk&gM/9sgVwi+qSUG;U?D˻G!5ߤrF^#}mfEgf'UW"}/1O!K}[pяj;gs-h9ykOim^Fy⚚#^C3 C3/u~6,ĚSa]Ua5gu'1_o0l%.u1:h/-f-aIpk5[zSjᘻ[5h/\(jdW"yq8#m6\|38%rGq>Z qK6]\s9Y͕L\{뀊7EN\<ơ8}ϬAfO9ssy]qF9~njG#pn|`Jؚ!"@*' (+/p jEJr>Z.Np㬸A8 `%}ĝ8\j m}D˕Zk5 '4l dj|1>aMBBܟR_]l3 ~M(5"u6u8}y@8[_Vp}zלqI= Q-l< (]Gԗ@O)j'pNY^o Do;HM Tp g[54$iMlc_mǫ/Y ?DB_4<"H{¿ؗkΓs/JŇܣb?RmM^+Lc_hZ%m^ dlt^PuU-h=;5blx7qNNxNL7 Qmq^q.8yiO)|b228.~$J?sʇP}_`DmFaM?F_ZŦ>jKhC0},G>(>z(۪u@jl_]lyr+h1989sxg|q^If7H~CR?DC`pvk_5PEj9Ǡސ׬~rJ@O>3xcD.e5Kfy-Ε4G}qs.ͫQ*qPR7ЎP#m! &6z&ސc&78Uփ樆V:-@y*zsuX?ŌErk_/S-,j}ESj_Oa9Rמ}p}/E?G/|,9, H=CT=+Iw-s rϣ#nKCz1[0J Nԓ՘oh2fVPl7^%nQ_t1hkn_ @gOpF5x^ 051V +G ;5E_0Og_VQX kZݰaTmmً~$LVO_i> fNiNp8*~u* 붇OQg 2f_RP6B VhoL,WĊڞYԂ!' 1iD CD汝H2ʕ>QaJzj,'$O}>dOJfBY:hoxC,a@ป}şƁ?,Z< y&gē-pg*u  *a>}7~oou%bOz6p_&Ol&&}6 gp+8GiO\ryʦD_  N@GG!^[UOkͤ B.s/[ۉ/(&tp!rT֝D"0D2sp$N&)x+ϛP|Vvxަ~␐~p"~hxLҍCq _=s wpż!Ȼj{|3H~Eج'Wjx74{Z1h9705k2Г? &K}Y kYO5Ź6cۚ6}z u6j k>D }1v/|><L@IDATސg&7T7 }rŵBsҎ -棵6^n_́a6 }'0WUs8Ț8|\c,fe[[CCs)z(!50[i}gY*HlPqFSox= 8'pO8պ^qč5js#st$h <#+'wЗqe@jx_&eP^gښæ/6^} 0Rdnk9ڗ~d;kojd}1ih_D1If4ȼJo\ >J]YD 9^sabG F9wM"DxGLM_[Uy'+Ct!q(3⦯%%8u| x8ͤ-l Rl~)@6 'Ϲ/K6JVN"o8IR$})TC^peۚ$1sl̵xVŃf1Df梆D!"cA#N+Q۲ASao 5q0doz|w&FҖx%3W\)km>C4\NA{'tB x}|#Β}N9g:30m7ޖ#pN&Dg?J'Shbc_mǫ/Y`~0Wq6 |JɻؗkS]:Nu G= - R_|/;(x|1|*~S_-9#{sgFjccZ:s wrMOq*\9X c@'xۀxDԕyFy}9DO/sLU`H &qeP^~|17RVˀ~m}1lxR_оnqS#sNZJMC촹яO?F/l$>ZEZ j}z$6wr8j5!#qJ&/8[_dn"0O+eryv@YuN̿n9 _meEs~6Π? _y{Qg~T՟y~[/~O _>y"C EuKz=suQ<9sxg|q5YBZzl|'sg_"W"|3ȼޢ (kbaI/Tc<־4H܋crxo Ѯ 8u2"A_#Ŏd N@ȶBTi}D8`Z;HS=DGڒZliOu sWjpȫ'qZo:M!h bmn/lV6%RM=t 8'#q9ސT?C}mq6g!#ิ ׾s?y/#k<=y x _GЩ=Gg>1 8'L'p>xg7Ifs42gn?E#xۭvHWRٗqe@j5æ/㸀ѢhkЏ8ϼbfʋFWjF@վ|oNiojd難98Xj;NjoL"|.Dèfjq 8SJ,O*čg ]X|/0O!K_pn}5Af.>s2# cvW y 5C'TkNkr W2ĭx_kG-G<{.2?'?KO}Is)_y'ïヨ=<=y wj[3$RBuN9g=<nc}?Pz Vs~XgN0l+G3 /o6ոԗ'/2Ϝxsn`$]p8glϾ8q/f7g̽+|!!ƕYLB ۓVq-{FӗlgIkd8P^^q?[&|9$}yV kφ})T#^Lܘ}i汭9$1s.̵ ^%9O+4cȉBQJ? &b<3lW2srj]s*^eQaGK?k_~l: owE  ÀÀߗ'/^QsOI=s |&p> a;0 sNo&ڞ1xNDpOȜ#8g.!I5D3Mpd5D ,i1/m4q.8y-9m Ιich39POn4{S?ß?hXA | ?5K˾I֚y<'pNs_;=nmS@KISnW;8p[LfyiG yKP}՛qTn+5`Jؚ!"s,W0C.#lָ99ddѕ|+pG#%W\a Q%8Bo{~Ss}0|kǾާ825"`4<}!O`)~/H[=c/5mʡ=1 ں#J5=ŚF3tڼ.8ȾZ4A_F_&l[P5xB,l14X"Zx-ND}O~^oR@~z/͟o?7.忶Ygs |r8|rk}1͹>{u9 ):OþB_B/BҜZ_nǩ_vν՘3%k.43`H6T)avpRSz',k$zyύE)lGjxk̔ʞ"ji#dauFsoc22qx,V q­ H:Bg!<~%_l6~m7S_O l<`kw?VXcuzD~ݠY\fT ʡ<Ĩ|- 2S2&g_GEIKJO7fY&e%Ю`GkazI0c7 Y}qJcs L*rkĬE9kX|$+7Bs3rsh*^Z4Z3>ߴGy~OtgDӏaփG7 +]N1~ rc1}#Ӈ ׌nu&6紑K+Aal4SH=a<8sJQR6cK$L8͏&;c'57arpm'Ba0} ݭ593N<[U9rW^Gp4ڨ\UD}Y.ybsqlpQC_;K-1a$p4nHWNq*=mBU@rAluKR\X!KpМ/ 2&3^ /O3FNg8zLZHO7Iɉ7tN aa7~tOb}$KkK?g<;>VX}}NʎbbzU,Q9p4]gx(X.8 E eP'{ѾV*n9:QĂ`l>9= q/a=Ήl &v:'ITc+,Fǩovl61'Ys}%g׈H ͩ]}f?4 *ԋ1x^6,%Fm tǩXsZc xh ݦξ?}{vF 㡍nJY035/h22 *TPqN'-`7)'|O78}ǦVFc.b|qZO~p~ OG18VXc8Wַgx=4ci28Tw N2^FМQz:q,d' ͩMhgA/ҽة)'ˌ ҠF4qC@X.St}Jc>BS_Ho9QYHn ]P?}L׍67^CFZHpayכ ڥo"AZl_[idSHz/H_c}S5JʱPphw^Pl:V'Ho)?$J1!^eg ' 9 ?ϹarsH#M_G@V[}$a_C j؍'8}[8;TIKv>}18VXwrjj}~ˮX.ASdۡ; ;@ Cyj[ZԾ'+ 'Jўy!5l?YIW/o?d I@j^>4rCyGz {i14a' t*[ l%#9L6Q~-Ck0^]e_l).r!?.{̾ɛHt4.WUbn>QERcgK'' Ilk'bx1;Upa QF0fw0r/R#8VzSNx!-`iFđ8sq?;=7>wzs8}_/.ǫ~BK8Ʊ +v\v f~lYfCv6f6 Eiय7%S(`61n&= Hp(^R#^+پ<}zZ=a{>_E ؄k3uN|I'T2*}1\Ęs+q<.jzU.r܂CȖᓴ9v[%(=INKt͐S1hnsݔ(5tJ-JaPTe,r2cC)!9#ěIyr>y[KD8l_gqb+@8Ʊ +p?Wxp?WЪ?7 bq꛽W۝p}lpCҲ}ز/FͶ$QpptϜ4j_Zp8/stwJ|60s/pcapwmWc6&:uKqS7# ^We;69lr<`tfGfqٝ+ۗgv@ZKq)+zc+p8V刽BYơrb"i6ws`<54 ٶ/Y̿sjOݘRx)nUSm NҔc92GF)D,!p< *LgqjHʄC}9SyIݻ/%miLW$sǐ~׵jīqݟf2F@_s 1"jN5\Ur ![#_&89GfE_K\ұ +}a8uc*Gq<&GxX9͉Oq(#PB5$<3Hɉ73~ӏ_?]r | EoD/W>;?Jӱ +p:/eھjԦ~yxy6 7{a v. Uc-mu. fIYΫ^+ihٖ38΍z},$#mågݾ5Gs\IbW{&ۼrk]u%13N\&@ ?xp<{8Lm6SjG!Ff918my.k<)NҐ{sZ~ʁmT|Lב+@uJ3$TlڄZnˆor  #SjJ5^CL?O=<Rx Vt ?++V G1qcxoyYsc r``]_ 1Q9w d]@i(sks!OD7ȹ:ijmTpHMd0;yQ&o6*rЛ3e#bSWi5cs|3g)b0[P; vMv\6VPaH-7ֆ;N}.@ilˎd\P+8΍9Qվz-3d Up/n\,d+} 3Zʱ1:/<[ Z" ꓗ8MUPRyb>0w3?`7u\='O4\#Z`{O?"m4E_}[?*KI;No18V_wmE&"F0ih7^$0rfKN0f¦3jS985BWOXĆf$}yy91@='B|1=b|+9<;#j^>4d ï< Ll )>*ƚ%;EqٍfUpaH{>CGsdFh`lK9!`v;4BLkEO4/W nY^Ԩh,-8(OsxS FS_AIA>% ^Tvoٻ& ; wo"Gy~?}==2Ja?+!gڿ>sx=} 01#~xZ.9M Ȋ7Vl8Iι}c'u`wid w./ܼoNg+pXXú+{n&Xnfms7"%/ S'r @޳w܏ ˏlƮ~&r!w^ C40o%y 熣|eӔGE_ӣ xpꗭo ޣqɺk$u:A_oCQ>kfF2 N]WB'YxSш'h`pV7/̛\|/f_8Ʊ \8\nNM~FF7뱼 )vQv@/[w, bThgl0҄Qv'/6m.&|hМSA n{MCyzf'/ʈS6*rЛ3i#g.}XN vju'JװÀ55JQɧYU40ww{e G^_Ջ*ӗb7uT\+6:|%.8L+Omˡb(^e>_, {ԯJk sG<3޲eP$t't,&N+Z?ӣ0Mp ;/%51c83s}w ~I̻ n8rq9 Q*0ҟ%6Gd; ex3PpeFiJMJFͶ$ƫ;w62`S-qї=f_C )W nWEҗ' +. SNI!#yl*VFd4W!3v9Q{F iF`Nqq$29NS!s ֝O7o}ǯL+p@_@_ÿ?+Fn6)8;n@tSunS5 i yȸK?7] V< 8^/^G]/0Zutoì^/ /ȯ,~?nZ<8ꖭ/nM2Q]\ МڑyTWa׹ }ݭU_3mp%Ǎ,#:O5f5N TEz N_c#zM)n(UU0E};6a`Z4U> X_#> f$!0|ӏ_8}(5]W⛷~/x51c83s}l"0r_#;,.!pζq7=]ǩoC .Ҿ]HiD yo-t?ee :nn^hr13!@L+l˦/x9O/+Q;}.DO(j }2{\Ѐ`N4}p:~ M|@A֘ ڗ^Wx;ف7@+\xRO#74h`XszAѹ1W~ql ؍-k른ҏ}HPKHNǩoR?^o5_'ŪM}e˾6#`|-LSTp_~ڷ \y˜y+p`Yn*tEt뢠ҤjS X˲җ&yԾJ/0ܯhF_)}i s/; NJI]i׃YIk_F9먰C/ufG_fq]/et5osUM4)~]1&EDg JY3FͶ$ƫ;wZEoZ//{K6+s}PY5*7)r N˜44HL 7Ub.7#`o#&k$E~nٯ}JM/GڬƭO}Ƥ>|m&vLXc h{f܀ ˹umo֬^$fhl2 }%v  @.E_R.MFԡh gH}]<jh('ӐX')iOEpwU,{TYż„R7ak5O8v!8/q1]̹ƹM{1񚳜6 U_mN|gR8ƂsGHN !\uppL/WB:tvB[k+v8D{Nx"SK9ss'U:?u7n}9+p`[2lٹVjĮqyb8 e f ![Jj.7{8d9(*j[;QFܵ36*վ7rɡڡKF_/,&:uWh"mḢu#C}' z;!}SRLU4/-mA=\ 7GluQK[&른QќK48Q$=s 3{!t #iюaL#G'~},ez~MK֝?~zݱ 8;+Lq'R>6EO 10twMtDŊơrQșkۻs"(\cKkm}\Po}angIk_#ЯcxY_17:N}/y8 L3g[L-,B"R1E_Wm2Y ͩM\gA/ҽة)'ˌ ҠQ~-â/焀ƃe#bae<,+{/exWF4W nǩ_F&}w,t(qa1pĜxS\q' "v>}o?pm'yLXc RB7I@\ ig< 9}ĕjo"M' j35!n.x^= QzUcÿl_x¡T}? vx6 pFbIZtli$=gq y(?JN2ػarMۓSeCZmU_#aiR ~EqTd晆IY&ez_ūَ뾮Õca@+19=6tbyXRȎ8NӉ*pٌM7 }Mqq$=9<7<{i<n~k֝_zk8cJ +Xb ݈fv/($VmˇQ6jS sUjO7[{qeѧ]C}mqckyW%=k,}i s1;N"ɐ s+Ҿ̖r<+|pVuѱ(;]J (;0W; vM~M{|T1 v;DS&3  \dW@TeFe @ihr_!, )Um@IDAT:OþB_B/BҜZ_Dp;NsRr~:Bzs@3-]}+1_8}#~C?srq?_;߃KzNձ m+p<xۖ-~B7йJR`qr70^pY=" n9NfHkC\S#Yt/paՎze/֫reR]5ka@óiȅ5N:cK#9`,Iqj!Ol$4 ɏ'}IOqѝ"3ȧhĂLbEb0S~ NJ 뗟}$28R\_Rz=jrF`Om39$ @}ܓ5כjc0nz=mFQǩ_ĹT}lnjIZkL}.D\9TU)}uflb8^bZIR;FQ|5Q}֌(m8,񀡸y"%&8 7s9ڭӝ;k{Ǹv+On]ҟӛv;&t ޵ޅ n8s Q(nZ1#`ƙpZ+j}+p_pG/;9 Ĩ|6?9 1㮶t`8t+eМQz5AɄpX!6W_ADGԸl_V+yQF\Qzqn9L$rRQhjAVL' }ov̧_W vXx50ë>|Z=UQ|h%kxn8zu1lP5)%pך`[{RÊjϴr$N+=8a<=r0_˯J1xşz//c+pmm)y`' <lKP@iLAnI RABsQN+pVv]Cy7v_iGf7!P2} NJe!70KEfiHz^벯l֗mJgGtkدS>TziNmO<# ڨEx[T('ˌ E JF/ ~ږSG_K 981J3vFjd!Y4ϴ\/:YcfߍB8A^ ? ov/|s2y04kO/'9}N >ŧ޸O~8NGg <+p<x/߃waMpfHot[ %oܢɿC4}m[s07J̍'= P^oc_UzFBx!oka؏Eb}xҗ2EhWje_`죧xpV/ʛoqqsUN+p<]7%xm"`9cSt\-,U9^dm"x gif<:F֬3{!tKA\tfƢF傯ѵ"tY )d@Op,2e/9(]L'!p>99xq8ek …e!\҈壞U GC_>j=dUn_ iQ]~k@'Bb2_c#&{NrCҰ8T V>{ %xJr 8^xӋ`xGy?k>򉷫ơ{ 89{lp7Wl&D7Knui2|e7,}ѷ4a")!lDfu}N-.vUze`8b(OmKWL&?A`BƄ8lTr̖{})*^ʥm$Zv31=Ǿ:%v|KF sW40ww>Va~T.ꛝ8Eʒ8zZY4>:O'Rl٨Q6 mI/p1`{H,їc4ۗmҤs5Ø~Iso]VOG z0xf>LV V~N/ C?uRq~,M[/H|\'n_|z_s]Xc+p Kp=&v2=n: +HT1.cy͒¡o:T͖) 1 ")P X68E{ }eU(}mqc{_idiCkm8xHq (z 7fgN tڙM8}m$ZnV襸+L]$en`v+ gD9od; ~Plsw>JO^;!9 -?0hMm^ N(r&Ҩr#T/]/ >W4ƶ/)( pd0qaM^g+˯!q2<<c6Bǰ/M_+_y7SoScS|ܺ8g^;Sk>cz +!XSC7]99?ed r9D ' M>7FY[M\R#{Y}Ƀ67ӨbZ\sy }-B/.<%Қ]ڋRe? D2k\;OzIUÀQ|aOmfSdQd-ؤL_u*W䑭lWp)Ja@رS5 C m}O8V, Q_=m>cyu}@@y8 $WrxvG&|e7Niz%'^df^nycC{@`/^)nAf4`̯|`v}\(V }ӌqNn^gIp_қ'>~$u?g_5N˱fR>x&8ȜmP3' 6O)!rơ7 / NǕo}GYp!gQr<(6KHKCOWF꾅FeSi#6 !y~+QovptF)N!FkٮaH'۸ Mn +=LVv z`,qΚ7n$+;j/5f}Z޲<[җ&}e;gal4Kj. *G&tZ)}Nlze-JT;gxgd~>=5g:ɇ,Cc;h +C_5~mr_C|Wxc8V\yYIMBf%I϶шmw)mF";N}S I6@Fk 4}Ֆl0[G/;9 Ĩ|6?9ํܤZXoh!5Z9(FͶrrPiɑМڤ_}a4S+WѺvL}hl<-EFǩoyc~jE CrAU-?U87aT.k 2iA2gB5l7ےc__pOQ 'yÃ:n__ۯNx?Xcl mY'L/X}wlѰcfyl*S@S;7RCBsjS*1nI ]jsNiptn>7tWZ5٘iޘ8xH:t^a9M孯qc6*Ib—{&:SFp~-buekm1*t܂3Lc8҄Qv'/GМQz:)MbS&EpR yVY'ʈ+ՒuJFxz9ni#g Yap;}ij>h%'1+=&~?H(c3אO<;+ㇾi#E|MlvbS6ai!~%#|2vΝӯ}_ tzRw~x '18VX~%z8> `n꜎M|%zs1sgavp8-\'j4B^,QZci_/+[޳5>xP7 jSX^IA=ァkP*^rI` Fʹkɕȉ@2a8b(OmKcy_G*}1Pǩove61'Y!x ^ ˜ ^n/NUm\ԑ@ry,FaP^sK88X=,xFp85MMŐKLy}_f~[_2{;77 Wn/\~;P(q ܷ8ܷxp(1gq?/ #Hg;_߸y/?Axtg)߃a^=£n?ޱ +v^>?k 5[MNxe?$)X cZ!^pcҠqf.jz܌MfKӡue'6''<1-ʡF40o%^jk9$4v'xIjO{qvE|I'̏Ԝa=]þZn"OjSZ\ Ji_VwPQ|0*[.9pzO+rY輁iA2˩6S^-˦Q~-CE38r:jW}y ?.ܜCsR|Qo>9X7lę~pt3sH9tN* O587(DFd쌎&:5L__?. { |?%:<ֶ8Wn^}@~ktc8V+־^ځϬ>wk?fK2?+,X1)c+cd!`v0X+1JO^;p(9 uQI67tZ씌2kF( mTN!!,RwChx`)Ɨr^ki Ó5OgĎ8qh#6ܔ&eQԩ9A"]׾^įkOWa~1=@svS>6*qc }=+䔘囐nK5tqꛝ<ᐺk8 siD ]qcFt6L-8̖CQ%8N1*_mON2N?47r@kfʁR6 5ےZxq-d@@*3Xs{Xh078[\$LY`o#@Ra8 ͯi~f4 vRR#7{ڗ1K#jFܝ|%NoQ)t}U.kבU)|%NTp^վʦQ~-CE38rҗpTmjtﳑ^y%L1;ruʆ K˷<Ʊ +pVxp5~`oF}o2gt"C Pjn v 3|l<8XqB7\_4r@BsjG6;e[9oV&SXGZc*SʕmR '[\ p<,F dNBvrvIc/Gw`is*;Zk2.mdH,Mcwe7NpᢊL蠬^BҁQvQ9(FGI׸& P\sjO B]uo4 (eU!@ihrQqCЩ:GWρyx`U ynjC{r9&xNlęa3bbG4C6+M4Q҄:6ӪOf|QgGz8VXc U}Ͷ:N5*7gI#8FCHq-jT./dUXos;X^96u@2[8~56K*5dCd)xa6Yd&ySM\kmĹp1Ȕ#1!a}&vM#;iXXEX bH}'%.r#eIHV7fDgԾli:tU]j$Sisg0ͭd]kFY;U5!Fms (愝5\Hhw1i͐\qro8 kr-vMjSZ\ bnڗ9ƢF傫ѵ"tY )d@ 0w 'gBp Wm7ےvftqsNS=9#>з|;5\}>SsMbq8Ʊ +pVxp.3!~ʑMd>5 ԰IZnV\|\9 8Pl$GҶpt} .F<7<ӗǡr1[D7[k83vF8 5 I ypP@MkcgtF<0O7xܵ.*+(;,L`<<}9oW}Y 9T.2Ѿ?|ʣiCkm}KH2 ?/%}1UgtGqr0fGyp Įb5(?{kdErJ-$p"s \%uO{/>Cҿ79MV{g_!squο +pVxp3~=a"Hv p|"d~rdpKyaS!\7/$VX$IW Q8ˎ iHj8^ԨܢB6بVFٞ[ԨĠ~"[aro>O>mL8©꽔5pf"XH͐а6Bp <(+mj5]Xka'Ӑ5l_ɟ8cs NV/6\FZ2xW5˹N-r:~7o 6 [9u5>sq= p^xꔾ6}&}c+pm> ޶56Oa\TF˸+~MyTM'ۭNYRҗ|Wёel4׸1#:pfKӡT#9PLkFRCcQ90{Zn\.hz5Rًe)#QQ)xap~Wg\ mFj[Jj[. 0Cyng7,-CIjXԨ^ZtAÀD0Ňt:zljh .A@5r4yό q1ũJO3ik_}!`RᲰԸl_(#n/W6M_ (L~Cw[O종;7Yng;"L8xv#Λqup1y#mqm˓O*XD \_bVոqx0Mѹ ZXc+p<WS|Z7}-|Cih9ΕH !%N JrhʇpmI@%婭ڹq3qI0:|+4lp5/N@jӐ@M4qꗽQE -8*بVF݇A=1t+ډM|͙]O(iզFc92Wf"zA4[85aOU# .YK0@͐8'= P^oc_UzFO-T_b9da(]/5\֛"g9߶`79g uk?{Gb /bSx1Ѹ&} 40(>&D0;;{su닍ɸ&,;Ʊ +pWxp0'~'w4?̫L+cll PMI/iQKܔAT{bY_ ᮬo`2FǩovG8np)xJؤ5tx岴oSRΤ9f[sd^_ainXG[}f`:]_tx{ܦՃ46AHώv\b<(beGAyQ#IH_s*uj}u\'5r1;N}jmp){345*|:)N!F:__!2e/9(]L'!4\L=a{ꓗ*I|8=)_\Üo6嵞YlI &'L ;Ŧ#VL# Hd%0_ `5BljG6XeO}À05ft Rڥ!5 BrX1L)@ނ6"M-"a5ڮxY.ɲ~,yk„Qe_㚌hҌuy/;9 Ĩ|6?9 } W:0nCih(=鵟AɄpX!6W_ADGԸl_V+yQF\Qgo2xb8W D+cȱ_1GrGu8xvԉ80ay}!7\Ԣ'absc >HB8VXc %}0&?Ɋ\ɩd`K0?^;s8[.Rfw]4rzR se6]"!P|g {}mpay/yji|n][Yc<}-q 1x|pbϞ}7]2s4{~l]q}*z% /FًZӹ0Oj {\p!5܉P}mqAGԱVPpVC}#-n| B@% (\{`ˮJ쾱^Ua( &jIw܃MjYr[Rp#pv8݃'JH-A15Wk'>zT;8g\2ws޽f]_al>qBT*e9jqFyeFPӲ_ȞuUv6q/48G 9[y F6CLuE1ij/@4iF͝;ku-8|3iq8xp7W^${ r@]F%09d$7>a4lWqÀ@6+^;q]V8f%~J.ED,Ƭwmq=_lI%)C2١y1']2= _͟ ;,F!CwgO/y1ũgi{'|Y?! Ͳ|fO>@C̙e>!ShZ\b@9\y)^ŵv+!'Zc>C P/U#׋+W>g4 '>s WicUsx}4$WӚ2<,G㕍˹$cCNx6(B+;$u%o>zڱrXKgg1d9{Wkz6ckEhz2.C*?DF 97 lr`0Ai A[CYIXscɦ/G=Ьpwha'xvނ 7(z:cy\}~rnSXcUj9o|8 26|*?7|voa/tJI=u5`cpمp<ϵ$'YyeR,ia~0uyIZ2n@IDATrᰋi{̐9~(wָrnJ#|2N\2.kܒ ƽD76(;y}4Mrmk'`׋bF-.%; e\RGrk|*1o$ |h !†qi}\ik WR5l2aO9嵯A Gijc8ʡuASվofj6~R#SIz@a#>/Fuxžk8Ë3ۂd l9'z*6ĸ7v 9Jm|rD>0c  (ҩ-/?ŏ}W&{K8yļ |;/Oy`_px {o ؍yx{󃏻7Wcz 1^\b~q0r!WL@5o 5i3s~Z;͠V JN6Nue n;lPH6gY8ykDESq@c!G-^O1uub'C^%ۂAǼXZecч-;ه|Nl07 Or2=GqR"zsp+xFT$lʦOWv~ه1-T2b 0ȫŵ\g0΃(/?AM~!{^-榑$[}* NF8a Nm_.N\I?cȲő^HFhlYo$yp-67W*zn`4l"vØ{k0 5%1a@@–s^ ]G^񤢶5θ#9^9֯2roi5 }"I׍c8%i!lz"zɍ^:Xxի|sy2,xN@'fqSvyYN'Gy0c+0V`aభ 4/?7Y|o$OyAy K\9 (4uBd]D/:O l6;!ƳrpF#f,W^oκR^ `ǒݏz8)/]c WόmA*!<TX Yv{^ Ol Xգ>f0EO&b.-.k8σ|C6B('a'U>2ɫŵc9*8 $ۀٯܳa#72ػ}by,|rp]ʸ 8<#scJ-u>cxCy(z}x_}={ nȦg\eL=)M?ґ x(C!mqBg!z1a#d\U Jb |`PG c+0V`!.A?0reuo daQ,⌍OkaN'qNQ7N(.8hi%. 7:I',2|b:'A$f:B@fD]Rw8ezʫuQRBI3 [ f8Q} aL*+U48դ,f רd KjM$lO pyF/O0o,n)jqyPa-lgfF}A˙]Æ!S@q#˘rk/[e0d[*2.Fo>5(3WgT_;[q#tsCs !lpz [>/aϗdEWI81<H eoxxDqc*C>0# +p+0>8z@?X)"۱k&>z|mM{o,kxH`gt;=Gl qS_px 2Uؠ2y4N1i=?ۼԑĊN>a @r^=?.I9W+zn0Kit?Lz5+Zm6LZKyHZƹUq N! ,3ƹTX$|5Ǩd ~asVAe46W=ۄ,CHsWWهz"G6׉OJ d+[4:WoiPu:4y2[qzS:rB/e.s]y:XccqUpב`{~TW}Al ,,"0@_/D4[9率xGPe-_@Pٴ^ciʮ/yeJFy!&&}hlǹ^r]Fe'i/zo_V8<!ʞ-dSeMz(O31qDU(j|#/}mba{2_X,<X\VvR ^=qPGP~~A/dIsQyټA}oUh}W8WW#\I4+ʫKYhSp B=/P <KS6曌j5e,gʲɧ>nR^r4[cen#ʐ-Br3ڱS b6Cs+2lxܠʸba6dɵ"YRqB9lxґ$4=G䎖ԅwu4Q67wїM|w{;lH}'ɦT5O*` F _4|pBY<SAmC+0V0V`|pW O~l'BJ]|m` ʀ>)Tw݅.C"@:?lv,=QDcd,/$Ø(E4u(J6Wwxr^%N2&YmY3] T ۹\s̊& zrʱgZEY<\w.BlIq0%shX]{8'T^!hN =B>gvLP݇ ka84k>`ߴjnA˙]Æ!S@q#˘rj%2GmYk#/hL$i) '=1T6Г{h ACNz/a"#݃ǚz9v/8@rm5&x8"aa\pWW^-v"NAӭ> /h '/7Bc:ڌ=(CWZ/@5ZR-6V`XX0!޻ۿ{d7'ya/?=yoia~X/tů ,~\.]uYGG]Xȿ\/:ޕ.s8į Z )qO%= (9,yv4ùMyٵvCH y09 Ǝ]S-rm]{&V/-t;K5ί\VG!nXl33?R^"ɶ_0 y^aoy$O[/d߲gDEHO8d7wWZq vA80,QQp]2UqAzOœ8}EE=Q/nʦѻo)6ቚ#;1;gelc 8\<0doZ;^}[/?~a{﵍{a@o M<، ==Wr\R"_E45k])l.'!n^0a@r=ƙcQm^梮As}Hq84^2vN*63U[k ŘBb28dh9 " cMT=~:ڱ( Z\JU˧}h8|B= pI%[&׎b6H М= >r*㊽ۇm F ǒ'׊ וǛ.ma@Pk mH^ 9g"+<#. hB:S  <q@;oΓeN!^>%&(8[$2 +p*ݕ3+v߱;v/^;ągu~z;.n= Y-NY).d.#>LJKdiE ňpuċ{qm 7U8(5Oxv&PK_'ɧ{ާ]gvLP݇ ka84k>p=֪\>l2lkdSNƲ]pR|,\9 mI?Z n!Mҗi¼H_ےΆ/X^i僝'n?Y/8zĿRUD9L 'N}֕_̭E~}qw7{atA?Tn~\_Wt@@=6..g5-lC`9dJ>+.W0 siҪF_J)4-lY?W.&ZđT^9)N:`P>cۓLpso-Yh,"w8Z\KIZƹyy9}Ł81r̼fɶK|:+^;YH1Djs9z~4Z|nXrA8Z#F<> G\4'CĆчmÂ\zPZ<겾?mx᲎2Agw=7RGrֻv6'?P8>xozKG~.0JWf\y6V`XVa[2@y*p\]DT:BȉO>rr铓w{Aಉ_6\( ..S{0Ђd̺8)B%+R];'|ڼE ~E@?yjQɯ{ P󵣛rƎdݤX=/2.;71~,vD!}-,9+ '@(2 ! fo(+nXôh.&$ʉ M4! Ow.8NyY΍CX#׋\@+78=@3q}LƃsgP͍wp@r}";ka820Ot(}tSo .q ,p}+ K^7#G>гgkyd7)d~'zy]2bB?|8z^lNGc>+X@*7ȏΣlZs+p+0>8z'}IVa3bḳqǗ{s~{os[_޹~\;~ag BK K`4+̅#/ZAe)_:>cC>.~8'a?0lŪl0z=)/ jZ'`^ p)0C>c^S|7 s~*'#8@aHD̟eRuUIdmMW};O%%Y>M㊸˫C5`2L-IYS-}bs1*>Ce))z9dOj [W֘XEăчcM7jb2Ut3ON=oԏ`VN){Kea`'O>8mܰSnOл߰46x<98ÎTmۨ?_G8M q*0>q'Y;^HdtN)W>QM'r9;Un2S[O_~g5C0~Me|mrIWY| )5C,N=\x  끏 ]sHBf[r$2.E6 M ![!!iՠE8V}ϏoOwCa^KA1,[|rbƚ}#,RǼFmeî;h? 03RZ%4d=@PZ?윳 9BΧ a̅~53#Z?x`> Cy}@*jeUUz))~kjZhc_[2w?a8?vhx[6KFفuky8ŃI2kǃ?[>NIr''*Ѹy) ۏ<6V` YuϺI8B~oq/-LV?yO` 7w^|%|R`{v>hwMǃpUhW-O|u`osBдA/׎c(Ұ~& |zb͡A!Whs6d+;5o-8k2o9Pu_W7g?9?s״^c5nUCƭC;yv\MFk-.C~هv'X* U *}Wb-s,ۀٯܣ=e!sܴ TO!SK;}cp[_-Y7s'{Syrᱛt(8N_jKt9 /(hȱ,'mYo6=elc ?*xe!d. DUf 0|7{-}ík{+] ǐl$˧{S^`ܺdD?59>4VmT+]۔HUꙋ# 6Lias}S{8u͜YMR^a[\Kv?Rܦ8^\9ݽ|h N}G&L\pe,Ӭ@^}\[~=b=nS@ ḳvy%l9}&vd_N7|=eK<}2-}ZZ@Z@9%šd oPFDos c /F?yoק_x+T`|G,{6?yOF74} >ӹi46XZ[?p/Z9l󗶟¿6om|:~'hqŭɄ˧~\0)(ˤ8/ӕ26dߘ]$r8Xs7o+ze2AMk Ҁ_2<0Chq6.y;r^Xnc~-,%8feDM~=9ar^ m#OMK6Y8y?//ǰ8'Bnq>0>r\ ȋzeO9?j.9y9j@szpE?`k>Ԋ~8ˇG%=h$ْcGSoj}m Χ[*N6Jn@:r\);iȝ^y׷ 6kRm/?.@o}C, E>!pzQO~B_Hnq>t25b\ qN.7-8.?g[?}#0w4\kkI 263j륱^?eȶ&\7Z ҡAZ\K pZ%&ƇU'<T*m~G& _e 1wM 5N1fwo^qCT#b˧ r.Yn-;5{!p\AB%be5s~r$fr< >rsU{^^=E[%2Yg zJXYEfc9?Ǿ1mk]y%^7PRr NR:\&x@!78c"@_8S/*i+XY uO:+UI5%рtqS]xa1i`mu5n~7`3?0=yvka-sdya楒&6i`e bgjkk;o~& HzCα \߼9/pn@La6qqf]-ĸoA- ~E172^a.]N [[^')4v f2e+qIىz6a CT;&8Xyټ2O`čD~Yy7y<y F2UF?{m)ȯ/ i7$'/a%JEoؐxMrteg4是-5=Ʈ)z١/<Sʇ`{sWsvk;o}9| ゚+>'kDI2v^yͳOq4S${: 'b~Y5._d]oqyïo]OSm[y Q>/ 5]ɇu2c )4-lO1fҷ9\y)/Mi-K$rp尊bzqJ=|2"Z cdpرg)zى}'1`ύQ6ToGuG1N}e~e>s|LVSmf=辂u*+V  `#|N']%sgsrɖŎR$BpE$sgVDv?q/\s{3g;u.mw [γlʃU{ ¯ Tڢٖ۫s6-~3ܨ6ll K_>s^=KGR7 a`0cu5~jBR[m,O~)sg"Xtdl 'cp+`9Qvx̝wQ/3d[&'w44T\c1>Pe98lN֯ Pnn`ƶ2C_Gylsb ioetqJrV+4\'㐏ԳP: Pgxb'd$:Z`6~(n҉Eϓ$cb+PNRE2iX6>  BW(h~w𓨟?d[Q6Z,Ed/*E %F;Ƒ zJo>tvCA:K7yB:ؼn;|ۍXG4ϒ?/W7w?ڿ_@X.s'XB hsz*p:RUVGLHi@~Ee3?qksε&zl[f{ys[\l5!-?ɖjc,K%`1kd. wU23@^¹2ŘK߾Dl\y)/M$dOkW*ŕbT9y5n<4f l~SϥA57w$6q\ɠ>F!I~SK'Γvٰ 9-[=m^қ9c)u 癱 1!7}IO8+qXSwb$ce~&ޱ}p:olX#_?Oxr5W,'2Dz^~=xp _$l+J2CIaŵŅ~c?{g'l-|ÃQ;Y 52qA~B&ը]X?̹ؐ~qO%FiOmƚ'DϏ Prqv]ͼoMȥ}cnV"DuI:vFVdi%\F5\YaM ikq7вdl%eDYSK2wm^>s0C&'w44T\cx%=| rpHky|nn%1"=nCn vRCN'ia[# @4dr ;p~5E>pqx"01l,~G=) 78clmxaz~\pxdۥsʕ/ +_ 5jA:s ]s̱Hێ{W_]X7~~~^n;[˧Ykdm0`z|Ckގh8#sp<0FY9fS2-cd)x#~DM ̥f9BLPZ?J@qr#6c˟dz|rw=c7ZnQOzz#z.Ao}MJWT'vn)eiF!|N=:y6{cɁ)"ǒ2./BG^9 2|}"8k-kd>!U+5Lmvm}AcɾހS2uU.tlc7W,/x7qOWӾ(&'dx;x@௙8%FKkɺ@| 4p|ڼk>L*۲O%#gPt~yvez87(/{Ix7g=/sD~Yɫq1漩m'AB, 6@>Ǐ,a]?#^v=7#ꋌ^>P 3dЄB:pSxt; xL{J{WPӯ/o~?.઎mXV`|0קxZU`Y!lϠwւ ٯ!E^ElOpX^m'[)!Ú*mˋ k?vOaӘ$^~ooO ziCP~6\| 2cEOcIms.YHzt0ؠhxƭZMksbB(u!;m'KY>0JMЁ6ncH)xW1CoDF#;ܝ\o^߸x@dvc ?0ק6N͛Gw!uQۛ6^X^Ϗ׶Aʅ_ĕ}ʹ%]OD_'SܰFdʍɤad'I]>NciJn?ه3BRrjq&pnP^Vws y^179 Ϊ'8=M^-.C6}Xe2t?7~ ꔟJ ΃a^G/Nr pTqa,>hSϜWLbT Hc9Pzpn@L)*Bg$%n*e?y¨ pskKOp\ >6>sv [=A=un.hC=8ƿ@{ꘔ/ɼV6V~6kCm-?v5r` V;z(@n8V ]nHZL]-d M WM78kx)/?2R^ `-.%pnS^i|}Oeso@v䤧Q:`Mz s)l<=[S8Gc46X>4=T⑎2ҙ%=3a R%['sMK coӽ񵯜40׭kiA%H6*  ؂I؛T9 ZB$pqS9p(xKgeg]IyE*)$ψ!dz׵sq&pSwO݉^0}^{򝭅7.<z_֦\Q{~G_ommkZF slMW>??aFYZx8#HYo=lmMN^6|"6>| @9W8Q^*rW!p_?d-h˜( '>-RWH݋gFu7#s,N8ȶ,Gy-uL\.ks.YR^暒q[yu_Ę>WN:J<1L 9uX8K6B OYoAyf'Tn JUt9S˓yRn_ٸkqJ6Ƞ MreX } ::Hoqd^W-s_NfM$_\/BYb]lbf>L Oq6:7-lЇ2uWpfc.v,.n^yۯ?T.lۛ> 秷_{xb`!lĶtpN=]>&{bZKdF!f_?y7{{n;O+?=3@E6pb}j ˽z8bykrwi&4RS~&Z9cC>7*1q8Zx0.ze#~_wa>WsR cOx~ -軇zǀi\} a'8vL~s叝g}[G U? dEP+\1k.x-twş{M9q /˿8Vi -kJ_FG#τmO PI;l[x+`A,Ww nMgN|͍w:xgw |=ul.]XE1@``8 ޢ.ube|}o[Ι;! 謯 +ox( 3zukC @:\-6ǻ}j3Ӿ.[e~!#gP<в_p0ZO! :VR8bShZؠbzI2eUM\N8lB6\;V͸j%Fʩ{ʕqaÐKuSK4adŖ<6>_ڼfxc\ ? A!0@™^r_8 PÀQ8j+| ׃~ͯ3ӟGV2K+܉KeΖ|w  _ϩq%'뜺rJ|-&(#8Dl!6%[ \~bAfhy)>SPFC J*&K }'/>KO{wp5 {6g>Oͦ}O=U~5;:B}q4Q-OMNSdiU r.Y[//_7"&wX_yI*&{3gnzak_(F!nQ_69Soaz|.37]?s-&{c_xĭ/qs-!"~KBrʯ 8>'@`qW]}ڴL6ЄW12IxrL%m^*Bq+9p`xf^0 a":R7rU-2{{c*'(/z96iҙ"| W^s2|&^rڷ +%O>v7AA_8KŁca*qal.N{V'8Qy܃2{o/?X|Wii%*d-X#Z]cz?No0o¦I6 J-p$k"C.9 {!d3_KFshq1 :ji>}i䗨s3;P>lB6\;:(so9pݻs_v^87} _xߺQ0[|u@ =Ӂl[;wJU;QU͓Zn?!ql|Qxί?dk,\1#›ݻwEo^ չ~~FLΫ]؅B–ygECݼG8-\MH3&ӳyXt -~2Ow@;q7A!x:Uzy? äזz<}Pq݀ ZC~ћ<2ܷzG`Xx>]ǿˠZ/#r_8_6z,|v񐠬/+8eRdZLC}8h9_ȈWB XyJ*Qy[ }0/=BVEU `ed3&T!loX:E w)gP8t;N2(B^>L%t7^2vd}JŁ#0~޽:c88퉋;O/mlwqeZ0z|z'|Gak7f+PJ޹T>Oڕ\S~ m'zϋZ\K$sg-\~8 E^El3r`ku'ٯn0T6 lBia+AmkK|'m;{3O:yv}axF!k{n9| U1Dt5|m`%嘜_À'o:^281|BдA/9h_k g 7T H]Jls=6ʆWM-'=o:S'[ָawFeJ!Q])/ݙ';ddn)ښRM<2x2?`<8_/ӳb%RYya(4w,ovo'O.;_hϭ2gնf ҭ)kg ƃT@'tr`,4cS.ɟ;_cg~ ue^8iZYyp *D^.t ]e^HԹ]s!AKZ^\8w}'M{]>k\`e)nv&WAG ~L-T줵<^>6<pBIlښ|5!$pͷa᷏H;7řu{|mmxرw=_뷯LoqY[巧?ֹ45; _űG#{z7^ׇm@casp$k"|l}ƍH3nHy) \xӴA>4KԹ^(6!Kb:qt> ݼr{;on< \~qe|;0GALH約z8y_V(v-as7 WL; rL>+nWqt? {9vc6 M [5}FC' }1'7HD.nC7A`822366b䃝cЧ8@^1)nNAlt<)wYoݧ߻.<J坧;{cVyZ5EOo>I X\Q^}g?l> <_r{9 jqy,٩9H-Z [C\ز? chֻ]8>7eT83 l ^NrA-}w!M6dK2w2u7RU6#GK1 \}ik*>Ceq *bf#wql a'^x76_>7gkga/JNW랓gv>SA+[ 2 pŧ;n?3Nq~1  z5ƭ1= miG*R*`kٷyU6 hmY?9xLĨ"_p_<®OH\sh3p`uP307PG*~? 5|+ͭGZ lטlk䰹 5YZ8)TB~|&2Iiq1 :GgR8BG^D~)\ ׎N1]B"[VSaL֖nYlź7}ӏw\zGvv>勵tOv{_ȇ{}o m<*kO3wA`1VXcYca3kFR2$Z-cCDQ )ljC83|2+va.d-|LeǝÖ!l'4٠OxyaI~رs3e :6el~2+,Q^*1QNƣ8|)o޹:-ޥo??`+Չޕ Lw<]|j|m]?crGy6l '|ht.W>aд>Ϋ~[!̲PybZ<3.;#hN }ir^y.gv P݇ a@Š0C] `[ȩG'ΛOܷ_[]j={қ|}giv-Gx[<~J rĦ˂(}Hm٧oPuUgp c>R˸{L–y`/ } ůQ؊pEɡZBz;/~\c.a'@8d0%m'㡛o7lc';CH;@;O,"  zjyZ6AH󟟞~VbXJO =j}W?Q3VU`|p}q~<)o.3jR.I&w-IN1r^kw!0Mq@dK2w2u7RU6#GK1 \}ik*>Ceq *bf h[s=M;{ϼcygw>; BwN|ɕOcɧ:Q:-ki'ٙΟq2`~ˣq}Z\zby@?p ٰ_s7 m^Tūbh12w¤)(G PW__4x@J|*}?F֥O G CVB _T2|l.=S4N1+|,/΀V1*S`/ rtȠU%D"^|kJW# 'ZO޽n]ī/O;w|;O܉Bx4^ !Knk+4A_%e ˦h3bl"-W(/9 6c)-$(*Gy:Em7O_  yHy,}w"hu70b2C{Sx,yb>3G4"s g< m<[(l,-L~ΕgoY:~[{?|e߸Aֳ4Vڬ:VOώ1l\+A}t±+p+0>8~L/t-㒁d{?ùqB9lT>z8J+$#Y8o>BȣPvc,l>R^2_]ɍWMM !ոU7-MσuoU=TWOnwcC܍`@?$"# B1"`;ccmyq>=߰9ݷn;<߻:{< ʘCOj6aex9>g>+O?ğߖ[oHğ 3x 9Y%xXV]qH%O qeǥct8N`d|q):į.G9 bKB!;L3a_W/?!?yiխӽ G/7W'q^}R⍟7bCY㫍|{ÿ"7ʢG|@Nσ -^ƔoqH}Q{zK^^WV7׏ۦ_B6UPs퐎Kǁ}=[Dh8ف|}7R=TvIjs,{cԅ2f/c\1U(泀,G ]_.(Gj㳮nqAdmsp7otpPc=~x$qd 2فv@Hk}Xa|m:~ڏa}ǚy*O[ >nP cxű2)8) x Մ݄ޞ.&>Uy߇=.//r\&;8"Q`.v,|tpȺ\U*n Td u^~yOo1_9׏+<x^>%O |K^`ޯUHY'`zGԭxy1.k<GB81G=K{>O_Q2׫ÿ̓_?*u}BW}xC͓֕ sCg,d/=Wҁ`[@{qW ]lQ\ջDt(qW\@8󙖜@.PWa ].!&IˋS saˠfNcPFS; +>&=~]v$c}4Է@ >%|jl,Ien1 H1X]9P_aO{|1 kA/4 ݎ$ց`|տ$=+=++Go޷N`)޳s!e.n|5OwG##|zv`W;0?[~ƛ58a/w'.nT-0&JjvuU8^g\(Ǹ~nq!ĝG)O,dx"L5J!|p6*|h/ih+Xp`r^ 6I ?~\CzzH_ `~ >iXnqrD|-À  HøǺ`9p~텬^\\}QۑP.n;go<  SߔcE/mR=zeEmP;eǣcj8PM]R5>cĶ}bnqX>q^o|ovG|DCCcn jՐb?Cxbl=\tۛ<ιqqY,b(N< bP@W 6U]a,kB+"ѕ_%z?5u`R  [7K .qVKe,|0/l^1̫d$MOx99FxasG~kHS^7(O^:juG; 6P }9Yű_DF'Rv*2\b=K|^@u=']=WW.ʏ ~F2j7>- -mʲ5tJ2ım5 B r<kTл.~go|ϿpzO5mvtЇ+&Hq"E7׳ցwuA08;ԁoȊ3NЁTe.XE=e/6DqX1ArB_KsQԗj\ X‰*'`É' !Zz>,|0)"c~XYŠ +y*S]@1;p: 7?#~V^Oo+{W;~HyN"Ǐ .|̠<.! qԡa9iA5SUa,/ 5JI즄'{tI?}ozoZ>-r|m1?YRv;:~ȍ]j/ q8~cȥNqz4r7LJzw3Ecq~op>%n=C ݙnf3c< Ċ\s=;vqߣ9ǹ':/UdXW 6]@2GF8qt>"}C|Iay #7yCw.^|,d3~CܯV&ymqKL1.EZj ?lr&z]"4فw_}ZS֓5 Ⱦԥc}|*#m;a#$K@>7ؔ#Qi0ϡ8儣3bPVOw>GfwWJ-2ԗ8\1s,+?1 Jٮ6d`')8/Ap6# ݞTGD?x5OG?|OK}~.=C D->g_ClXTPi؉>ف|}^:D(ה>ᤌwqJLFd2*1a$N[WvrU]ąrSBHwQ&`m|U@'֬bܯ$7s"c`a@tbm_~gt68k ~!k0Ձэ{*qF_9FZt x:Eq?GV+WT׎W'AY`%+&~k_8[p ;^eYZ81<+' ]xp?yu]k߸yOYP܇(\;_ f384 bcv`v]~'p6pv`tc>qo)Vaɘ0P\ d 0>X Wy!G7m8MU䦮Ǻ^X`\b`]buP|3.i;V4qT> >.C-agց_ο߸}>y퍃g%[K|>*4ퟦÿ,~m`_ W>hU-=f%@b]e?UƂnWHg7Cy~ǯqJ6>tdՏޔt^2c{,  SMǃ@GmxP>Vljĺ~s7}~ϼp~~ߟD;>DDQk< s+~1Neفۚ>ν8WoFn2v(^}~`<[\ջVtDLq)Wu&)Gn `\ uenu~iN,q1bB](8r\.A_*P3ZTg^kcǮ_ϊŸyW9y[[O_{wQk_ퟦ{?.(_ÀFǖJ>qҡatÀą@0Ða_|cGwv<^}Є#oW> /nuĎ흲W{>@(*B׃8lϘb|'CWcWV\^!Y/~}{EcVRa)>7tvۡ;Eiշ`Zzv`:0Gmϵx8Qy'Gĸ"a8*k$s4hGS-9ןb(/8D"r9z!%&YX b ԩBKuGc3'.z܄0sGy2fKjXg 3فmtw6+{x3y<'%|{VPh+'rsDcBR޷0]|WI\;Y=I:^Ə _ Àئn~voj0jJ |^3WOwR>>'@C D->'_C!n>Ybؕ`oف]|[sYS. {b+Ŏp E t S+q},/őMny6 >7Yei,KBH_Á?}At{c]A'֬bܯ$7s"Ns1;p: 7ЌGQ\N՛'/v?t>3UYՇk8^O|?%my pS>5scS; g{"G Bds񡇯>+sߎ?._ߔc;m6/( 6 lZdxOʍt? [~n_,}8C]|%KsQ+K<16iƹliK]Ou7lYc^Qk`t$ȷRI8ֹ.Kd̪b/F("XF+ bXW*cqau!R˅cwu=ksẬ~lwDalFoqrk>>s!~՛7O? &<~=AOʏ M>% 8 h.L/eh )a|=3Z3.ށゟ~X_`V!@d`ae^ߦ׾MUAmo<r߿GWVW#y_^:|V>EˎIוM s+~1N vi nu`>ح}foxCSy Mt9#79P_$g-ݯqa9MdU,ë:ߕ e $x2/=Kuu8 d[y4F545f-%qHك@zy,/w޸uUy(|mÓ5<}/k;~?%|J@ ,'`𱬺,wc*7O=xp?7:YSWRR|n>AC[>шc:lO .@cv`g;0ī0+~fNJ\ؙ6x.> 2Gegsy"<| 4B]F 3 s|MVj HՉŠ.O*MX%iIQ õp`æq9d*wE;f}>w#v>q+^<\<Nn?*7R{,oO ⅁ٿ.K 0@[N ō>#tw6/J<~tG._C #_xի_Q;ރ_ʓV>=oW9|F-Ksݙ_U≱oh3Îq>с :1w8M_@IDAT#)3fpL 8*Or$.G >u#N[WWuqd.1 '0c,]7uVoIjqBéOY iŌ9_kIotojfj|̘?Y$?<{. @>DܻquW/?d ~~\ rc K We?IO1 ž~Y']]GSqM6k%?&(}~R v^vblvc3˫R~l׏k_\[tYW>C5b>7tvg%Np'ơ}xss,w`>X'R0ȵfynn)$i $Q .I-uxʚ T>WB\[BAR\Gs]*;gǦu-R˅cP  [cbXڎIfb݇.RB(TwX}}#M~G7N88^R{~@N,|/ԏ_Wѫsy ^nv}ʩ~X~\['{}}dk?r5?{>yx^?^Q+QVٝ㫎v1_#r'XkѾsAzv`;0߽vxos&>=3yQs <Ldn2v(WuRP@qU6_q _¼,PNƠh7+ #uS`.-TW@^&0aჱL~gct 0IhȱpM~XSʧ <:}Jj=}POy[5<v?}/Qxxc}aŗDO |E~`o|㟑ч/{[W~:1Iꀗp5+ |5*#B|q\|'9 q`W߂iK-٭ݷ>k{ux ݭE1q> 8p3e Gih}ZWrHl v8' 1r8PAdnPAFꍬN,u1V|Tas{u'>Xs.MNHk9Szb`uG7Dl8NljFu0ڕ}>}J@ߖxGk|mAW×NWx={Ei㻪%];Wȧ^Mkng7[Oɏr~mI86s!TǼ[tXoh `sl_5ULL/0,he>ᤌ{C:,h(*@SJ\(Ǹ.0boI7d9XB R&p[* >f].3 GZ1;p:s(؅۝yEIP|a/S_&bͼ0IyW|䝃ߔO $.?.lO^>]=)WٸmeXo/~6;<1|q,pXg`W_$|6t`>ؑ }5Nq6Nv1R"\s=q!y2[l$BP&1+.tR |ި3.uUYSag_@s]ZpF'sP 8ֹ.!MurI KطmgYd?O ',X/+N0OB>,|0Kyڥ|9O(CÛO S&:ߵ,@M:J3:SxG\/; bij_rɛOQ\Xm{98^obYT*;gDQs <LD.a^kp2J#8/i~A@h/28sN9ྲ7+sC)9-TW@~;9فmt@;8NxS 'qR>G]9{O}4<)?. &k q1ف'/x=?9~W| ;K7_ONv)xɷdv4Wށ`-?B ?U1zQ[)s ej1Ƣ4q>N*+Yէ R0Y`U<@L_øso@ ^^ 5qdbPsiN8߭ ps=;aQ\8c&@OˇBl8u8G]Zy GI7QqA[G믽~򵁽'9׳Ixx?} X:IW0wg;CO=}.Y_ǼGX|ԙiW-/]ۓ=z\!;} B8i [O8*ct LѪnV uU8@Qim7->XAspL#Z\s]<ǺVb7E5r*V2FSZb}P2ź=~Î5,#Ws2Fe0,$Ns.ڻ~eׯ\ǧWaȿ6pQOImUNW_[ov36qzHT 7_c6e /`}| a:ف]|K[ux%csV]n"ԉ0MeXTw:FZ [j}J[ÊLA tRd@uhHk)AcpY⋝?9 >\&[Orub]%R+n;K _._7yq2>-S-].Xƈ7y_{? u鯿~ן \'qppb9"IwsCgwۡGO,1]} v$uh,?qyOu`>ء}/-Nqp'PWUp,I-OujҘrN ܬ+Ką:Tu|%ybqT暀c9~ʑCCbk8r+#`K@+JD5Ջ\j@c6W">;p7; 췔ppV<σ-.@JHEN+_"?,9yi /?xɭSϊ9W%ف܁7O֯o?Q}%GALJ@K(b!!ƞ}W; NZG`&h ف`7g9κ_쮾RJ-F3ZoiyU KN`88|< X.E |#^9?Ł}q, ;b1{<,!#KCva0ɍ1*=$gv.o-]~0~oA=OUl# 𦯑799 #}Q D )ۦuI"g!StwbrP$8ۺ/.4ف-v&Abݳ쫆c]e2>Xo:Nu!ReB1껃oEK1C=p:y|JEy0p:!Ooʳo:̓OU ~\t* 1|q,pXQ ^W@Zd}d}|K98gvm{98$]鬬RQ@g_YPu8>H:10$p&@eϑQ._8ֹM=$UYά+"euuiH*\`uip4[$]@ne܇}gܰ8<'C)3:T,t‡K2nwoޯKׯ=wsO?b8:Yk)[Oʏ SW[gs~]nۧu쟴+ftCgwۡx5F3o8Fc~k|KR18mՁ`=-N|1d>:3|Ƹoyv&D!΃0GM0hya/(uU8Q.Kb݁ 98('z{ PCG9ྲ$ueJ.s!E:\h[vv` HՉE't1' 1bT_)G](E4G7CO+{x^v&oˏ ~E>Z_ Oa; zgs}WokGp`+Rgp(V'ƞ}Wo.#wкll7LV@磰S<pRv8C+.~⨯Xű GsUк'|ME kءf[/r `?Xb>CsںF6ƈ<1,82Rodub19zpVd#,W ;^&>Z*8v8'֬3Ze p]9^ 81n_~X7N"?-/s3с?88m[7OWKven'"=7aYH |bxu 2 Ҝ;;gp(gvn-O<gn}7\YS.夔xsOwp6@ˊLHP nҕI X}S$‰5 H-)*$>m>qrJcr_ߑiz[*N-l|p ߲XWRUOI%RB-xu1ߞ#saygu槧ߒ7NyS ͔: ?}W~ ~ݿ\o͖[󹡳;а;(a5_qÆq'I&DECY_\ }\فrB::@䮺qˈ;UG #(.܌cA­$,:#s\ a|+|E3v()_Q3OUűgk<3ά+"`ӺKsS}s]*cÅC\8;Qa]iqt-_9'J>N6WE`!P<;4>JL?Ol+basҥc^G}8 Ko^=<^?#g;78;ˇ<]3j{N[|~A\p߹9 vMWi?_\ҏ4iY]-s1;v{om^n"<83.rNd 7 jnBQLJMA]Ca`Ž 'TeI,;3!GUZr9  c3WaCy:Q̥dRlSrLSHq}U)Lҝ|f Gzpy ߃ f5ĢW~/?xu^j?. y o:Z@>%4sF;ߓeOB'A@;jt>7tvg'ƞ}Ah\sտ!v`>-y=.tlL̜>oJs9s1OZ{8SH_[TpںZ_59W5x 2 7 X<42>)\ g}8&N8_օ)Hzi'ⰱ\.Pǁa/yVD~*A=$Ju%Ω;"]`] oѶ&Ks[=w^Ev86;:W3@ %?m$,X܏8@Fc]e?wƅ-֛ %HQ 8cc{\RkrW/#Y?>8Z7>\>*f3UW_>zCLnNp;43q>{|?st_NN1ف|Smrqĉ) eDP݉*cņ]יLb.esj,#mаkhy]!mq29^|RpbW\UZ\=DGc`qPܙumɹH]Zץ>ֹ.cvvsQ]N'y78vɊ眠G%m:`!Zz>,|0)4WqbsBVQ{׮=+~ ǧG'N bv:?/~|A6`F->ʴϣP,DX7,|rv-h,2/bzΧvsV>ۋQ)j][w9ʛ(o@ :qpNddS]Â\H9#8CYd<ƹ>ɣ`^꘼ !va1ę|C +&'s޺sԥR̔fցr_LDZ%JJ!SϼqeJBM7:@Z=8S (='ܿǯ MGՁ~y~坣G'k]巏} w:hv>p$Ò;|1i#27 楢-ʧe;ف`'7'v暕OtƆȮP#|'=I_i>N@x9?pbh@vP2~1N vŏKB;h}U(e"=ف|ӛ{'r =?Ho8)Y1^/㼞cL G <%9/`X]9311NHu%ι"_.j-a9>hq p^£. <)Ǹ.bmԕNmv`{5{S:qRp <^WoUvX`\bao%Ht/셭;ۦsRv.K8,.yؓ72[:ωAy^>:_mji?I CL`#O'rx o?Gc~[=QʧL_1EsWNFaPXy6-BH]=D –k ^%& jn%g4 45e.&y/(3FU1J/-u1"aqP\)y79K4M>8}Su:NR{f> AF\Z^c',xbZ 7ui a|!P3ZPY9@.y=|My"_ͷ/Ƀ򵁏 MT7W:aiKsouO||p9U≱oh3[7 $y"9[Χ.ȱs9;vzoot7~ol@@'\IgDA$UL9.oyV)D!pM3Y]nJ.ϡ0yVGInU(;|@j|n";,KOC0Cݼ q:98T|s=;vuoy9 |A2?nT٭h & 9pNˋu 'Gy8~aU뚿~},W1F RCsںF6ƈ<1,ߘ:; eH_#蛎80(9&eSWk|oűr >f].3~%%ަ)2Р $\z.Kݒ>8}ճ}ݠy_x<<.ٽѾB_vW<)x 1g}-q(uQ{q,,k)^dq 8dMJD0 p|˅)sCVƺY{@q-&c)vɓߨ@EmBCLyNu q `4ZpJXquqԣRӪPW!]5pOfnA\bLiv`4[ȸ?q»7<o(5p)]NWy2eTOzB>7tvud!R#HNxbշ`h,qu0t>Q^+ZDNav`w;0qIONa83 %NpڵpQBVuU8ė?8baAm/bs)M,z?#եA7>$MU]@(U %p LJum+2⌡$ O Oqs?G3فw@6ÝT6}![ոq!3R #uSQsZPcpDPB3Z>,|0)4oGEXR-Ouʡ͕Eݶ߻zyO%i|ߑ7oj|m@R)֭_Kߺ}p)P9u{V|nr(V'ƞ}A.mpݼ[7RaY${C*BigvNo-N^Nz5VϽTslz)sKKlBRVq4Mu "stQxlq< ^$02Pa< 8 *x_P8/rࢿ۰Gň1Ois1;.X 'ZUyk|rv-ر5?ƒ}ܟnee8WLYnK}Jff'>'␫Laxc]eǫÙo(n/vӮ-[%V8񨾱 s`h/njKIƵ1enBg.[zxUy1cr|^<]\եY7.Guq)<{쏃*ᖗ1IG9D< (@>lOjWŌ鎥:-M!4ac;Bsqb] KY<)mnyc`ܥC^AyS@śG$GNonQem\ ccOnro!f>sv>GSCGMoصOG=B'8 ,Osa '9R.aEX9.!K%@ՕF9Zsm] *&Ŧ J=eX!tcs]ܫ3\pw5˰exU(Θs9;p:퀛v?W>ʣ@LUwN[Axy)Az#A]Ճ:U>/ QKɏ >|zӃ_@<)2,|kɦ>G|c x#m" d%cX<4&u_k kw>i2(:(v8ف|c^N7k _$Nuc JÕ0@}QHhDqp/PWXk_LU]PmUsD"㘗Z-7T63)$U0l 2/=f^% >Ҵ=nS$/0?JA]CsSx^o:^E&Y# s:xgbޒѦyY Nxbշ`a֮d#Ust3h}kS xŔWWWgFv`>mՙ)p˷sF Y1uR<ˈz 2ZYmɼ/܀x'Q/3YX*^Cup]CFE-6`Z8B%,svUʣwZokL&'NT=-wyϑA.~L]'įo(5H1oqx!k6|,"IM(QWahGv.wR%TTűd_>ߺugk8Pmċr("X<16 crxؑe*/K)WFQM8{_)2} / WX\=S;;8$22z{!b$űU‡7ps]wE b1<`S[uUĢƺm2s`i5f\J2e)k dd qe=k{ء\{ܶ)lqnS9Hql㾪9$輒8O:a@yuen2R7_҂Kuu8 den" -OS>ŸRWXzȣcgNq,G+_ =sụnm!F7<_Q1:{%9wdq..d_u>A=HX<#|oKY94;vyoq*;)|!pwbHYHo8Ã(9(ʦ\X>`ƅ2&c#sTQm~vf[]yC G.y y,muAp*yCuL^JĆ/8(i~@p!;فmu is'|`9Yc/'̩b>,8tc5WcP | HՉ~1V:!'ya290sa1k/Gijq刹!(;- `d|Nя\Gp) s+yC8O8M,Yr-b"K73`[PꊣL_1Y"Ybb.0ف|{|& ;N*¾:G H=eP\Xg,rU0ȘaеX5G$ x5rxp_I"Or>Nf[},cB%*Fñs9;p:]0OQTB%,/Udž}y <)G{80 .fIW.xjB%luK5J&5gC]Le^c9%O860Kp59'B3M =#.ZơOd7^5 [#-BtݜjřbզK{ [s20;v{ouƍ$))Ny>N\@p R el e)7Q3KYymg2Oy &ƕ&/j-A]#}]0Ha9ܙO+䈔z m Ge,ݕEw@k,s'|򱕹9~8N3J$0֥%z4w,kֹ.!kKCMђ3T -6캎۱‘΅ ]]&X*V0FBs/P,D0{1^9ف]|[̙O8/= P2L|Q^XQތXW U 3Pu]-qk!_}ne1i ꃺl⾖ǔnjU]@\JS2J1j{S%fҁݭU,|kxQu.>xusuʹJ*ML`.솜1ݱt^pIE8G'Sh8 @(6Fg΢{:%b\,6b9숰)>86=BiFt>5f O}5x8p,s3DsTgvl{;82*P5 Jky/D2HHYXx]saŹ#A]du.fYsQMuR\(MdL9 Ǻbu8M`M+1.Ui~$2;po:g쏹Ұc%| Z~qq>{p%? Pš@]c.X.a1Nw^pB G+8X,,)7ARxnʕt^>,1 ue`2Ff!ZXW"(LA2>dqOAB%,;@p UzJ>׳[v?7'NxҪˎͼuc DRJ u %NJTF[?\Xp5ץ b!R3S -Sv;]s,zW` J!E@jt>7tvg'ƞ}Am8 9Xu(wS9{;"H+;e=#!2ف|L:Oz[<+x'_T,+NBFTOsuu1[2R}‰4},2@9Bs}c]*Gq*CB­$b1Qe *@VJ☱CLAI %9P\Vc\8[oJrmgW%DyȘU&W؞rzOl,#ꔃ+ۈJx޺277:%}.p r;Bgįeţo!VjB!%\Z Ye,i@ĠaHv`'ơX<7x7R(<$NW-A\ʧL_1eEkp{BS~a#0C4%h8f} e܁`Χ`|Uw#޲˅_`Q8&H@) >eK8s2-b ʦ\WA]USb],p2o^@+,mc فu ?$}e줩Ua ;(RxA 8r#a8e0"5Fif)AhF>ű2švY T390U]p#c!1jqpJ丧^ d>y|wx[rɤvηuZGm.Y@IDATJw.9Wqsdgvۚ~q ROX ƂI w3~ 5Z#*SN/( WE!]@]su ⋩ -c&՝#QǼ>OB:Ţ:ScMN@ddSn8t>RxGkJMpTObHDJh "Y[R"X2LH*Sq]@0pfqw1.xj3T 8E[W%ZbuP|O<7J}k{̼J|2p;밇:H+Cvu4vrh;K7Kv50Lqv`:0&7 Ͽ:>]D<"ڋM$i$s#FSWB0 'P;> GRI$ q5jpC].bD)GR_G6DK -<̧n>ԡso1Xߴ)$94޴;Qbé;`= 08p:\8duib!Rf9TwXhB1p g<թy Âڠvc[,#Ձ4.z?%QO>aǺr=#!x衏qSء;Tq7 '8ݧ'i3uGnΑ1QBluu1帢8n<3 1:FՅp=d2&/qI$PBM81]5o{;&p%94cqYiJᨢ/D) >eK8s2-.BD;`a#s`ͼbf)P3f+ ŵ"ղ1͕=cH^; VO=c湜jv9WipWn|"ܱWq'd>98kjkL=QPuXwN[A1"(mL5K'B3A]-u.8|^`多Ş1Q3 y@PcrK1ϨEsCgwJcNnQCծ8wD8L0}Ŕk_ P_mR^mKquDer3׳;ށ`wmN?N8 \1},ԓ8)P^z;S9xR)[-OƀЌ݃J~>.b /,b*X71E(>H=y);\z r/80Wk)/< hSz]?WٟJ/-W($aYri]\@?ŴjnYa|3zOayT&HF3-ۺ*(IP|a/QA`k񶇟y,y28gqݛ"|5<8xC+EIJ:"/|VţZ+{7sKB)`j(:ʜ{@ :Φ)r][{q*uP=yQtED% Ahu+A@-u=X0J%zp,PW U]+=&c)bb\^}C-1Ճ!۾jRaMΐ3[@;>mí4^Wc}qRP9uqİx9n㙡\qJJo*U,Xa9>smU$> 0u9Tp4,bJe9Cm=FQC%%;yu-`]e! ɝbZ8N0rpіz&@\'1ę|C $uerc]s(9UidLwvOQf+ ڃbʠ⌡8 .0/dJp'n<1[/8@U܃,-pU0FT)VoDmq\ʡ18Upi[1@F5;p7;.Ay}qm p0 >crik893lj>c眶. 1"(mL5K'B3A]-u.8߯zsĜ܀QV [mfwɎ v/)J| |TSo/Sd,u(6^SPm΋Cm6q:׳ځ`W]% $rQ8Dz 'jy4Z0O#J#^Of힃ePQ0b].2s #8p jQc_p`U|""lԔfnxȹ?eUwC*؀U8Vs] UzJ H:*XRʔ*4b|\.x쉒.a붮'JL)2yi|Ĉ aZRs^=ܜdw?zs}n$j7go"P0- r_sj291(zחzv`g;0㜋qȚ~nqNn'֭,4E*Sy/*r$:ܔT‡Ȑ,)9r: , .B Le7!yf%q3d,%p6@L=WPpq1`d5y-6Mavu odge'*cvz98ر72FUr`'þۺGHr#եKު%&H00al!i=',@ m1 `2ml oYaUĊWޙ{U]On73"Z_Os[=V/)m5^\c^[KgV1/?ؗj}%ee@`j\}wS}8744Z [!H|86VKcbE4)\Yg|ρ)+D!{^ͱKnEޜ+lM&X-A"gi r s7uJ$Kӆq (՗y529:7tjGJسy/R]'}z1G%{ZfL6`> |JDZ&a"4rx_y=5aCHi-te~ASr Md]۩PAQ#:hNC[11g kw[T8nVe -KӔ_$1∹w"U{ -?9ϲ,6|FM#Jxx^sǵnPةPԵArn9I>z$D6 vF`cx&վfj#їX_e{KTG;4;r4Lt9Jxg>qp167W ) !V|p1?E~k1n$hX 4-l9T#5/M7{REE_K V # +)K]Jt8OjL*`9D3f 0VU189TPESy#GH8_[ʯuYm|n}F+>4$-0шt-.xx^ICmIpGHQ5}-j 2 FԶ& f$vpƓ}yJxW}N<J:k5ƶx-[5iiU}y |Cz?#ȋh#ڶP|+<.lO8,} C+ ؁NRgYPW?+Vyuz3X^͕$boc$jˀ[Ћ%3޾-F2TAlt^j]pQ 1"xsm_{~վ=Pm$)vjԩC=u)yv | w=OczL:DxWM Rc~u}LF`UXiY lh/ĪaԆҘStl3>kG3hK 4|Ӱ5l<;E88P {*q=?b^6k<@b+T3vghsݕẃ_AH=:6-1Q 9k8xVUWy'Dy=c(XJw;5zЩUhkOj1A"gi r s7uJXbaĦ4X\Ii^V krmnIl;zi%/:̝VjG^rY}<ֳuآ. N `4غN(61>}3hX Z:-.b 5epESȺS5f_jGrQuќj>'c bqܬ#kX/DW/PkUۃ9 c sb~ ҀELj Jܮ9wŠ7; uh$mHTF++xO;YVq]~^U:p?˹LI?g_@[{ {*>s*mCt[支V):Z+仍M͹UøWi>PjO##&o#UJcbS"FM"aYLmp4T9`1(bg.[ڑrnRC, ,@iNb&7}Kj=γ~P=z8cB;Ou5j9vDk9aK1Af#|-HXxƜpE䊩r Md]۩PAQ#:hNC[11g kw[T8nVe W?B뚖u9vE²9n<t/}@#5ZQ4 |phmW @%mHLqtب t!$j#}1xo_tYFQ1 l~ qMx`З`F 0nyٗޱ] h穎v=idy |cЕcuțS@U]ZH|i!9A6*qF%#4W9}%'9ɓxlFlӔ{Iu`ĸYbs^5bpsLƨ9xY"F<ξ&*Vr:Y`%qZUm|׍p߰̿šϓräy|V]96WTk{…Qa|#iDXZ\^iTʯгVYXs$| uzKe}pWDM+9宠5va5h}_rMÖ!ٗ&ɞ9kzR{+f FV/: Pe9-AUz#GFnjeǤcSVۈ^"G$b_mp_1lr(wI>~y#ŹUh?asQ:y-:sSݮ97ErBdLh׾7^o}e琾/?9MS2^Q_J|aCujZWdi\@R{9b2&/($V|iФGL&~ꌴԌx! ۬K9 <Tڴ=@b+ĩt롥ccsϳD)g kQ~3䬵% fƈG>++xg[\wt<8dxg˄uA,E껝w=Tc 4_'V5pCІ~a&/Y\MlUbXb`ؠ?l9L24cZ-`}˵!oh;&5R휧 +N _r [6Et8;aaCc.PIڗx"oGLȩq$®gяQ)+i\W/؏E7V1%f5hP,ܾA$'Ѡ6W ZA!5neGd_[#1WB l/:J(1t\mL{m^c /щgg@~Wkޗ͍}I_5kҺF[m@5y껝}MI@>J}ȈWMm_s.֘9>L_rõ4a'mlr(wqZTھ)֓9CZ[<#Y\ ~cumEnUmiD}+8)S!^l,95wSs<{@}] @^3ȇ0`Sg0?$Ԁ!@s\U R.kќ]5r j"-౞6GҎóALy:v(yJ#5hՖt>Ұԩ1 Z_! lZ>ҵB+jX"wp4T-wav\>FpARͲJO W C-uW]cH=@b+zhk:Eb`FRۗx+ymQF?J975_i W?q⢼]w5W#/{sň,זءvjtTiZXհzp[_gKcNnNl÷Abs+4ukn ,Zkk,uW-yg^{ڋ x53_m؏1–`$f1Abt~@ :s )6W ZA!5(2峏ȾF c b ptCDMזEli9vЀpcxV[y8s %xT{',Iؘ܈ֱw'[s);CTE Dڈ+~0Gek<,K9q SR]N4YM4f[unKs_W\/5ξ4v66sݠ7ӽAIVmnNţA`|vI}S>Z՜>+Z+x6~Ej c"@_Qn֛&2֚pq,joEl} lPK5HUnHGJ7_r;5K'r5i<1J |xtTo'P벅Ίp9r v]}7EZhƂW ۾Z=_tqg ǰyc8%uaS6gȾ]jo8r= /hkx'\Ϲίs\?`j45[<}=*[/ףYY|wu}7o!~yE:8%5'9f7r6͖3u/y7}D{@}_ <m:Ӕ}1 ݷJEՇc}ZͯRi $չ]5*dzV;@};Oѷ61Yy2t]'^WlڶN] 52ŭs4^6Zw:#7U"OhgaY2' 7U uԆҘSty7a[v\>FjFrՙ2)/6, 84R#]_h;aŕ}}VFPpF85[n=FK>7Zl\=nli$ 8 젙ȑ q1U_w}AL{Q;5w:3G4eI ذ̍azp˙9Ժ_stZIty/Ĭ a;͡djcC_ yC1m6 <+ +&upr: yR4#k ־TrR QvY;:wbA,K'̝@_͠M25 K׺^5}~K3M^AR}nthNW(md-WԿ9Ja=r m^X׭Иz,YMU~vşƏ['pklI x eVFсR-f;-H CS)VCuʶ CbDqxF@ wl&,}3sN:2\ӳޜ[rvd.k[Ti֗ToUE ۾B:)Zԫ|. _sFZN91EZhgA0y[ Q5׸L[_+9.Zz˒QcEɾȣm< EDw롒8,!]Q)֓9CZ[<#aYn=g+?Og>T\՛&|3veL1 kQG}y/,xډf}_e~P /5kø]uk_:lkT:I>/M5>tƧoEEi-Gk+4Z+?"Rc~Aj4ڥÞ#EVFȇ URBd;6WT paachl:Nb^kUS=EX yqWme;vjESiԧ]r3_m؏{ VsKF7cl3P& 64` O<+ݢ2tDW9G=+Vr;VXn: 5_m8tQ{' uV RnpD3EZ#lli:B;/R;6uhX4B:Smy*}gω[rv}A9|*s; G{Lz̏Zm|2A y=mba 9|$km x՜{ j?~Ƴ/oYǥx}a s%!}邛m00əӾv޺ >^@,zmk7>GH&X} ؘ<ݮиKIAٱZP PL>g>se78vj̠{Xjԝ `Յ-M2 )/%vA,껝]+P7*MIyJM}YA .^"+)c~TG14ja;͡d׭_uݚj`j<+I+*Kq<9z];OTt@Źaek_9IGĆרB;OuI#KObA,K'] ~èihclB[ ʉhAjJ*,5}AQ\z?Qy;oa]>qM#h ?1Fk w#׽0 s `yZtnR sD+pʙ1D3EZ#ll_uv&5<`ZAS}ye䲘Vydsm}o_rGUQr}1JC$9q'iB>V|] be 5\},5#-5#NG5yv,xx^=-.y˅xVuPEv*!6 e4t* cLc#%A=09- r'O}}T )ln Jmli_ވbfKʅT0 v&G"OS籟w=P.0 XҤ/bkQFw+*N͖+z:?}wQ~VWy]zx*>qc)ʱ|Pq̟ăi SsU~sFDRg괉 ȹ{ά1Okl,ݡhݷB5a-^fRZd틵aؠ\Vj0_/g<++Ǜtz<7>yӿ0_ӤҬ j# >ؒ74@طλX/ͿrnN]+5\`#/ 9vVڎeL,SXh@na [΀ sBlU u1& Frk-C_$ű__hwTqǒeM齏bohFXk5{C3s</ п˔{jBnpWz]%AH#h}k|bfN59vT]bM4._[bĒ|7𱊷 s?W}: bi  vC_{ߗ|g>ylcU7wu8O]ԨX𰍡/;h<F_`bEPv"6FytOYZ~+ bY:anFELC `#HF,d>:@-YlCM9n:1gF{|Vz&I.y=ˮi} +[IҀyTRi$xdqlGjf74Y=r W?>i֚ '/4 \wdNgFS钧9Jrթ C_;OnFܪ0ɡ:yk\7hPL1_}U1]_*:k2e'1.?++P7mcr}<YmlrD~ ޾"G&~- f؉f^Tv$EycRCZ,fPuڗUƊif[oÆͭ jS_ҹTEIZk}pYcHlHX'8CK8 0xtG[c~s,Ԍx!)kfש[ᬽS1xVWyrp-}3hGm\/@IDATf$*>p,l}yACs8bh;f}4c?++x{L]\t%_xլ\|qҴFlX{> /}kWP^ȗڹK-}/WƒZ7{ \֋YٌMg/a~6 8/5AZN{35zDqCckg<+@9>mu%Zp:v]$uH*W຃(sbl.y4 Pڀ?Ve}GjMbp:vM$2/D\IWZD0Tkt@w\:,yǭh1L@ IP,66'm_sVয+m_tS)8bW1Em$~[]uőg?d֛0ycAm&O}nY=Iw롒ɯw=GAH};5Bu fn> jF<oG ^;d]>`[Sc?+z+x8^Koz:3A8j#)te3$,iycas.WT,\YS5N͑uTäLhwûT9w3 C=Jc,VJ[@ag{:a#l\Jjv-slէD6)ӱAbs+4ue@i<ǢZXЗksCvLjd9OV4\WklzZW.{Jg{<r_⑯YbGM#D0ql#߱--cC b4N+֞-t.mnc |0Ixj= bi  i{<+cZ Ȍ϶~Kr=G5(W 嵑&o_{-vLgK?yBE;z\M ⡋tS9rAA=|Ǣ (w1ڑ4E&՛GC_G^&Q1uǞ_qo}((VX9Ⱦ8T43`N1hTKGK`¯&ulGLfıX4[<&<&f|1Ÿ ER7 O~VEWygO{\yƅXܴ$ = ^WB1:F a|<^}xqk,tՊجQw#/\iOݺnԾc n+9iu)hW C/e'MLwSH__b}!5yy2c V#V ;O  65#J93_V y xc՗ZTs@+FrnzI>C b-ӱԎ0a8Z DKrp{.X:X1H'b;#p5s^s^쯚>yd/ڊw>pbZgWyxoulπ>PN[^G! 2b]}AYZR9Mc5y> 6AI W?Ը)_?{R3@1AϾRIS3J71cx%/BtCDMזEli9vЀxAÖ9WpKU̙}G4ncm 1Վ g:]dq׵IkXjFZjF-coh 8{̩ ;-(qygx 5|NSJ G+Ć('P}angԜD*]7Qƭ0k+)mb%@`b擧XjkDԾ#fA|[r7h<ӯq&^iRy9zM~MWyw~?eߛcl4~}+I:dIykω ?}Dz/,^&A@8w}957vNTǰ1S]ؑ4a I O`ZÃSq,id|M^#Z} ]PfVг,Š1ⅯL+S˝6ƇBZiCqvf9kFs@{<vǖf%q 9Fz6 <+s | 嗟.g5\\K KczOU_ijQy9Sۦ!ac Yb{ 1FRx30_5b57mXjΣdǰ9 9r'O}]/ ZyK|%GAN Ն/ؿV+62gcu".0zT_'! jMg/a~sZYUE_V+Ԩf}IunW uWcels HcLp;5G^RR{R4+I 9rc6KCYPL_|am$`NcnhlGLT"64ml˚s5Z >b70z2oX=9F(˭>7aچ_(ʾ :ʋ4<$;{@R5ޗ$3SyD}av~3Gj/5>җדu>W+͑W Pau ʾO ԫviIәJ Q<Ѯ_BK .hdqmV?o2:T/G6L'v8O$/ƜCR:ՠ)Ŧ%& vC_/J9ڇ\cR#%yq^ Y}8| Cv ؁NhW?o/b-9~N͂!۱t a,3x VyeA#7F˜fО"󉍹JP?r\ PL햣`Ixz= s?fؿ>{_ ב;hǣ PLmdom COPwIcpLN}06]%,{6BtDͯ[=uYC׋ vi9ۆK$m[N7 b *a&f Ŗi&sjLvpœXӛ40^طx1 ?ج%g`4h4x{a - (M g<+ZZ+P]\n-/vG$rG<ݮA)JPssrB P朶HʋkK fڀ?EߺqrlY aCuj;\0Gk//VL}f[UiK+u~?OO_?KBH{OOǨQ8Wu0_jVRe2:/a}Ź@խYUwUz#GF}3hO^}eι#PHbnTLo+%pF|-qJe`\U[4> ^aWqj\q9cc[M|1 ^%ٹf~g^n/ uW w\X?BwTLQ7Сz`pẄg_ƞˮY\4a;͡d׭F}yܐ74)v{g_!6s3sHfijm4Uv ĹW !~‹m$- %b:o̶[<Ws>5yw7L[2sa߱i@k^lxx^1.¼[M|2# M7&26^aWφ>$d iH0gSOPX啽cP>x4OlTb?C8[,!bjG7 `p, KO~M>Ŀc/k{6KPXOyh?MW@0@1 ,Z8Ը 5jK eYWi 64+A%,06BtDͯe ]/bc 9,\zU6`}y/ [PD_v G*V`.RW w)g-G=75-= Y`gnjtĖi.= >{ <+Wy ɞ9*]q.ڭ #K%3!1yϾJ}_ f8B# ss2aT֞H!Vr3_my7d#Nn;oVdl¼ (폾" Yu:Y~5y> VyeA#79͠=EsA<#o9*S;I U^6o}]eOi;ejv)i<$WmJyhgM'$TLm鳯3RxUTjUteʲLC@lhizvq”M#qLj#vf˅JلU$ڎ9 `ʮ4vli6,s!A]=0FXkeG W=g^ii5jNχ"QKy[.'p,+]5 wWiWuSrRgj_Jk4bbE jvΓfϙejxNlrh y5yBa&PWd?_/!t̵ccs \AfYPg+?0k(:NmHqLt9I$/^w5L6B׸ "vMguf3˾ sZ$S "so\=mst G^@VSs,4Zh_:ysL^sW /vy^bӯCǻ[w:b_e~a&O{zꧺLS0lwr.V+5AZ ueR(ٵXr!D_is#/s]iX5FҢ)ZB-_lǶxqka^⃶a`cW5{VW s-.'kX~ hArijuDlhsMk9Cl͍gcWڇS1j\jbw}w՘}LRef|Ar.s3*Tg~j]+tԵE4-{@?^|~W?eo>_W6{@1H.GNXNk SӔn96H>l9yg_SMYT% vC_2C_{X/)z؎Ʊ~[3s3s,BjmƀmGJhf-<Z[<{t]9_mԾ(%Tu@1y껝|w}yv`DN$;+/#Y~!s3j}yZ4QMfK.:dfӵ8bi-џ#S$qrg$X7Ba߰j53>k?++x3Fx=]w ^±> 1ۜۃ_xLvJg~̡}ݍgF¬3ǜ6W}yZJ9ǮVѾX<޷Z뱿 X#?kLk'?u}eOCS8(ϓ7m4rAN6iJ޾}ZzYu d9:7`z־@\V?5C>s)?y_Wec_[8}W?Ji} S nt]hŰ4.P6t.qKn3[T+>u~j?yP}_w_ =@3sbz`#hX^Sn ebjg_.͍sRfrUl#0  6B \^tYk-WAثe_\몯J0s.YrnN5pڗxk|l6xFTڰ8'g.h0/`?3mLu13Ҁto|q?>ֳ/ ?o~+:.uyV /c̾C_ΥQe}d-siGR!Ü.?hč4y> g-KDKa~}+=]xz~_"g?w &'edUZ=1<g%f1+669 0MuNId4W_F4% FLa*;v MW F𰍁+yQqs/VJ#<Jb9 >sL5V]ZjK*oio2ō)hqq^aWqj\dd9}~ٱ0t0vlXy}u?i}=F==+R+xM/z z;(-IC1'(`կr}G~[)|8G57*P{j0F/g_[Nq;5^yrgL笀 bjm_tT_JUjW1EmƿS.:"b8"䳭A ack(]|N /LRJ3j:|-ZErY\ ~cʸb]̨K>TUsW:tbl]*\KyоN!}9+E+Ƴ}cOjV8ws.ڋڞ6<66b[s/rbSK?}ϳ+u KgrG4O, ȱ-9!vXi̙-8EZvհK LA8#kзzj%!sNCR9G]}@S1}i;Ka~~so~)* [$E4ǣRȝ'}7ĉY A|y3PTǮ@FY~k4J#]_RzD0Tkt@m[U1b43kNd<ӸdI\cՅIc.%lR?8bI͈cE]ixtxt0k5W74|QR7 `A~T >֟yitJ-ii=.xx^޼q]2`Nݷ sĬےD[56>KXX 8 s[wEP2BVsboUG.wieRiQw~˹b%-{y:u@V@䩯H פֿ_@@c9׬}yki`}C}%92b#Ҳ 9/ LJK5ϑDPu|ud#ٜAbc[<ޗ-נWQ<ƍ霹t/w>pbʅ_ :8b*|G g<++x3K1 &Wa /ſyWq0Ըڲ@Smzo_n5docՈ]\ T5^UUac ss^-quK{Ϲh/jC5Ʀa}νɝBw/TG;-eg+_g?__?ɏY7q: L`N}g.ԭDY#m;D/]Ws*~K q^؋iW 'ƕ)_&W@49`ؠ?l\LM`?{oluݘ A@X"(phfDEU)UY$bʖ\RQ\ȑ(NTdWT~ĕ"VdHM$&@ Hh7Z{>x{o9LY0\#t9MiuEz~azv3T08]w'uh}V2krJ|!eh.pH&ԋF0`}2"CvW؁uuGFt@rCʦ$c\j` 8IP9ƲUw uqe1"@,^^ybU$q]=Ĕ(XOj@} H GYuq h rֺ(`qHbn\XphLKk~ob}i~cDk1"ޥ] RmdMK85/or Y,a "^汬L>t!.cMKO30d"u%\dFL<n.u5T%-b2PRH,Tv9E7dN0_d@Ӟ}#bV7$;=g햣 Pf=yq=v0v`|p5T<2>ǻQED.Țy*; Sr،a9ǺTgCiBn㽈Lyl<'t5TX#|IJUќ&]PQa!`xxv7O'q9rpb ;tdAgYܪgQT Pǔ>‡*Fn$iU$н.(9UhiG=t1N>2PNU1P Ljf2ju>zLhHC0KM0(+|8ƿ|%S {לs 9|rf'؁Cځ!W{qE"9 βj8HFɣਯ;y*Su}ԋ"$pakvY ܁u]|E.`.* >loscQǦ}s#pSQW/)g}Er4|nak{jG)~s \7!oґHY6OP]XQ]|\q19qBȣ7j1vPWkuizN/R8Od-A 8b |\uëLd!)I{`m 9RU_Ϟ CǙ}6GuxbLͼ/_,F;HvPS? x-=]mIQ;p;0>8{U?ӏ'8ۤK>M 3;@:dhV])"wZFI0.3և8%EWs%X5za@979/sZXPYnObSӰu!9FWl%|_~ex-7>6Gblo0bf>}>Xub& qͼ$RݔeİT7Nź΁RO")9cQ/=f:af.ՠByG)pR>@#JV,b.|Yṉ//8P_.yjYc7^Ag7Cervٰ/(%e "kأh/s$L3|i/bpm6{9đRcaay/UJ0za0nk0ggD>lGjYuR6#y@J0瀫UWsx9I5<e1"@2*DE)',[uAՈVG5w@>,;|sc|LAh/gmǺu5֋u)R‘u]; dkF]cPx8V݄J-n 5+g7NϞXnjk^ %AG9q`o !N"e6^=N@i[ PFcer_IVcY?{mX!?k(|Tf? 8(y)P,y*ՠiHAXTE6_.ΕNֶ`˒Cu,yy Hm1#Fsk1fÑy\׬\D?lo#ҫ # Q:y5Yz?.wm}rJr<~q| Jئl^}h 橛w8ֹ_uWG[2ܜ,Zfz1Rg܊KTCc ]F[u͹dGO/)nH[mOQOF]g_Ɨ_l>8(x|1rxsv?>/J֖=Gy!@xӾq"g0`/uC'!p: K0-'Uy!XuY\f(9""؎*̭Y`E XPz>M`]~iVMcPRЈH 5j4-zeumn؞:rq'0q6($`r>c{>)|P(dr6@u ś1"P83>*;J]"EzrpӋ/Fo>cNĠqҨyG ;+b <<5Eq/+p]uБ#6J_Xr]b Op~vQ3Pe k=Sd wD-NPӗ9GxHsn86܊ٱ0ż*z\8>+s}iԃt'E\T,ጰyZX)|A!@%M%N0ys;Ⴭea(N|ʓϬaY,LAh/4Kz#9]6 fi*QW|rJVVj]|47wAiOa^%^+F>3Sv(͹ϙ57z7L"ہi[78citÿӞSĉWQ/_ b8] wUzÀP@>%ݜg /]`Dyè1Nfr>)ו}pcgi?|XWRY |Ӻ8_ h:8DzPKor`N2= N(|ec0Wy>*>G srg.x6>q~󼏳,}ӢH[@3Fh+|g]e@KdHp8^>K>q΁ wc$E) KD$_V0KRMO9S*Wv)T+٭ڞ Xxv6đ/sz>%vn=x ~yFCԏLQ' {!u۲;p;0>8{5מ_c}O;‰O2B*C~C`"#0'S 9DOYSYx\>. Ure Iw~fDZW0kL r)' k5pB࿆O  7g|!"#3ʿ.'2|)J8G]Wbe/L]Xnܩq|u8U"]lcD1gʺvvI1\,@5Sǻuq+'nsq5[uզ$ϲΛD ɡ$R>9A 6eqn~q47)Zr 汬nuQGx=Spq=vw`|p7U?}+r k%7vkm@*_A\!yCN.-fG̗qi,#pL9.ܘ#!wzTǧ%"l UTE6_.%=BʲPK:fN= N(|>2ݰqniM>?|-_>Ht >`CQ}2=ϵ<&R]yt\MOdlS eNCxHsn86ȼNSIw8Sؓayq=v0v`|p5o<_[z5H򵁩SL'GҧՁn[I$;V9oR0겸P'o|YO*Ȣ0@ ²pဍi j4yͺ82*uꪹ7.HN爏rW'孲S`}z'M#: ${WJVuኃUK, * \%\Cu |.";3.|h Gr (؁c#yX+d0Bdu8ɗb;ʠ'5."$h=_/~(]}_Qڥ:-,s"fpB3Y|2q"'uU za m4=هMqwڗm?֕j$ 5+ ǥ7p1!\9B}I-TELqG"Fp N TU~qs;?jpEb-|J`k< O ΰ벟:!PB $wy*;J$^k]v#иAnqqKZU@3FİܯF R u1GpP >#?'dL ҥcx}|7;}!0,KT=dq%'O% y>'F,nՍ>t~ ` 9' cY>(Gy"WV I*y#7eގ %j*[]*F‘肣qR@>#i8kc}f\e|iF80"^ WN"yoK"W8aO qz|\8&e9 ,m?iπi5U<_|J@60',(PIewj ~難s<_p]уQPMNeYϕjӺ:eI:7] 6MI&x9G!Ǣ\+l{t29rbɭ1i_!H)5@|lU8>Qaz=ߋI Ρ,0c5F_A- c@RdZ"c:qC, uanݖܜ;y._\Ím!oAaRؓ}ᦔH5U)_@0 _^ =8N-Y{ٱ6|1/q Hj {3Ó$;px;0>8μuEmq-?xے>^z֗xvؾۮjW _ЯL/hx _Āzp1!z&,?3dq:hùĽӺ8{|qQdNq# bg: ?q%|"T#|lo;iV]&E]Qʣ~'UXWX=D׸.#P]Kj9PY7zZ:>9h0! _y΁886}B#JV,.|)۬5v=22烋GĬn䳝IV_wTV>ڏ` iȸ}/D'o礣"u9էr.<;eQB׺,H hԕ28d~q|g_Ok(<~q;"??acqdu }~pA}cTf6#@59‡`,KlkB1748@&y|#GD=jcJ+bG"Fp[3S]`ʞҐ!s"-hIӾMd$cӞ\-8W#{7Rt1w˩1͹)r9Xjotƿ]'E('rPo;8C? HrͲXT=G%+.lN_ƳXyne._V ȃ9{+=aYGOhq}87!t|Ne l\QqQ8Unpy@ˮ4cD 7|E>opf:#_gg.X|lk~#+#wfxk/j}XN}>#ā (j.K2|^#pS8{,mW) 94ŵWQ`]ʟ ș9pΡ\;' '7.8&%.86Ͼ/uGxrǍXk S{9 LyT(D {;좆o6G X1vpw`|pU=ggw42R*cq|arn[!^| g7^#ַމ*z0Ml}jaA楟Y=j#O6fy*6/z"Ul<2c˥m{l ĺնXse7p`1@B,Tt r(*?O)>գ ߁8_}poֆ-xNhթnxocsd|,5=q' &.`ռ)9g= `^ȍ~o(8ܯ95|50ӕƠ8G3 1G3xZ!8"ˑ }}@6}# K{/| ;w2rhn&f"/Uث'K7 {R9.arQ;p]w`|p]o[TzusX$p'' I⥺? 𙍯?rnc뛗޼=yԅ~m:p erAt_X<$8i9Bi.ܘw#^&szЁv_ϯw+Dj\i?~mߗLQCXEDF>>ɐRA&>4[ @5֕B9cc]GbpP(M@3FhYW8+24 >4N:f„,N93/#8k>>d0B7Yv( -І.JHM)}Wᅗ}}fM'P_5zBK=dqRWؓ24E~yIzaWizvJ"jq0ފ_G  si?ב{]?u>/]? [7~y`8n[?UN'x=F. Ur=vߠp|LtY7]V.(iZAP'!k\Z"|+>s<_3pj}?zp‰#_c ՗pco];p@z(F;}~j}~'[S.1QmRqiňJ_#>bDL!3!G"`rI5yE Hp\bkrjxSXR9d@9R75]{2zUiܔo ҜN"e6^C!|]2QžWg`Yp\ =.0E 3q"WrOhҊ,C1I>A,;2?9=.~^~~e+Ysm݋ݝu=Ƨ7T"~Hp* O ȃy( WZ6S8aP8bBz[GnW3!QXWyj.8rp'<~&־Z;+߅~|S^~kLc=c=v_/݉З iGwC1`f_>#˲CrsQOd1}>w(_=yN_1g>uqAǬ4R&rhLY,< TO$1Y׺0Q%OcT98 >+՜Rj×> s)}#CxO4Om~480s,GG! _ؓa5|9U^俅؁ee?F v q`:_:8[e\*Xuë, 5}x_|sX{~e|j@>%/~Xp_yy :qAz8k}>STmvcPy[3^2;p%>YQXWy:OҬ˹! W}.F҇1ջךv#g~KOyoz捳iIwګǺǎ= <~ݺ8t`}e 4>ဍ\.%5yu sy CjԜp-, s, 'KRY4 ѯXwZ~=.pȡ%E]Yk pur.㴚Sc=Ao49G┾F>dUxGɶjdo;8\1-LΰWyHȖ,|đ {v/8Gg(;Pw`|PwdԯH8Y3dz'.͡Hbnu%HŹ>#Rt:qs˧׷G̃elk] ~7@-)AMGqⷱ (or5rO}/7Ҕ[!{Sž_@ | 4f4~uݩGq )Xq a9b!|5x ޴9K[_xqw`&5|u/8 c_ y+PCw[{R}nYDAv2JUf]|ELM'-+|c=#Wkp#v7oBL09|'\RMKP8,1BP큁Ĕcui YRHd5yʴH xf;My*N8 rpC gpًGa _+NS1C/)ji+Ec\uhr%@>- @1/ː< ϥ|3KK5w/'֣jZٜg)_q 1ȵ\d} Dw{V/ rLsVH r=^$W6j*;8BO+To"HLȡ1easnW+th2O$1bw.M:JT&P]̑vLyl -OŐ/sZ _K|#ehƍlo;'{ab^{Ԃ$/)g)' _!n'Ecvjq|6A >' gBqɦ.b㥵lOͯ^|'8}5Wr0A :ssݶ|]`hz0 Q Z-Q̞VH~#lǺ;E|(b,}0t!3Kk؟Բׁv]}eY{_η>'A&YPn#8uR½.B=6>p҉ތ@ݗ{a~kkݝjWNjX^Hcxh0xJZ\,1  WS_8+.8Mp]!7>֋sǼ|j%E]Cc 1\r3J4K<2Gl>9,H.—>ʵџˍ4%zT/I==ɿ??8cvہn;6w܁|6 צu0! j~s(1gB⒔Gwߴrk/^ͻֶww~XP\9C k6#ԟCx8{j!5D`T `v8ksjn$/²x.t{8`# #eK~MgW5HGބ}b/M7;_}_uGqBzˬc^P+9ǖ/ܿs_?=vO/ \#GfOkΉ9A}my>y>r7r0/f]L.g<2q%bƤMq,sqHb+E4cDFuýh=@cHsrsJaBsqk`p$ {Myv˩K 'LV{/'7=' \=)ZGa'Na:yv^_?s'έo!ҹ+ `:y F&1?#ǜBF| {z(iƅx_5r':[KnY| Ms>r+ˏ7zops_ r~NFp}bxc=t~:#vuf'=ܑO'isj{NXS5uf]7r&:G"7-Pc=dF,n՝YW#qq \qD r,Z.&%b8,Ų@CP!dPYu%#9c㕜Wq7W:;"Z,G(m {} č#q;׿==-|.uSeYبynuD4ѐ'LgCfبn5M0qo[z?^N~x g'/]z;wdkf7z18K Kx0Dl<R0z[{2sҶq^)N5u}>jxWsjsl?}h݃߂g_SgK`De`|dM#0e!!1"āQ0*8h(_=!\ʍySD\,9F|[7+E2V`Y4u&N=^|/Bܢ.]ץFpG-}0r^LS 0+v fvq{1SZpžbv |pc9s{nH9vd 9gIC\YgsK6wYs$ ו3Eۏ//O=~S^S_q;~lOX8(:,_>! :|:e&okX5¯$j*`[K$bՐf~77֏?;||W7C.|)֚WL'aDwSSN㱀@$ lY9[+h><xrnzZ*6|tCfǺ/> DQ8>B {8`F( 71@cxR=UosG]9ByjAcߏe2a9UE=lr 2x/T"W50ˉUPQ ٤s)N _*B^ 53u[NJQ"WaO ;q &!؁W+1NсNjr1xНƅ^ ,!7 BCP?ݴ0wG<*/M{gmc}"_Kb K?,h`\{ZO{Tþ:{AJv`$2Vwi|ck.ܴcGOb.M?~Lx~nM no`WNçvKz0`%yIT|偍? =zb@\᳖R*>O#oc]e- ƑY'SYԋH7b&h^rj!;G 6eq7ȺĎ}9@>Y%\RMKq$Y䃋euB8Oy$N0]l|>C =,hpޓ,sȵ涷CUe—݃xB/N[؁W+1Tz`  DeY4pnuD q5*$d@{R_0 㹼yyOݳ,/zusz<|j6n9-_8;k\ͭv^-q ȧ:"CI.;l>t0GpMzsOI+qvtԺ1LG_?=M~d@E{C@i׸sKiEv {XƼ^=aM~\pm݋_G"b-OG+ Hfx@ e'ÂLq0~ sGcBB'bl#03dEC$(,58F|{8+QƔ i.}m#z񭆴1cI4 {nSS.CHs)ja8Kc}ii$!8abSا?03t \!q;_H_BQv`__GpcY}CppZS`F_KcPhq)0.Ln;|/^:+n=~㩇N 'W.'Ͼr^+ y( M>4j=RRs}GRoۺȱG؅ gn[~bsa!0/}>۽wİmjFj@W!C"Vo8a#u~0_#8|兇sD|I? O){㮔+̲6u`^2+%xYP]=>(E4J0csF"'jc.KsSlUB(=;g= `^ȍ~jA<ˬ}28QWaDf9#k<CF% f%}K怽y6|SċGž9S|w[_Sq9v`ZtA8iOj+ Bqz 1ۂrWE!GBָqћd+WI eq8@g'BY9JCy߅W΄Ʒ^~=wnM b:6>4/)KKwa|Ah{*~Ɔ=K,1>Q[(N9-m|?>0N,ʉBncݶg'E#^HxFes#`T{a 5q>娆]6Ѓ{(x,K~&Wc^ @P9$ؐ)Me *O9 ǣ4!k\Zͭ#pP=8!ELܯ32պ(Ͻkpͼ$R8JqɟkYW8+p9]n;@cHs(Q0R'>}d4QyC* ~ S`YMV\= NaOJG̑?lB˱cUתӇ,>D@KMahNs{>gǺF>X.SrOjt58^Tn?2Oܽ,/w.=uzv3Ϯmߏ).k_k5< l|R@eQ?)&9<]skE}~%9`7Cn~Ϳ<'-gg-o昹\4N] ,YhX ݘ[ =g%vGMO9j BmxX?!pSS!H'&H2)=_M>qrrqE82[]6SDf]CI^DG3ACTX9b>,[uAՈ&vE:&SÇ!z%jm>Zp9X6wk,8oKS0; {`Q{hYZ{'!|aO>cǐnv˸;0vv`|a@IDATpm}iAI6 VI8$5|-5sb]e ,l0m8u2!sQB̔KI=1ȍ= $^ 6u`˽CRIb:,Wex#]L0㐯i[ gaO 8>gM\4w =r>&?_xۿ-nf# U.Lz<2㺘5,Cb,ˁSF)}z1*MٌMrSX$jT֋J8J5~,|ZpZxcO>^;/ÇA.bG;ϻ; q#ot.r&i#+ ۩|;y`?`14Ѽu"7r W9/݌ן_{{۔@B9Y1ԼVz q>YĦޛak]AS: vqҙ4_QS}]Uj]iCznY뺜#0dYy4LK  ,*{%{ΊJ 0/R>2!Npu8G#4(r=2o*N rcȆʈ8iAM9JdKp>>G_XWyđ=52AEİܯ5kp:3h1}{rjꥭyu7oMs^t1W.vҘ T>% k<$qX2i(6.!;Fx掣_X_0>?~{p/ .b9n}0`Q9nxW\wJ=hr^@mC",/Ƽ=*Un*fcSفPz>'Z3caQXW*<~16bXX/"bx#&'CTX9b>,[uAՈ8bac25|W:_Lj~ AbRz\#xUw K{ZFwX'!|aO.jfs+Y~U1v`A/Rc-rTvE|0q/Zɞrt\>Kyg^c dQXW, sgY.q=ԕ)e3.cui`ߴ<ϟ=av ON{Mַ7).p7G_X;?1=ֽKp/llwZ//[a>>ӡǶ׻>Ϝnak4d[Mϊ΋ncO^] p}G@wbe#O&h/9,G?+S~w`5D*.}qDce1PcLJ!u.e;sɶ BʲPKo`N2q#fĠ. Y||;},vՍ|; Wݛ|c{$! 5+4SR$^6bW7qrF؁5nZ| '^zs 90*ԐEpR--S"QOi5 je]s Q8ʙtG&'{nW9㯭͟\ټ_%x{rdc_| Nʏ ⡀Ӛ~|u/A-bö8;ڞ<؍.|սuC懝Ŷ_rw&ĤNn^r 0`@Rj*5Nͱ[H޴OoXAۑs_/儚k"k>̀vs tal y@EnF%s:{<8]s0Ve REu,Ab. l^|7˹i o/aFeu5du]k0/Fǵp_8:|usQN߬t n8a\Jmz6xa8aOEž9/#wSq9v`An.;Zrsfh_iFO|s!GRȬ>&7qޛūqII9UW$겸PEQ__Ʊr/G&8ק?_|յ=sw\ڜʧ Zd{_XW\ e!5zBu/l? kDQm_+oZŠ.s' 2#jٚ*3>-PUb@Sҵ Մ #G+=9/G XV|> T&0_?tC.|0l9TNUe%6+bGUWu6Y58f^e) ĸ5rଫl@ŧ9fؓK<ٓYgi8|2Ad4WTwE94(4$/)~حdXS.+_On8r؁܁A:qmznBN*g4X.SrpW ApuQwG=Rt+?OZ_>mݡ[[՝8 q[?y߱r|O޺lO4/ g$82~~纥)j i ݄*q[̺+=I.]x00.!Oh)Cr'_"s(ڃPr܇dQ˔hnbY= t>SyPz>'ZNVFc]G=Ժ1"~Ń{)FKPc=dF,n՝nPtÇ!JV'դlSds݁M6͜ji'IhR qžQNaW?__Q;0vw`|pзuZ_q9 '֧ĭ>0T|q idEql T ƹC eBC]YRFIC 4 !-慹 ? _:6}Qsdsy}imh,~NͅNݹ.Hg]|W{u/i;Q4/Yv7@.HRxWNE{x3wJ}Zƚ   ??)@ |FhEebda!ܱ:ȹH0\Ʊ%TY66_Ϫ1.bx>7Xm7&&s)|݋Q8CL;O} N5ck'ňy׸W_#{24ѡ9튗u 0cuT+9i3d3grJ3Dl /gGLFnzYb<|pJ8 O6ۓ)<ǔ_X5M}o=qO3}m`es=+;~DŽ}`&Sc [?s/_݆gl.>tXGoZmKU .q!ʾYP (fفc^Rc}" aϾ's[-ۭxq!&H$[ i?T`Υ0p@~/@[o7A:sj|[ĶP5jP8Mb#rKD0y!7'>~Li9t6^C*$5/WG('#^Yڔf\;p;0> ?LsM\N63qIu@OA/y#g9#JV,u1G S>.xHhx_iI^lrr1Xp"4j!r$2;S&[ߺx ڙ'~y_[[:yloMyCnۺl eqmt͠>WK K?t,O|+ؽoE't=.A+򓿰;nЪhΧ2ÞȦIn$!\MQ2|P]9 񓳱^f4!@bn{~݁\/BED͝|Oj:_ȹ;Es-C1Yvj\D-ǪqF>X.SrpWoVrhP7,^ vwK\b?uiG_Y}WV7yv㮵0_Wǹ_װ襯u?8x`fP{J6M"WNçv7+BsnAmy =UnOkǝzXW89v`0@ Һ|o p$Xn,B5v=5fG\]k]0c8pSyj.kq#~Ń{G3ACTX9b>,[u(p"@9r, U[Ai1tU q$ЍP< u8v#G"WؓG5֧[_pkc\Wo_~^ 9IqE`T_ oS9~u+ Z'ʼA0|c]eK,lpzV] IAHq<:\JZz.e^MQtk^]w}}ǕzmG^tO[z7bAjʉż>(sx:ݧ/-@3KX['e}9OK-nҴ5`:.fGbPidSy GGfesӺK ץrR9!ɳ"XK?8, _u+ZeY7<+sx9G!48 xAWcKub˷³"|Y1gO|DTDv'{tU6kRJ@Gr"V*&r ͹oW:"wዿI7;0vFFْlyk#.L /'&>r 9|#+@i]QG.rc.3!bWup'Qm0c{2UǒuI¯' ľKd ˷w ҧn<˗^z{?ܧ7pة;å Znw9Ϝ݉m\F)77oB+ڞ׸ _̉լ_-@l1+ݳg+ԦaK؞MME W"7.:0>>Lr|49^|bF̮q'RK@HˌjD)F]H;G|T3<1I1@7xTBUq! rPc=^egJ<6=,/3x~mԗ^Y}XW6szk~ګwjsi#AW7V -; dEKjgEd+ _qjO~q I_skd-} (S,d4jQY/DkEVS|P#b,lJ}L̸{dD/G<ܭHsx2z99:fuNiB<ߨz1qڨq#GȄou{dpn64bzB_Wu8Cnǥ0/ɲ{,|BNX/n9_GmgJ "j-|Cim$/ vQ7#P>/Bi|81;0>1끘Uq^`}, =tpBs;/|$7dSUIfqn@z]!9Na'EĢ*ϖ$QD*R@e^#I4bW&刺Q;0vFFܪ`N|>-/Lb u /#a\1=g['I`pU}J.z%7@Ӏ"(QXW*FgE0ʍhȓb&af|8Y`bs{,!^=Ʊ1(~I5/5F[,7Nuv/faG^1PjZ jVhîN3y#sӾ.4#2"^당GZ/_˯*C E)ŏ kRa#}i &iy|#E\!b(ɢyjIjj5ed*o9)` L W`ݗqnxZ~r#i6:1q>Wc)S}"lWF-;siR_3^&)4Ű󍹳y>q,}E[=Ag 1y}>y]9g.|>w/\+˃i;[ǟλ>{ЁõUm?tQXP2SLx"^MT"SW|w $5ow o&`Ν0v4 Z&\M/+۾Fό7]xf:zt&[}͋/'"FsכG~Ƚ3}޷O<*mCdC*恰yXbc 2 <= RB<ݻʫ[ Bu^/T =<1cMD.s+RTbIr~uULa~O=e`{eqc/]U3 4 {\+ 5ɇv$e=\Gvlc ^+5״lo 򧞖:wc>A_47ElcbSà 㵾x3)myMܥxܜ(A! K1[W%OzgZ҉dӉsWO#qe ~/OvO=gW2[9^hiF|X3y6ltݓ] 38v7q/dLzmM!X*6^W/3֡f;GyxYApy@>ay1ykL7~3$`cf1jcy|f5o#21k}U<Ɨb@ c: 2n10JP8E!xls=v U8?8?g`kS!qy\?+pۭ@v3+˟|:̀*̪Vcň@G+tQZ955<$ecM[ BmESxV\ȔB%y L.uv!W=a2U1o}̳r'DkA}a ̽S>/{.|qeZa 1$,/ʚQ=1ˇK;`aFe%wuQ2 ! (,%Z]Q3Gm._kmrC>%|J,ih@Υb SY5Vy&ޯZ0\{i[gLO&Vޭ%.+݊@=Ȩ*1\ZILa?7k)4i:>b[dZIMbU4ch3?>0cn6~o߀F?hc$4xG&`IQC2d1P&}~e!#FȖc^@yiIR_3-bgSUVB_̝Ů׋yUaYX;agrAo xZ3f;_=_<"I>ri$5?[fÛԂGd^թzaȎ#|X4x(jLOXciςCxҼ4w-X/)В٦'ry7# qT=tItP$O`1r{"H^!e=xq0mSLٗi `5]Ƭbi':u Ⱥ]}xt|ǥ_?&})Lx0D-{'||\𫬭tuot!Y}Ǘ.wnzXCz%$5oT@ԆY w0)ĺOoxJ'屬;fKzgBu黿[;gUl_u{ Y!6#űÆ !1͖XP}َORsjAϷXa@یk4AcWPr4aJu4@Ө_[$yQqD.4PhḤΖ'< V~UUy+9'?ߗ+4xXxܐޜ;XuvC-Pu"O2d줎9;=QZ5v6 ǵF- y);X<qLNsaSv:x\EGw/M|#/tpWT;](aQq؁pW $ ,U v؊Va#2~=tc`馄1 \@1 6Փw"PfR_Υɟ+QWG!V/Zte3_~/׎z=3o<*`swaډpW֠\C7ABF6\;c W`^ 7;XL~FjKnġjެ*N0V@HʖX'F5g 2BM>:}l̥大K}xW{1b-1/YLּ^s. 5[WG5q3u4M!/MG>+'-f~q֊^h_ãp[>Ҫ GG 5-\\q_1YU.)?YRnm7Z3 n~cM>pq <*`u`9¨Z`"2=c%KmC{ klP41 6s3r^,k̀ j =o2f< 4huZW:}r.ֈNw G[3 ϋϼy#g5. 2_ Cvlc X[q`mu' c 28'b!Ia lbUNHkĂjRv]e0YtlH@7ub (84 ی@ #ѥ}\y$|KHQZ3ըOz:co|r;XƟߺLhj Om]b:R-%n}qwРcb)imF׎aƼ/Ӿ|Aj6e} ߒrn:rƾ>:<4}<g5Hꀞ2hhA # j$y ґbG>g'bDc X^q`ymr +oMxӊšZXcmOᙼ4 +NVNl^B)9Ό-Tz>as(W, W~HRY-ceX;.X)zv,m. D;D3N qMkiI_S.lؖ7{Sw; yŬ 慙Y5GY0(X;lt-Su$(}MdO+7tԗnxq0m Ln4Acztjlz3]"U,5 Ƣ8V'AI3p-J6֤UH]bU4U y/ʅ׫9XcXq`VoQxC+oUxp#WgBrU7Z(Gc}U@y{՜~&PT6g;4vMoܗ9zB9:Oho:Yt8 ۹?)b5Ϝ_;魗Nd Yd TXoX1)L8$2EQ0DׂGmgҼcTg7},fS(xZf zD_¤+ ޽)Z~ՅK UWU@Uվgm0ذCOL /8^Y? f#  QX=^xjSv"P :pƌG^ܗcLb,H2$s_cی"ZU ͦOx.:G V8H)^\*f"|E|ɼY/i&h6WdnY.*Xcn [prUcoNH F: J^Ƞ'1T"'&3Bd Jc'5}lЗ̥dQz:OpEJ fK }u" c$s,AiRY@ea3+=c91Q%,FْyO|LLgD֠aEэD<͖;}<k-F^̣,vzH=K>=6m [zF"ގRq;;[{J ÓL@?Y-)E^1$]BY) + +?$ ,U v؊eX;.X)zv,m. D;D3N qMkiKp5Gڜ_S.!?~矜3w-?m د {:>$Xn~dh1؉!8Җ<5j#5(Cv3٢x 98=vŚζ sUB i=d(fXsž<Vve,q9:?cy ň@2bGs9Dxfzח~gzӟ +;Cb ̓KRP5YjMJUN}CZƿJF![`d:%|X iVjxFaTVr7 _%ҀR[u_e:}ޝyA#olmhhbHaonU깵3?JPXm=by1@6ňPB{>q> :oo|pzÞ*VlɼYG*sIz_Ve +p}Vzͺ>e|NMnO.s28V^,/ũ4wI=Gԗ~tIϪ/+qm"P=-̍kG糌cx1%(x3).xu\G)ڒ }_tl'Ay(asyvINJԮ1:*E '0I*yM;ciTZi R5,20fX L||ͤO2x!-nޠ. #<}LnSԁkk|{ޓqfcJP8IUgC.7z$bbNb.]JXϭp @|3oAbW؆ʫ -b rReDc ~+0~ps]gġQBw/ROO 2UdCIHi(Ұ! q\jKH:״}mD:2h=M셟<͖;Kw@IDAT}<kL}8O}k'O[{z504Wk}ROU&Pb~lܵ!#ijpj*<ܛk ӵr,uѵ*+P \55s>U^xGoWU=@ VṴ5Xh̛ kSwҮv񣯗?.(G^{|w@ʠF BbmԹOgtkYf, ȏ hj/0fc]ݾ=fefCyPEQ+qLFR[*oUIS@W5؏+0VƬƔ ?_y?8KWŀ=*o@>Xh@r\ {tsb`}2e;*n4lqdZ_OX&cw{$:W _:;ԧKz9:u if40Y"M>%y̻y@5a:ׇRh6h"B2zt3KKY3'f` |Rՠ"¨n.3J&20A5D t~ ]>%Swyx_GKkmQ'"&.&WG1^ D.Um'7a;SBcl+Wj֠ k\!~5*o1+_ K7TQhX7f׾Sbގ+td{XKrbB f!q`y7 ҥX%RHk'ki*g η:4ԗB\r>kO̭'A # gu'Cϙzblo6DCsmi1ҮvHBRi5QGXh@ap7iR% Vyg,]5a*>B+m!ZW aZoI0%]opq !#nƯ XcJP8!lJS?ab%2J$+0VX+/*߆5 o2p߅[5h7eD2V:2J䊟ơAa}Jeh؟O >i$f$ٴkX8$AF3"%7Fbܣ^|j/?Ӄg4UG,Z| WpPZI>FJd7>k?`@Uz~t]S\}St `B r p/״pipB>6WZUu7hYͼ5 ^~5M?_xnRd=/D(&ȹ<&I}_ (ZJ1:[jѩk ޴bD1Q |57HHZ2o<m%2zN,_D. +pW` | o~[BGk9X5[bqqSz+p^NzLgu욶/0*34y-1wjy<5ܙ%7~zSZJ$*64E"tI䲌^L+i~&+  !W@yUs^5px]F/u/pcU=ÚV8Kk/]lj8/4^mze3撫k'&_żrno>̄?܇Qp:y~~B7>q[lﴒLhӾ| (˸ۗ}~'X˼6`q/bׯȅ mX/ /.,,xC,o"$}4Nb2]'L]M:J a9GlİUTJ ҭI&XH6[$1ྊM?с̳Kq,m @m_UC˂{gh᯿:rI&$t@,pQ6I`^,1j.'`̗%cd'2HΣ) $;:S1k0 շk_g=FZW[5{¼r 5뢢ѻ8 -IBQ p BkS0Y70xGaU޳7tnU W[ ys݇?W)WM~w:]؂a)ڔK.*+O4!cb vܗƪ11H]kٶ[>=&纄#W0&fAb7 U2yʯ?6V`XslbԾV/>Lb<9Z̰XT٫tj쩫49žs_ϚQe|{/̭1a7d^Lau-χ53^O'+ ry20 (,n3A]UUثf4abHрŠ;cu;*U V@5WTUbxCK|u`uXe壺K;`aFe%wѺ3J&20A5Dn#?s˕;Wn||ؾ[Nq lL \,=O/\L igr б+0VfX Q{@F\)YIex1!)[b6u4оg1fqqMێ8!aUoV;0q-Q#ise"(Gԑӄ^Clqkp_-S_lżȿӏ}X=S֬³i~ $:{I՟VT/Ж{3=jO/Q#[NR1y*b6#hKMQ(-JT'HR)`p4jVO ٻ\*.պ HGl +M\ڰlDi,AUNr|{z[/=$ޓNsO]Y/|~:7i/.t"v'5 ʞu +&{m_bڗ H͖>HDʼ|䍤X:_Vh +p3@LM^^+:l<` +cfAD A[ q˘ƔCJz>\+^̍{01|`.AtdiXq91 2v P<x|{O+ʡy|HX43A5?x$$p7/B(&ê! (Lyahs4W2/="B#{>4kV)PثME E^ʠZϼN ̳&q'.5m'}3ON;ݧ%K!LO}ɀ G|ߥPkR<7Չe/֖+5LGd A|*L<^+';0ǫXRʓfw]%_O./+0Vf[|W7x|qTe??˅jDc V` E{nd7ۈ-o3|G3'\XUfR!#V:3,J䊟ơAa}JQ4B'}%ĉFQmfbRȏ=4zϼYqsMbO}ezo)Cڳ0Ӫ.ҀBcz#:H j:C?Q6z$Nffs[uS׷cdkթ^z1 H"PM] 4BG«`]dRSk>%Jkk*LOJ99iU*>PCTꕆ$gG‘! (4' +M;'̧^Sَi57KZ^ZsVF,ؕӗG_uͲ(+?)6n%-J ^ p$_I`3 /-b,nuv.K_ɒan|K#_^狒Kr;E5c ܼ+0nܼ+XOCߠw]?i2q2xqFbP&8Vڗ Pa!cbqq@N ڂq j&Җ(P#t2`OM{'[.zӛn ft#Д ܸ@@6ŵKC>dYfn Df#cI7d:NbouL xO;FLd\@qwkz5kuqMZ+6kKPG%\ã{Ү^9^1ŹL53C E^={_}>]'>,_O5!6\ӻo-x|ϑ7#CU[*~Mt)323Vx Eo@ 9FxF@sN|h+0V`Ϳ";ҵŜ؞bCb^Pq3NJ :A1hFjQva3%T׫UI%ұyJJzӘjp_) Gg6qbd$k}8sN{ѣAR1ʞ2yI/1S难  Ql ^WȋDME4\JZבO!So|]0H_ +e丯KCxS<* ]ͺQ1os~mKI {ǯj R4z#wDž/n@S{ Mӯ?wYnlKO,c}6ƍÀxmk_1=+yԎ8@fEPUJ^0(V_I.J+0V+az58 ay ~NLNQR1*PB+ ۚ=1an7"Zx!Ix}O }W|(qbXUU9e+6^>0񸧄f"dBIׅYZYٛ4+C?s^p\}݊,{jO=71ҒJLwy<6M׵uW*dvK+µu f1LjBq͇ҀIPIj~}# {Y& LaxXpm[xÇpsH;g}_#!7ͤKׅ[6rC"opC@o dn`ibKwHqnk~3@yqUcN:vS6jOL@ZkͰR?c bW`x>t7oО:Q,L;:*? OZXqFb^IR)}XYqbq7.ҼƀҳY"uTG(@{hm_&"jY=B'ݝN|j\!e(/ !sR%vַ!-VB M5 1Ͳ5x<^ #B8)[ͺS;*i_G܈`}>8!&=Cェxg^ҍ\Z_T2mg/]*Muf{I^l֕flne5/ׅQ˜c%M?WV=$S7_d\mxLbI4c¿/ЗnkX' /lc xŮ}n㽴=qB+[ϐu8fbN{RF<$fx]}uyѾbWń;_4\trfZŐ,ZJ x4&+>7GgkvXܷzӃ7 Ĥ. .H!1H08P)kjb$orH ,Z)7i;lsixaes}3w×WqȴƆS53cdk"gx(Ե^}.ԎTZ5ԕGd 7JMP4X> J@83^ki8d2v|9}G ź"ZW|BQRvd{ -;iVRu#w>]rryWr. 04&I7&8s^/!ySRGKzowHqnxbõ S*fa} -v{ G8V`X[b [a&n%R-O(ѰȄO2~%E_-kM'-w4ٳz+&ҳ0ky  axrEȠTE(8d@l8T;31]A8Md:sAjiTIC/*TW0@ E*9Sk`(o՚ QA yaO ؟c+ &ZV 26 T֤滍 QɱU<e?mZ%HLP5 =ڐi>ؾ|jںYF])Rc v1 ~̿pa<oiF>%C\41 +.'dW~ÈM Uq4~Q.? +p+p+>7]}3]}jd37sIWb\G17!#6"Hy8@_9 d Jlx墂'tfgz!-Xh)I}iz݅?OeoׇG {(,zՄatq ꭪A*kjb_bWG#YE;AW +}Z^ܭIJ*}~vsQ#[N7M5umU/]uoUV)_*< 5Ea}>, ±pJDzkO;|[w~p!K˅?-աVn = c7+юiiL&&r3 ąշzoi\/3c ܢ+0nܢ0xޠ%c 29F:H([˵Y^DbOoKyZEofQ%ǮᾺh_1XYz2ڗd>9bbi_yFV"kQRGTi'lzיϙ,DvVy yh k^uZ#ppr{qB5^}}w^eh-zcՇ Gfe^ ytLֱѤ滍zkwQ_#x:iǨt@Cg>`@CTrNVEb굂+p姺xk/O=uW_׏{K rk& |T 7%>ҿ#ǷQ7{ύi]FyQVL 2Bϊ( D*'j +pKpK?/[lC$}^ :kL|G xHútfCN_]WFlvK&эJi企c]'^j_3!?O|lsG&$u ~n ŐZkTĽZ#q$ :x@r 1&a< j՚ QA Ql ^ p}>) & U=!iUH}Q85V|Lalj^{|v8+EQRmҷ|]~B}?o\6fh>Ѥ.1z|#to <$SǍÛlÒRUYl?"/ȅ' +pp?/ 'RT%1fKnXGTʣk}Los ]zDl\f{ x&}ĉuĜKOLOh/⠇@(.Eh)pn :d}n_ޢ.|jpe˟7C^$YmǪxWNRRSSk> w/mz#['~DIU>;FyצWoHQpԵ^}.V0'Rӕ~E>?ߒ@KyTnIn[QnMdI_WC+17ʗ>s%SMr+0Vv_qvܠ盰Ӷk7uTbHL2{3:hRh|L 5+?vA͡O~v֏ۚT (AQf>̔'<5Lɛༀnz ˊ/z1FxSm{/yxkw~|&9hе݋cpVիkB`wTޮ(IF@۩4_cTLo 0d}ng>uWNåwŠR.a.Ror0 {[e^{q^y43`^CPF(z7`|%fOMk8ډTcuSM{<5}Y>}e~{u *#)hKz8V`Xca kX!]_8'i\ĩb|G ̀ʄ4FďDƋ}>&7ҳHV˰cSXc;xx_ ieK +@rOMwH;#w j!}:~ti鞋/LǮo@:,pw W qC]gj5. FM]׶\h1jFpԫ˅-3'\œ*#GfUN>z\ b1`TʨԼEw\(S/GH^Ⱦ@LV`iO^GJ\9OM|[K.q.%z&(Xe [WCpXc \ ײzC8Dqv^ f<=|3@1&RDOZWEρ:ҳb@!Xy}R,4Dgcc 2^U(Bŋ>I <8;t+ Mϟ\<>=p@DVϓ *J)C yǞHQ+jZ!v/B  (ߗ{){swm/Ll;0wsL[>^MP_#.x<=VcڬBN~o;FES;OFy I&(XuA/[BdWInKu[D/8V`Xc>mH^ٹK$<HuTk3%R1фE0jW2.X=J"z,A!ԹΆL5()k4.Jq#o2n\UAi`&|Mۦӗ/O'ο8\89u['Kn2F~49>LD5 VF˳b)%}7w0\ko s«|TI9ԫW5:8荁<e!h5I@˛"z&z`h-C)[#$\x>LV`tvO~nR;7Uh͠^qXc \ dlH \ qΔ$27 ư2hoFyVjz"=$OFtbAs*6!&Ö׎"L1ՃA״V~2@#QՐY8)5j1tN7Ny; ¯ <{Q;)BM%u}k{ZUؽ \.}Tץ \xWE]$Fpw|ڡF0_3]:< 5x{_{wtocf%Czj +0V`5pK8 V p:qŕdb~xbts1 fѩG/u٣/.=bv/ tddQhc]NwILqh"cU_aKdzwP8F#O!uC{kYK!0ɧO wyzX@WHHD8)KAeiS0"<}BW%OMQ'oU$=^vTN1ʅ(T%[ ^ѠK^Me^HZFZX>|3zSNTw~uK?v,|\<},6 [–[+0V`Xu^ag+s> NLLͮ+&cW(A'2Ňїz9 Fba ecehO恽5g+|<"c鸯.Ͻ5n\L['69&"V[rQ:4=pQ-Y.?5t|zPL?s{1bA6#H#4qڍ6_97z2c]T'-3׎z9UG*͜|y.)ܵF0 |~RF_@L,jje { k.=t8.ۓvU` 4ɍeDt5c +0Vຬp]q+O|AȃBGjq0Qk8ZY<ft{dRh9_1|c,B["BV5;8Ow%6黦+rӹ ;o 9GCA֦Wx^k{Zw=b&B+Eh Yn Pvޡ#&@Ǻ}Cڐ10$Fpw}>^MPO̵fL—jWyC&j:C?-2oDZBۤbůN ~4Ҕp v-_簀t +0V`p+89 Ns3.\y?q58(5r{3j4\+t X])5zg!YotȰcS |q( cy3@ST lRHk"`.bX26Md_sk5F/N/8ʯ }6ȕX0290.isw]=+c *n^*BA4/j\}o+,*ި,z\čwoͿR?l/;vHsԛ( l֗;9ܷT»ڌb]g3%䷔x [_G͸1+0V`X ^վ=g)c4 -𓇠ט_<[uAأb_.1sc2i(4.c$&#84*󕇄3Hx^vx0N:&\P8;]_8?.xe[o'ڽ p"Tչ!ՈK/^j1awmS׵ñtK.$s̺^Q*#GfUN>z<BQ fZ#i> txS?)y# Q=VMmLـ_ ϱ7 H(,C{Ҭ 0O4 b+;\f9`M}uyaBU];=-kFH1|+̫!"lG =MGkzڽpّ¼'  IMgWVH-/Ґ4Y\˧̵=5xO+ry=swm/Ll;0wsL[T[.?s_?S>rȉFKy.74Uk:3:9,-g Xc 7~͆bH d)N%^\tO(&MFܗi=|G9_1kC̝QCWԩfj%B,Hxj I͓7yf7W5{c\gy]=Jqk/KDh3`94z~Sg?ι\t_]O͂?&6c +0VV`s̀T]H͸ ?dq+P^g&תƝ˸kXuWzV"j_7 S,grTܹSrcmc .-Κ玃=w/6 _sWvv?w։srC@ۦ[]dC&$yIV 8L͍Cݏկx[Z6xFG:.!<{}k[Fx7#GחfYU50kWG5وsaM n^w=S?"X}%RDey!C2jƅHXc +0n+y-Ej8t2ac'A%yidb ^}EkzU,M8ɦz;]AdK M$p̟6` ׼p pqЍ:|_ |ե[?pO'7h|3;ǧޒ_P~61.d!I_vp#Pe |ّp\ ^L݋Z1O3ׅ@BBεݾ?XE UMPW㭵bLZU">{sGݝɍi`,nUtMr#X,Y=M>c +0VzpWt 481-k`k&$$ < h]ţDd㨞Xn0mAXDz lK1HlKݸȏ#th,X i->AZHX>("w4}NW>9=wr:vOh3z(l nIWUmjJe/U-R~OVy&HR *mLTfk.Œ}gNmB8/o4ףcW.>{ӏOW^|HӼ !^vC/j۠f ^fGbXc \ W^ I^(l|לg5H]2\5aj${ \tvB_10lu_˔8jܮW*Gk$oذ/q.r /O;% T7 Z'}z Sqœ=n+L> +mKEpON4X=10"%R+% j 6#,rX -YBhY-yaXm!Kf9?K0 p`tOݯTfSuo_Uwoe<'~ݨޛLxs>΢0Xpy(>-*?/B᠑G4?T稕IRd 3v s+UkU7B\ʿ+F gE;8U<g!Ń>z#ԇT;zS^<gUVmِ1ֿt̜z`K;cbiے450v3'J]/C9,s[K<|_S_Ѽ_ˈ^7k+CM-c6:#@ۘP:aW]T>ċm`mNWߘy/Mڏ |)/d^35L-Sr`~ҝ% 2Ԋ WwokprT!xnnk_{1"z_RՅ]Ki{!Ϥ1F>&w0g.ܷNųpjx\ܒTy1,`zk׷oto\GFFFFrib?-#v)srq!"R8 ,HzϺ B3TPÐ7;/:7P8Ek8%zf~%6 K<6ermfA=OXA@`4S+ܰ<;SyqNG/VOK k˿ 2A:y;zԲ^zyz3=_tωXJ\ CF y )$>^ $%wKRԖM#1g :+X!xr4/kGT]"Z|]$6Vs8.!\l^][^/N_ORJLvӑ8{uqس`-Ɯ]m8V9 g&ĺmڜ{qQ1WYWq/PL۬5ۛsabV|FdZk8ÆM[9jegp=Zlv7r` ~Sa,ys˩7>KV_.x~;8!j`_X Ch+$#~u`yۂ\j n6]N[7 0Z+}gN6"Vʿ+F g. ?W9``n}ᩋ] Dxq;O?(O['=}:ر^%t` vtOd9w@^sdK_ ##9M=˵:3~W1dn ɹ150(GH9Gi4i9!p4OSI*\SN7HYP^tw*FmŊ;q'^tKs3%8>Nη{q=1=ӗ^ziz3Ӄ/ُ 4 , ?],ĕz7,98+\Q"\/|rU,bQ~ w^ L!|][U䝻Zˢg.>{poNp\԰âDC8Vݯ"g"jֿt fG텰%2@hnjC4N!3i7sU8.@E]0*W@ ]:~Y1#2Ǝu1XQ~_'-udRnzQ6j&H8l[LK'𶃮/$/\^gLY6=GL?60M_8~sg;'$V.^ۼD hp}AH)rR.^]J4Yˌ+x`sҿQN-/BDrދ$ǓNӛq?K:}tG\8=>ZP܁:Jr[p-I20y A`/ռGPx!e+s&I3 H|CXxE\(6=<+;BH˸xP h0Ɓsen\6qC9b\ωۘ~Tn ťgK$9nXS'_ӄ/ӗgǦ3ӓKD sLN|)89s7n/$\g8VֹMZ)L6]s{}~}9GxRuVuS++x~/~_} iFϏ8)%uN*sʟ(_ښ8j vlԁ a[yb[Vo1Uf U!CuTM&ƺh%Nҕ{pOb:Of]ޜC 5z<'Zj Lԡ iR'T/<6fo-cʅ:B” 8GhǏ5_޻&|MMgxkf:;y)~DF^̛!Ƨ0 UWujEVu*x q N |"MHw,୳_8xNo~cFsU;o}H27ǘy{E {t`t`t`t`t`{6nж;= lU˹^dlЖt.x ڮu&6.k\Ιyš@SWs!7*A/*ҜYXWX|j`b#fNXJ:y7 (gXuvkV]DϤrp- hhA7?2tEz~D$p6tNj{l${VC8 ew4ēd|ק9=_.E]bMa^<ƒυ`K/\[$69j0.Թo`8Vp{q,j.̻M*vR7ˌNV=zQ[Z[X'9N ({сссссxpz@'```[XN@*[pUq.< (f5 r A`FSikyu<,5c AI|Ă70Sᕤ:ǂn8%zf~%6 c?(ŭ0:IeAN^lTZ`~TnaAKr(5MtҋN~9gqi ʱŽ1O381{z=8J tk܁j"ab[snqsXSgCb8ˑ5&ĺmڜ{qQ1W3T^4u9ΓYk,r^Q@]7Ѓ8{IU W˴<ͮ b[\ǔ un)7 :q0юk2͋g[NN胆t`ؗ^zezI傏ۏ vo~A [=ёv^lсссссcxp MĞ[ؘ3J .TR8!Ry9ˀ%Ǽ"ʙ 3#]ܘH˫'`sf^qw\Ȳ*.ͺJ8 {q8B S`]q(s("rϟnJ[ 8rteD<q#t #<8vˀRxNՓ yB{=uޘd?6rwm'd]N)l089|F[m#uP?&>G&xÕܨ0/п.p UtDRbz=-{Xc?cmct`t`t`t`t;0^wG;}SsߌӞFqTOC0O,rBĹy1cy'sהڲF͜ .+u wT>cqYqR:\aӎnɵ<Ip2 A9hNB,쨙P}~l;3_'7_8gw7&Q$CU4  ]ExC K5.B4_?Fp_Ǿ2eQv:d8 e`u8{V <\l;b2:0:0:0:0: {7[M 5g-9:Df/4ESv$+<2Url%ŷg5 f]%H9il١@,3XxQȓXe~%eRBqJ=gy>6 n갿c@WF.j7HÂn6=-24xQ[ٷ ؀\6z .o9qfzsB.)R䠿Tj_%2aV2 n89YL3Gsݒ\M&1g/u𭧟W~!ghᓞpHz o#g_1zPwwX{u`ثmtXroclqyVX!Sg`m83ep qޏ?^(}yA|&KnB䤂Mؕ-N♣y1`mQWɃgm5],/`WyGzyct`t`t`t`t:0^S#L݁` ͹;h:1vsBq-#Ŭxuنs9<VTkнk];#ek8`6tmzue"P0,Qgӭ F,gV D<0V s"q(©^w<ؖhhya\f_pw->e #<8vˀRxasafn[K{-wg`/M/==S/Mo b9gmRQՀg`GM }~1'k\_i>ǟ2O` ן>?;)Ӭ&Af?^IT!*6{5cM=ߗсссссcxpL2u@غүgs9wN(%"prsA^`1IOr&6nqYp\<[iRCV嚃mR5F]r(JӕY X 1LveESBL6Ԅ?x(!|hf78m@ٝڢ hevڍ.EؑY7{G!ícSn+tg&#pqL΄tx@`d$v5{Sw~l1@/Oϝya >f'n2rK1H($iF #p NN`~> g.~,!Ԙ0/ ~9h=Ϣj0FFFFFx_ms?h[TR;HsaX9ܤ-^κ_ym5JߞW%X]JkX$#bS甸ِk'{y@̲Xiva@`>vb!XZw9pʗEr4`ENa'0& j:%O9hkrd&6nqYp\EyRK:\*(g+Ľs|qwpoE(bUtXOBÓɵ\^Hӥx=%u{,1I-\ܘw$@3VXfgOB 0e⦳ٳm| _83>t34=t[.ZVyLxN) )֓u.%:]\Ps4dʩcώI\eЫG=翌#=с`  VG}FE霰$\uB*XWwf370J 1̙XAHbSn8\ː#vшsU_(6y|p}mT~oy~`dPf?x ..Qf HC{=̓1\O9qֿt{tٿb$}p?"ЖG-m؇gpKF RRTƖv<Ʃ͹sWr&UWq/EQqw4x!aR Uo/x>YqQW!.5d̮ї :zp1[* W0D"LG0K2~IZpAv겟 9Wz}wO'ٓso5}s^>xizΗN^ VfbVׂ(sVtWHQ1a_8;*rzP=^?ct`t`t`t`t;0^\zB.{ZL w-cK'c 5".qxu19뚿><mɡ2<m׺Risy͵X.甠zui.j41HWAȀ-Te,Wl)E82ch@W̳nJ[l< G!sq;7^YA:UO6'-xǪou'3}8<3O޲_.xwA%rntMBz zm%?Dgq>P8=ܐ?YZot`t`t`t`tR;0^\jV+oMrzKM胓0$^m\Qbml9\xFf.>s™xt+Qř? rTD#V]gy%`n5A(qCE#x 1϶:ԯ0$1&/XK7,4Z;AK";r{e7L{_n?zzgzސM6M~O_glyђuNp{Z[qr:Z}ja68.ށ{8V:-A.{_@8:w!b ƩC zW7 XjDhJ94_B Ν3C׀J]! d|ͻ86aɽ/uYGꋯ-zW 讛*ugh:kx`Ĺ.rgKO07/; bx N avφ I`r-'<9UΞ}~k^zh|LYgK"_8w}9~J=E[>ؿ̍`̭o9x9׍sqmCZD_9h;'1'yp)#5Nsq.zPGC|D%E[ZWj`b#b.sJlJ,D! Ưa ! 6r1!ܔI#%4r8ex rJdM&L!^o7\$+]W -rqq z-7^FɪlUOPrm=q9qpw]t߇N:x3gvsW[T*o:V8i-[zǘgBϝCo=Cссссс;0^ޫyp [ "9mI{b+9J< bVhb:G#8999Bm][ꛥH3gP- 50v3'J]qV{Y[w+טڢ~N_luN;\P0~.cn\utE:R q )B%%6e]@3p'nk1j挺xg-At][ms>K1rN{z=g u\CּOϡNtсссссxpzmr[N8|$fvnq86K=u9.>3[3at+Qř? rTD#V]gy%`n5AV`9g:=Soz[톋ys 6ns ^pE:u8ڶ0gպ\Eax N!Iڳ]LP&{o6s6ENJVc`U%r_MAZ6jm<ճؑrrlq2hLFFFFFq4XV=s\_ [9 z,1ͻk۞YUq uҩ)yrygbVȺ_!]q &pތY|γy=yNa#1Q0B bXu1O>4X]-RT, mׁI`n4D3$- .L.d7CٳT͉yk $uopP ht3$g!.{%lhcՁ!;pzu[k q׶x1:0:0:0:0:{ {5"m[[5ιn഑X1+N f?5mJHlϡy\iG9- q$ge^[92Aipv~UTk'hx躍+Ň8p{9~J멞ŕuзC5Q:g5΅z\pAlҼen2i-:F[SMݩ9L5T=z<=jXQ8օ\EɢQz@dvR80 Xl!hx)sɕ`}dBgMbK gxվTڮTeE͓|"xuR`"WxM 7ƒ tl~ΘҀb;8+řya=?=߃5z=rֳsRbﳾuS{^ˁl{=eӏ/~olсссс󬮫Js=v~lIWxf N[TȦ-"5mX$&9dgbVFΫhqDۺ \5r"͜AYe^>#!J`mrImt( Hc:U\Am8^^A%` DO fZɵ<}R7{E &⥔v:2Aյ)Pj֢]nIJgpNhN˽[z=Nǿy?<\mMla-^z_mo7b_2OZ<^y|1pq=/potp뛙[jnNbm >3[iԵ_ue&.>4 :h|+pH` '[yKH 4I4a>m݃[:QL3<#khW1`> .s70}`qeA nSߡ}zp0Ab/zˀ- >uoa"Bq+o=+ٲ0.9rl N(E1M1>l1'bJJ\z9UZO,^s'0՟( g>?{`ссссk,nJ{&q`-fwݻ"̭r tծ q:o~0~_@i>GK{.STpu4%bn>ٍBT%hvx«g0.j:bBv<-7 z؂}lHgާĝ w t A?+Wtu2eubuNw >XXs 9g p9=ҭS[7Ѫ 3%I?S?b?{A'qҀ?wC>x1&1FFFFN k˖*7V{`sTX32͠fKQƩy"T8b ,G 1IYjsq:qNoKaq>^ҺI]BpkMe@jQ(d y9|"i1bʥr]z:l%W>p'?(LJT^4ZX jkGa= Z%/cm\%\z}KcFQEz]R?۞0xpe|e}v-qCph01|JRx[eV6i$kւVxP6WjHFn 6spxHr$sI$up%_ldCCn]µe%fwG.u p~ \nVuux= i9Lz5lgsybMt[ױk2LKsh `bK<=H;̓q%|`G<i?WpM#@IDAT ہ}WwaOa&e; Xy h1!|sh{0縄LW9{R~p_׭kGX?kVr#/fhϸf &`CWQ1=HHl9ͺВոhnuÍCtXے+dS`V}iq#\HC@ę=6<(:an(,@۸p l ltwt~& 9ܼ7'w uGyW6va<z9-=.jѧO_~YdVՏ <ӟ{??ct`t`t`t/޳ر};eBydyٚqa a}e6sAEs89_=fp`q-éo/׽3r̰W^SXշ?escA^ </ct`t`t`t`vgtoݜ{8˙O~< GM"1|FbŬV8U5p8a IMpp{ C{‰ѳr΅ ɧ=<\mwTj8'.WUC'O?boۄL=Xº>>v/%`-+C_0ށf\sdnW&}r,^l ص8ͥ"ǒ7 a~_@>G[[]R1{l6nwaᢡMq |aX\/0L!i:ݗ^nncN%w4G34 pa_",>K. }:yN{XR7տùR횧:у]0N/^?}Ҟ5m_h1:p,|W8o>M#`&71:0:0:p3u`\+;6#օ$n0KNTE;*ƉK_{|9"Vydm)յu_b0U E99Km΃cOqhA[qE^yC^[WǍ# 0YFֲ䋮KƱx`qۉ 4L~8`,cl\ 9sS ^X mp%zIWtSlY7gc=])҉by7ys±3 "4쫅ig8x"l6f}ΛC* qHkuÜxK@h)\װ 7\N_,r)úX1dԋ)F`W/9hz&5HaU_ \s]C>o1ıKֱTp`0sz~{XϿz w_SuO;.>eg/~_0:0:0:p:0^\yɍre+Uqh;zL8pˀT2x2]Kexei=l(n6pȃ0n !<8l[- 6хd0k6 7UlʜbSmbp^&C`I+fvb=pps.ܔ tM\tS"/]p=!=6Q#.1\Cmܸ'%7  SjlW|~ q.%]tVk'nGd[ilx#_nnXgٳ'w*1FFFd\nDb-/7v8c=x-"N9sI3S KNN,R34f.[ mڜp A[żKjxڸHM,8f%]YCF[<VX<pq(&0*Xn\7f:u ^K7q|8kI|G~sFV )o|gɃ07}YNOK[]"XO\_kϯZsɛ98='_87&;м8=?Xo0?6}և>?JY̘܈x[|n=oc\W7GjO7ڜCƉb5%պo+t+Dq&S; 8OA{Saf(ulDwrd&JM`x3+)D(*sf E*:Kʷ0eK-5^`Ж.\jK,YaWCqA+cu[/uxíz-[an#yqN0n>$p_-ST˘'Z=}/{߯ qw׾m|w|Ж|9ссu򱭑mp):cݱo-pgA=lk?Ŀ7}ahx[_߼o63:0:p}w`5[=7DǷŬUYoHNZձO<ƈ93g1iFbw!bɉEj&V]&b%`k$Sq!hkӺsI9_-{kMBҎ2pR(+m#Y9p(Wt[1iѳWFpHb vuNr~D=Z"! 9gĕo~Zm2̖u#k!G4+̓K'_'+Z T˘'׸UQ[oOϷ*n7B.83}GO?toFXXՁzj_K~}n~u^haSc8q 'n 'pW +5%yTDoy.qjp[>7xp:6yFϓ,G1v1T5fW0P ܋C ҁ6s ۀ .\%6q2 4V"8Cl=C13\"\xk#hxXQRW^d-`(w-{^ссL-;iͭmsնvǥݱμAmبsg}QxsUf-QT.Empsseܼ}e9m0w0Bylpzkb Yyw]Bbp`"Ml+$` ..bxllCACjd?/Fr.#Mm fl3}|AbIsh #Tku=~]+w<"p\xS}?~t k\aϩj%/Xiڱq:=w{;%b"X/MĄ3 ƸMs4] uP3duv4tĘ߀>o{Ň-qX؁F|Țt[aVs 2wK+v-.J rڀXP'k56rGhƙ#u)9+(5ٵ:7XhDf${ 6I Q[aD0YurWog(i<oR6#NuHk0,mfvtCX!}HG*\!3[i'7#S&9o8dCXR`Њa܋C\k HҰ.i @ p!lcg#{x\cu78 bW׽2`'R[lL7"Ele9j{ʞ̱լ[z=NhUl5g}ރ}ӏMI.I8o=/Ͼ~-?pC-l,ft`txp<+#b_sv[f3{ڜ{qY _wKsa!i$&s䠝 wՁyN2@kQ׭h]m.]֏ EC[eqga5C(XXY*&6nv.Q慯]ccݛbh pq8y(rF_ ]Ns6=ǎNk7BĽyѻ-fo':0:pv`I^6CE>cHbv vĤEa9P΁ZYlQ2n]oFژ;rFs\E9\WZLZ}k/ "bh8svqZl< y*\e´QLt)#7XrґAڴ|7&uhneZ ݞAG&/tK "Q ha^} <;tΑ2iplv}5pKwnS{o/S?tvpt`tFxp?᫴v[a$Vcsnq۵zj.흒u6=J|.%{+5=6oM]m\k'9bmڜp Al8!btCFJP6C]ErD&\q˹a\ۮc:%p1-6z ʢg"E7k']0u-|2/P@mq#"vp#z=87!VHϏ:_<[}wOɓh{kUø;pᖓul?F7q _G8ciˀ ρ^^;mڜk` -3bȸպ E1K45K|U jyp:L;8_%Ax9 LSZ‚ 708li>ӴB4u2pK>@v0rfOy<64fCMrn< Oj7ʦ"ς+q{7F_/H Z^r1k.SɳO}lplzG9]8qrـ3ƍҁ=vG-oEu\~=bOAe[0 pJ\k] I&97Q#\;ؘPYg"9ef1٭SM5<+t!̓o"`;wsY<"l@Ⱦ,2rwbK!cW.jz*Q{ߦ3Ej!zp2[ɨ]ᔼǺƚIS#%E jS|OL6>aܰx[=օS'}ca7w _~[0,[@[a}{ДC.YV3b"#9^]8c"9JRjo;y{:}]ʽ:(y8:0:0:pl/CH;oʸe8ȵՠ3. c6w0Xe@P΁ZYlԟQ2UGZ0Aͧ\=r uiEx$t).j<[p.t|mpA)C08@QrV׍(ϳnfN^bܼI#w#gr1 Y(Nfle CbJ.6C07UlEj z#^i%rOp}q9<y:y]cO~K}u/LL_Ƹ;p;?fI#kQxp;| {#A5ĂcLj/v 翜#sQ[o ƥͺ|p6#̟պE8O\گl~uqM䈵q'L! saQb<䆀.=<`SI J{ [~OABI=O`DH .p(WtHZ95*ÞcSJ n0aݎK";o{QVuo09.1O`ԅ}#Ov~`]2'cm_o^z7g_>s3>:ub^ոy//{-'j9GFFʓ1b#SιrO,"<΃gSbYW|g@5aZBڜ4 'n4k8UZ((%tqj<8p`hF p7dž^p=a'\KA1%k@,F`N ?J̷`. j{,+!-fόDrXץH[V=Ɵ%Oj7DKLf,ךA!4{>!aUDϏ&K>9݃yh>vpz?Zۤy}NO{uӋ|fz3g_ezәw2œ'^~nz7GFFNIiBP"MVؘe@mlB1OL6y'f=и}׺* ˓Mrn:Gڹpwl3 Tkjn2d@5Y!gU4X)rU8$KhjC\!.6f=Bx b9~-f楚=^Eyw]'6n\a"4*w73 (eX ؞'{. K ї{\-6rҡ\l?OpݘL3 fόLpIx+ ]`n$d17.պg7j`-F:MJ9}ZQ&x"7l˹=Ә#>>6r#On_0eν<=5gM/}}?{Yv]Uk _CΈ")Q"Y%[HLjC! @y؉ %ʊB3 _$>4ytw=:{߽UMwu=w>{>~Uuq_~ݿ|N\X; Gq yH3Qk8}ۭR].۰TʑMqS]/>W uŢ1Fq~Uoqʎhd¼`M1 %_=gVځku߾ss7D5gKZ6K-rM8iFbxʓm&m6-9쫹s8,X+r~]DuUlñJ qmo(:+c& I]j X4bN7eE>׶䅐v[/fJ.q$m4t< .GCxĊOrg0m3zyfx.\M+7l{ϞrQzVrl\w0Mᒣ]B:d+|ϤEoTÜsfooq,M|xgӦ;zk؇I>$!>FGyww.] /p7+/ՃG>ф7?yGvwX;v`ځfZ1⹍YMwQǹdsI\,i]+b(cV uZFF]M +t RG)|^ˑozVYX:f`r͓Fiq[Tb46r2v̅QmozE+<Ω3ۗu+gmڰa˵<. 9Ou2>[1} :k`MGc"쉂qp"'O1uw P|S9gY\݂ugkiC|4zpo-3>/_m@YNj°}կ/x?O ]?7>o!X;v`ځc:>8A+|hK odڴ0s{ 77HS#pPE;Q\:q]MR)GE3kT&q6TWKcϓc]u+p\o,ބxT91gc!W-q4 [t3Vqnۗc{tUwg@QϢI(OF\u۽Iu2nmPPx$da_N(*WQ1n 89lbY{?YW6r A> $&߆iBdNq?{~Oq {zã|;o[ x_}^~zλMXܦƮm3>xp Gܒ G!5"^s0Aj_T^ڈϵÀ>9ʫ<>.+RƚbI^uV-ā@& $.mN ;\pcw)R]j?]0䆝x4p;]+<7=0UbAE=#xhDb  +%*70ĵ n"Nn8)0I9#qԳ&= y3hvB.э? /q :Oo,{_i~'#>~AFo:-^C>į=|W_/^G]Ǜ\ ~>ޮkX^;v`Nue Nbim~t,6IO it#1K&x0YmYuMް"&<3`>eW8N,!>F v)Nfb6*ݤsj|ؼ2j!GM# ]Pr{7YWks)8g¹\'S "~jr|'1EM!q>moΏ`g}ܤS HMu?+jэo/>g_ڻ /ٻŮX;v..0oIöeM4nat07o/i/(aDu.\i\Wݯp{o|Ǣq[>rϠ$kk];>x^no:x&`q4%oh &^jDgEA~`+S*5}0I;ZKɟk\ 5 X#k85jlռ #f+E{r-In!BÜnwntrIa:ʥMxDSUr`-7P.zژas%vo_K̷ΰ[}36ф<`(86OK:#1:|G"h>o|o| B }~/ܼ3N[;v`;weZ; +sܙ`xcf7W=s}N9:\a`t;DۈۭF@ʫ).H`>.+]7*If>W ^gR7rX'p J.t+vDuĵ@ţ{rc[0 zZ!F`{Fƺ0@E /jqfq1-9̱sD +s#.9ߎiv(gtcZ Z;Ιgsuo!߿t4<ŷΜk-mi0F.~:/:s~qF~? @SĉΆtvw|ݿ~OvWy 7.G{yn׸yq?ЃOݸgʳX;vu`}pޮ opdswMNuG678]ÆLW@IDATK U98iFbuMfBg (I#h«!g*OyC$΂m2dsjDگ|^qӘx [VO2łɩ"'&yzp'atDݞ/aM-oºu91v[0ڞ2SD29$)!1a_ЍX2l#^nK L]t~ZcYMб5/F`c/fn51;AT7 z'yI-Z Us]^pfpxmd?|'#~fF> Kw9P QNdtHx鉇_?Zҵ|k_#W~GI*^X;v`ځ[؁[JS: U{<'fgj]P n6<ǵ WptxC1/yGFɞ1+Z<婼BA]6ITK8-ƙR]y~Pj'o.mv7a^5u5Jh93'P1 Q,0'c|č0c~MEGeXEXhd uE-U;)0aby!1ɼ ~R]ic}T0.pG=AL^߾,.{4Oəyzo lԩ3g|V?gˇ < bי2p še#@O[&DW^?z=__뮭k8Y'm~P>7^$6CC.hܴ0 Q+5~K uN'?jq?{7+kX;pk;>\:k?K_n~a8pMc4DpzsPnd\57h٢r8Ѽ's/= ب>@0Fs.xEqj %+-#AjrŁ.sՎQC\`%-#znQD\ TFߨ ( ^w.puayٗ6QXB~؍x^棞K{+ҁM6Iq(Q4鐒CΈ3{rjG|e%vƹɆmca /@0[#⧓5/i~? @7]Y^SN<=?_wɏ| y;/=kXaځknk\~ᗀx+v5lYΥwv/p? G2'49¡`լ6sN4%|]ngo4`լ&o|C8R\E IUW8X|WX[覒\hG11E,/Xr1]7tL}h;(9ku9ӻa11WNwn`H9Fdp2b4̥݈.6EGxoċ. ݶg>SQ3OJ:<8š> 6Ǚg4zs91z&dzyY.%bi?@g36tp he}N䉼K,(5~w {ʅ҇?a<٫o]XځkQg5|.3/Ѷ~ v0lMg8swÛ4NqmBŴʙB'ÆQvŵ'SyBM,';׸BTWi OamohsclwU+Tہ ̽=V)peW8b>҇^49a͕GctLp'b8mLjuxM6,at\I9}1"~l9lJΙmzekWsJpY}_hqͳ:\(OMdwTaXkM3k=0ha@K?즳~zX;pvX]L]v+ ^`L{8&Zmp;xpxaE3 cg MԈx-eWs (6'Og ;ڶC~ [> ] _.Ai# `k1ŏxuV'S3µ^hHoB_ֱv`ځgށ|Mg~{ /m/CxM]%AjoLK&ܞMU`Ή9uFƅ_ss0q6W8ik\[LWo\ua̕D!1j %X$%IMUݤMIXT;*i!FHSĊkS#%t}QOBN9TTK4&y WxY˜ɲ' ˜/\A%sEs~as[̙_ʳ=?N>A3]Fá> Ud|Q/UoeB f~; mViXH#&N54Qgyk>cځkζφ@[?@-7 ÿؽqpi;S7&eC# :f( "2_ٛ2͎+Ul2&qZ(NsiV&&#W2Bԅ#&yzp'(|:+:vPu3qh\Wa>tc.RrS2<p~@gx:8Ɖ/C/sy㴇QN 0?=5cځkζζϟkq ޾Fࣛ?+>G d7#6BإxR9Sf' PsL,RP1Er@\Qs!"88/UŠ酄j)YYr Y0:X69̹X6p6tؘG%srK[w *5OnS'‹.a.#zڦ ڄ0@ǥ$]іX̅/a$l3,]0-sUwM¨aGYs~s؜ڶp;4ߞR|KK~j E)m^~H+j,m M-g[ַ8Aͭ:7zgramu:o \ځkYg7O>{]66?&D\w21iS$cĉ749*/<8QrԺ{T ђ /@ :UЁ}?nUE6_o} B05#Ex NE:X;v:>8F4`׶5 `LmQUу^ j'xٍ 񆩣hZyĕ9lPWnm!"I0'f~uRi9;#md-GԉDZ8oaQ'ٺ#*ʥg kɪKߨC^zBg,! 6=ׇmj~c f`QX 2RފyP̜pL|;AT7 z'yIk z;?%<| jv1]+[%z[pK <0FVibiGDIБ&::X;v:pܔݚLgс#g OgPß {v`$h .m#()6P<$F_rxehMpVj˿0l&3ᕺ :-/<`$S)߸,RU+\Qm }˹ ɉq^v#ra%@K1eҳvP$q% \Wa>ӥvE_Frts#t2ia.Ec#-uܳ^bKs~asSrfooq۹5; *v/iᴃ88 ?/tҦA_痒p ΡS [:&idG-8Sgaױv`ځgہd?7'{. =SCjc{=Àxqa7iļ8yV4qa WdG9&qE57xafgxJ3FɎd'qr#7["F"yp}mCmVRTWΕ[Dd \X ;aE9` Jg&| H;d_),7ciFjZ l"OEgҗ"no%oTCώÏRw{ؗX :3*!oiЦ狻;W>O[7o_;8ڻyc>w y] 3P`>lPG7JSܗMK ~̓ bGѴ*9*+r,= ب1[,OULNRX4J gn㊩n62XZyL[$6>sJi.$xk D󣔶EpU).timbĺf}f_TI;и77FՐȾ,a:v&-hHwiɚA8zs9?8y]?Tnn70'o7o'Qa7?7LKpN:Bj79#^\\ځkζ];*\_瞏f7o«̓w~?xo{2.l@c.?o p`}_:Ùq4Eě5CsgoCjV{W]̀a ԥ卯<+c1gqEQ$[)t[F { uVW2:5H-^c/fOtKَ4/q&4bN{F&pըbv[ܰ6їFAF7s1]+~q# 9+~%r63?em}-j:/RlWԢ(o]ev)??˩:s/Doo^G)M= hȃ yۃ!'@ #u@NiFױv`ځgځ$f?0Z}vu9εgO?zxvPX>\:O4p|.ȇ|#Ft/' aèud frsKy҈9mSFX↠u)%SeӫVhIBǦvc b{.6.lHϥ؞\25±/})CO]9qXrtt0!:lp%8InBZLmrםfy< ]a3Vg\`4_璆D<¥,ҠGt0ŧ|휡C8lфA[Xew~7dHX;v`ځl}hfxfT;#\_ཊ}ysOv'_7'6U{ &?ooڧOXg',c| ǴmVR5lkvgee¡$c#,67eEKF1иjmQjq9Xs;l%0KXBFwu0X ƻ=[Kevmi7#D=@XkRfcӺcohΘpfoI}gY^Gizk/d/ 5pcxCq8Xlo;W:ƙIÚE֙#nrK?n{oFg-0|kX;pXaS*f@XF7o-r3×/s؜ry3[i8L9߾-9e[oTdvYt]~#>`P_yRuSoTl~5.c3y0QƒghRO/܉a:X;v,:wMglq~:~%~7 6 7G} opd˳?/~? pqpW_^ݿ7zC| ^;Go]|n]@Xqoٸcz}eh[Cy BmBۤryN{DC1gQ\a E)#"nK)iG,Mol/Q]=I&y],=>m{X"xpX\w]2gmD:;L67.i_01Ƈ_9?cfΜJs;9Egyjop|L0my9OSG6k؈SF>I=|UG]gW9҄d  xi3cځkήʤߣC5\>CxƟ pd6)'V#Wm Tl7p`s#(*7l4bN\)[@;n*.sisƕR-=+1ֳuΤ/[r.E.uVFV0q̪y4bNY%VPB3ةn8wbi6p-B]8ti̭ ϗ.w;)4o`qy)u34xr0~_0uXM:zJKN-Wѩuf٤o'ѐoN]kX;p6XMesݛ 6› 8r a4 a<Ĩ/\䃗~ۿ"7n~˯ѓGo}ŐOx˅]?Cѳڸ'skܽŤɸpT4i[¶jZ)s5vq Ecp/_޸yӯ/7zm'{.>s_$nՃW.>w#poߝ#,Žk5ğ#sY.FE+y$)4^XkX„+*̌a.[@T&鰃2ZKaKhiGMol[؞u\R=YgdtGxܥnݣkX‹.^ ^ p<"} nFɺ;.%Ҩ5@iwm5AmWX/9kJ>V7iŷÑa4_WN՗f4<i:18:3@ᰝS9Cϟ۽~:X;vw`}p{|.3xs=yt?cMmd(3cЀf C.Ji e_ }'o6|yw>\=1ܝ݃ x\]:¿.pt~C`4}=6WЧذ ѻAWqګan.yͪM~G{rI#Fr-2vqjKhs󆘶 T ;ؖv؉]j`,7tk|[.y'Z _"L?pC/{>fpK{/?/8(68ğ h6rwț \:#qp9CwCfDR.Ij8DTl|0q)!ns| kW{K{o|뵃0n?gG܀+,< vuG}j[I"nG(ץAķ͢r\a+8t%vPPӠm[IFjcie3QQuCm e8G= M=BlejMuOyN=cɺ{)ඥ`\TER73`3C|j ˨3mi?Fl{@acķS|pfrBr4N{ Tuꚤ<, S{CP8bSkm8s7˿uW6^w>ҹkĺkμ3oH\vy137rN I#4c粨3(W ,ԛ>K{!}<xk_ÏbwwoĿ6.!@ <o !}:bkG0F7x}1_:#μ\nVk+*7l4bN\)[@;n*.sE|lBG$sEnŞq%D.9sFc m^ |Vad,5\my4ZK5Fo?!8:l;?yv89P{~'C-z8?ߋ7"8~gp~Y!aځ[Uj.q{~(^p -kzAhpwj'hAvQzZowҏ=tS|w/_zioK ??yU :Ÿ |  lRQ߰O0n˕la0=p C˓FiO%un"Αxj U Ih7l|w\)ā\4MӆPxKaf0ŁSgï߸<⛍3 HkAG8RNzaBICdS/pPP?`ױv`ځSwqSq; Cx)j礑C>&qSkXh+/-2.tHM;+/]W 7^kw|wGkwvj 7vo\͉%*gᵋ%>os0xÄٶ05\1) [)sܭu-gJ= bi$Qtܳ\77Fܢ#)%uյMplnJռakkJ[zO̙)1nO45 @]-L;j~Ni8x:=^q4x |v 8᧲t 8NiNy/V'sLOqb1܆NjE'z(cځv`}mָw?&? lL6U`>νqRhp>üÛu!r;T=oYWa4^slray{?z'_S/Ճ98:͝ 7\8sT]WHzbKq; [!ķ;ҎvCWҌ3,RKXե%2[u Z vJa> ^;!"cfƷVq3g;.庵.q-:s;Lm7G_>aG;vbis:pOÙy }0@XSrf3ZSMDN'ČI0w95ī /%7| OϞ翮`[ֱv`B YwL׆cչ숗CIpJHBMA0K |D3#r;Zv?#/~好o~ypI Bw<St'.Rwo]fO.S>01=qrC'6F-j3V̵Hw..ɸ[K6g1}0*\sFXoy>$:9]!N]F!xskt|KV+ 'ϼi~^J#q鴇hNhp>ҷv8s0QNH&<0=2/N/1[sS=']*{-O@$^ 糯X;6B:}7G^7]6rVӈaHY5.$݈4'L]5vbZv̵P GK-o}6n+tc&owqFHntPxo>b,s#pX6zODq^u[]w4Ds#@ [<;+F"-N[eȄOB%7hDS`"4棞1xsb M]t6 '3s؜`sΟ7Ky:hIs؜Va{t_K_çZ3m[k}O?/.it9:wDVUS$5C_?AW0#u¡iWt~~MOιl_~<ɿ/_řP@`kp>m_7AXL^7~050Ûq%M7Nnyա|-?|GDAx_;aS~sc?FiSJ8y DC356"DlXYW45[Udں獘cd#>SKصמ~7Pon'9֩PMQVܼ8%/ME6|d#&n H;&qR{hFYX.aU\]״QץrxpcSwcsgI)G=#kaK*"t[w^'ߊk ̮;6In`ϓw 89xE>Ρww]qZ{7 ׿Á]?ԇ So P:* L(JR < N]^$,%.b8Ϩvvnq#o8.cgZnn.C&|8 G`cg]qakEMguP&XrXqkcG؜^4wX-o9Μi=uVjۤ| 6[k0@0שOMAѷ BH,uLm3^8y`|/<0wBდ\qs{`ގwIS[|gQ˟w`Wcځ[o+6{uoeA G`l7 c^7C|5Q@b9j]}\.uUW/&╗uM4\ HM;|: ~s_sѵg__{7?Ggٲ>̽㮂c+M ư$wR4n8/AGuvd~Sp rڡKc.A4s3c7cԵd-? (> \Qt[t]eo֛fpǥ9q/mɹm7ҙc6Zz@ +ŷL4"~)>S`?8Mj5i?biw/-tƜ"u_!/NsC0?5#o{9{ r/|_>%kΛuoƋ^S86 pH89 II\A@#AVՌ$ a9Qy\y}+odwku}N K} ^?⍝qͣ'vz-]'cmo&,r⌮KlCz,8/FvLR\Xӈiv1mu]1=[(5x]*K=,`^&9#Ҍ5Y;t:m;x \ŧoN?_9$kk񎪄:(Not#L6{#8y88(~-eGlbL \Z=hʺl C\i~º**CկRmi͛m}/q'$͗zƷ|?xmklw /Y..j8#2D` zi\b,qY{<4U{E 4oQ-˜ɤ YIƨgbC!Sp;t &eU]#ha+x,\0 'n'szs~0Gk$_GK~^Q[>Z_}A|JO}| gh "=vk5͟*AA;bϸ4i&iN-ʘs>]v?RZU-}W6DK`ھO,M?o!5t|_qH@l$di9A7}JmCY;|#<xg?<|:8ή*ށ{a&X;7kLƬ ԌэĪC]}\WQ Y8KuUQ]舑vup>8ds\O>rk}~'c.(&8G 'E,enc0y^A&vSڈX#.1m J3BT#A_.aN3? p1:= тieSqK=4uQ6Բ롏ےsnY\ s9a 8՛Zd #g8yS\yo񕲠0I~a8=KQOπ:ͯ'<3S{}n{:^5䷞ljP r:=Xܞ{ULǁeo!bd7 FV O4mU9 m!Ddu`lXYW3lZgϕF6d*ω@?{gu%la[Kx,>/Tƕ%,Byjy0RپDMA]=rI8. 77K)9Gy6 oR8z]r4Mk 4tg+ǖn. Q=K6'io!~Ik 046'q?א X#^ &|K5ZVm?ִ9/_m:&N)~苏NQ |-}^s@~-^l <~ y<CuX;p:>E\e#fmDZFG61غ(.fpiy$4OL4r,Cʫ6aoZ8AuȰQMwº*G%ijkNU[8: :\  ǜqtiFiXts+9/0¥{axѳ\ FBi; `l*.s$DDf _@ӊ7W;F QC`P{gexm@ts%lG7%OVsV?YjKW e|/x؞;E]Z q~I)ÿ'o ' _ խcځSt`}p;uG|h?CL׼Xx%Z%M#-U\p j lkaة%Fr7-(ҵ_kAPE)9H7i3m*zelOJӈ^Z\7NeQ_"V> .혫vC$K܂Q=K]:cۍi˩ps2 D}1婍Hf\i$E2hj̫>s؜muqZ5!7'kK |Ung89_8f 7R#0&8_840.~MZL+'Ec\ (uLЗflyIXځ1O~~tw@ځt`}pLVwn l潓`yf$Z4^SэĪC]}\.uUWܩ5, fSc̨.tHM;|:8GPWi# ^;Nq#b"Fx'n Q#Pf&?э+ [ɺf*cx%\G ʨ>iGpEMy,@nсjŇIB^;3V,(L!Sr(%6G9as zyuNVpΆ;&O!`éip6ы|O)x!{xPud/T}(fOyJA@rCAƋG~A#:(n| OnvCƋO [_?Ik6t`})w W6޿6+O6w1)k pͱ@.:Ni8F]!Fe}]\y +jƀ9uI1*>y=W~-pD̵Ω!q0CW#' Ss:Hj#!<8(.s"]kqAcC4o<x:?nnglMrw9 Nbadi30#.棞uܬFԡef-Y[(1^3ѺKF.l=SSk?6a?o;5I> 6ǙG\è\ pZohӐ&/NeӉ,|ȇ&t~ly&a(_)G} uZ':q߲NߚT@G]F;G#nb9Ԑ.ij⿫1dT;}sJƔ/cͥ=]E\Wx}zU{tOc!p!/oR8R0UbK= ^lFV5?{od[v\ݚ#Ѝn4 HIabl2EϒHJEY :(K %[6#D GI 7֫*vf U7]Εke:2Von;,xS~LaSiEqxFuUסXy G K\{)]uc҄}kSdL_u0C8'#?l_|`Ρ]=؈eJxa]oS=_#sw *1_^z1`S v 9^qj#u =.X]Ud^Iujqy QF^G+s{\TH:QW,֞b)1sM@7ư _[-P`4TimmrӜ9]4YMņvMX;qxkփ}H3vT6u!S)?7%N@9Zkf‰S➱ ):O>oTx9~xhF8ڛkgL[8Qf8c:..( ^gzxw&!MCOa ?)/۫m_j~!'> fsXgz:bedέ'r#>"YHr3Ni< T\  y\ Fr7?CԕeKҹ_4f1dE߄ 3%J,9[ic4xqT\]D^[h0TN7A؜X73ư4+(zY:޼pSqmΝ~H:- SQnB\4뒣ۦܕaH*sǚXglnڍ?a NM~$a.|k<c#~ƋG;1عZ:cڽ ?x=Q{/9-u&C3ө:95c =JlY,6㨗?}M߄/_4l-%;<%_ۿ(@NX ..Ny9G_Wṋ4<&Y3ۅo[Feuqy.;Օr\J`V`8D]KLɊ,# ADt+Ý 87pmnz3NsrۧgZ1I! 1:X3Zlے鋜X_[%\̛uw\& 3"qᔦ 6tG? ZQrFO08dY H(iOFъ54E=7?Rc?<6?S?mǂC~q/N ?xȇX=dYMx`(M;E~=:Nu)Ƃ7}wk/'K/}|ɟStVVy%:x)>1EOfA.a܌̓r*5 `QLUE'3^>A/6::`=G#ht3:y|3^NF`޲&urzC~iB@u0A ܭz]&v\·]<'ڻ[ҁWWwCNbA[/F *(H~Hq&Ne¸Y3WKnrTzr _[-P`4T4,sܤ#4giM g=71~ [a4'OɜL6w<?<\UK6+m~a|3rN'4a$_gkJ=*V$}Ф^royG5gL|¥Sr~WM<]z˫k{wպ>;V\C |/,cҁ/ /$t5]eϭ\Pq7S4Jp s@IL&9x:be05̹Dnć]$=ui5 1\lvnݺ-ڼ4刞 [c`Q܄Q-(V-7e! w.CuQmSʰdzt5E:Zդ7¦EM" 'ˇ}<@p2WL}&JL>b+.0,-Gs٩,㲟Wd].wXw)fҎ#>v Vg2Dx&Sjr#8'}Jp> H *B7]stCYupUżYwǍ0Q,VڤsʛIÀJcQ޳'.ȁWF4'ce`3j8rS9lCg7L pSl;NF8bhGsD!R1I :NJ?_xҥoЄgZaƘÓ(t|mh Lqkj}T9ii$Y{0nqΑpcEYJlYMТ1^.+b5혪aO6?&Lp+C_]a28?'(K6G}<B't>0ru]Gɱjhà:u'~'>y+b=W:p}g_?/AsypR]thbdb``C$0JG\PTe}/pK<8М5Cq.X(ji~W[WtKn26Su [D]n:Fy+Er^7&Lx TǡsI<Rư2; v (RZn?M[:̭mcgЋ"vrJanaIs\ø㌶ ltеh@(AIR 4g6AΑoo0\+<(6z6 3?[ʗ8Z/,t MxهT|2" y Ɵ_u~)~#>4XkrswWbܩ ^uུ?ҽ~b[pX:0сDc1;]7`fV6rAQ.?b1m(1v\ ,d=>0A:1&7v&PA|qrg&4ݗ҅q1GUf7_$D.̃?Lwe:.PҧтtucyNu8v,>y ^\71.[Q~pF_Lwͺ4]᰽vi,pl4=5*ڎMY_T_6G,z0{t]n B\7=ۦnLaS~oL-O!C~8Vw5/&KkT6&OCxaͷi/t-y3/MPcƇ ;y,^fa0c_a%NN`<58+ ۻ?,u~{{L⿶ʻ榥],f/;NX=/5z-= (4q8Ƽպ8wp, a5)G|`4X[\WS]Y*e?( f6 _y3XcDTW$~3V2* p$g0O)C81:g(M NaS y(9::HM)a,z^|M-cM2ZN}ĦKB¥O)18` fa`9uth"΢I {0 ?W>TaWt`{ۛkawg=qTG3Z:p,>?eEC WBFe Ч}\3B>aͱ7##N"Jz^u߯kms$uե8nl 0`H=J)㜱ͺgf=Xaqɝͭ'u] 9a&MW-BsF8rej 9rSYe_sll(n1\YOLɷ*&NG8ТS:3ϗɱ˛_s|X:p=\;*<>]KNn"W:_0Na~hX9JyMYx5#88g¸Y3 (zr_='Ꚋ Z2/MK/X ܯLUDb 9wU`"͉Vb ͹X1= ,2L'1#fsasڠeE (7OsӖ7skpTg>_[3c8FC9mp`r T#4ױ`"pKʨ c>9 ̻L:1/#'norIa&aOC/ll†!cg^ün k&>4;wo:pzm=f8ጁ_]ب-;=ޟCow9K/Sķ9wǔ/cl$]~It`yp]\45mq9.3/*hV m_P ]A9.sJ'F`s鈕QDFB9WpQ$A6a耗hz}\N#coV"]"#P(6&O\ Zb=kɸg4}׬Nkv1.mK5b z >ze'/(CE8m0gϗ{bf9О>XG4Mus2KӴuXcr=Uxm{_9~-6[R߁g=vjȾ㨎4GDz]:pX@4Lօ'7;ix $)Pm%m@5Ee,dƼk!J9"K4I̶urVG8渢淢Ꮁ 41N(\0@7if 2];]&s.F1fGm7&C8v "кecг@`mqͺG0rWp&żYǥ` OڙAn _ܔSyf}y42,3p Ms_ĔޔM^>SzS<5<ĸ믚jyZ+gő@X])V81ѩ i:⅍0_o)v06mv\^ҹe,~;~ߋ0i z2XLt & `B+tPЉ-d^y~Uu;uqĹsrs]\]ZZDSS,I)Sqm3en")7²ppS. NkqsrE}%E6]a؜;irxh7ѭ;e8EE+5ER =>Q I9V3FΝw-N5i/a½llak1g~!f:&Bw.Æ6oAȾNN$Mӧ*QNl1}/i]qZRR+Ձg?ל;8QA>#XZ'݁IwtS_'З΂ cGm!̮J\Myҭl] 4/SM]9D]q5uA,8L9~\fb-m#mEH'4[.s&:~vT̥|N8ZM1t"c2n6w6 ~S?Qn-n6136 C;<@w5y; zZ,w6=ŽgtH;q6?3P'XÔވ!{z؄m~J1^8/\}:dЄmodIPz6ӗ@pN0F~ϭkv+MX:pon>Z!qG-n7p`>Xx uyrAmh9C<X0@|ӑǩ{kz.8G?g7Ke|gHwio![)y~X~mQ1/ L?B Bs85<FIɝ}rERB }zEe, ySvDSp, a5)?j]$yqi͝Fb;~TXnJA"bӜ:`DY3q7ff$`'s\"nA7bq$75Lfpnmw$xOIhk!"Ym OڙKc4-C0?FS~FMaS5in+8<|̧'pN___.~s+c| C8cCR{l>fAy,m85_!O 5: MK ͯ8WBb_wkXeK9r~{w|aQ9|`'E@\w]hM./ DOq M&jG!_]s BpʦK~ fddczUi~uqy͍W]oT A,r`9Là;Q,s-nS {#AQeχŪvn1q7!hZ_Q"cM; h O4oj7BKr79| opNV672Ž8qӜSMi' rb S5LG֙ur< –>R/(N9\z}ʊ"#3l~ Q'üvap)NsÀ7Z~gNkZҲY:0ځ=PmVX!w5SM0o)] f0mp#9WisTK yO_'zddJo?X7C =?ɷolqs ;~ο\׷w NΩ8Dr> (Upp<7~0-}X-^hoŒ:S^!}\0BFhJk~qO?8{qwu vt-d܇v@IDATƥ^}_]94_`XHZ+6`/'ցrj:SQt :Nrӂ14+v0427⦹6t3F{.D`b(\sf;j9Fw%2&tӰ9 [x„yS;A`,:`\ Bʠa i.<7G\LXÉg~ ' rb S5LGzyԯv~4{m:{C+b!O?Ö{g~I:1S: sKC} &6:CsB!K_^[5ŠlLtSg _zW3w 3` et:<8N.:MyȮ8Fsd ѸɘUsQVj̓r"jẽ 4~M]Yg.Sׅ^ :l]F/~տӠԵ,O5~S ۜ;9ut"Qu36)5Z.<=cLNbG1M:ܴ\8f{=&ahDŽ &o1RC\N洉Ƀv-t:tT?y'ы5L~7?Fc?u~ wcO'gvmWvCS0QMWew~ym~=#\:6 Cp;9){_:|W/gKۮ׷7ه}}ok|:a 9~c.X[SH/ )c["p,͵~>{f^I IkXSΫ@:f_PM2G.0\ kxI hH9xaNQ #HZ\jnҲ1#4,sp /&#ʰXa $>ǝò[A,5q4xq!3ݘvr\Xic4xqE$[Qqpbn)L[p ;]kї"&qQ,iW\n|Xs&6.o໮0;Q4qy qb SY6_~~{е;}ھ~U?g/ m|: >|l9*o:C~TC>+xNZdihX^O^10\!FW|vz?wmpC;Y]) dҁ:4_0TNk (<;``gqhx]|M8^`y )_$9 /q(l󦮬\s( /~@̶px]Rb9Q1oDIYmD4;uV!qtS:ir^O Х,'tDOlbKDN{hZ4?+ur }~WW^Ge,8\py<Fx\ªRxHAűtXy uXY,xRyi\p0"t`V0^ŎKƹOO 6l02iЕ6 <6'w;7,1 ts8lB#։~g\K6ޓlc:g:{ۛ:-210g[Բ[:pXxKA_os! W~IXϞJN_sXp]>./P e0L~.nbh\uɑ.^Le"wSW)`Xr(:Ӈ!(9,^snq9xJ+"WaQ+ \( isY r*űMم(N56$Vm1&Yla$x`27M(tay䢍URsSǃzRj5 ]VPر1>[@;qEt3j3򼞖{ qsK) iN2g8}h_!Na*? F{~#?s+w;\?x!t` omE:^Gz"'r~$F7-±:i~q,V6֢tOr@8:$ׁg꯰tt1߯B*fD!8}etX }Dżq$i?ƐvuYŗ w{bxQ%_4Ii}Y/MӗvFh~'1s/]+o1/com|WQWa³@QcChs7T2ҌQ'0`1Ja*L'30 9ws!1u {>˅H?zPw6uz0`m\Jfy{,\8@Ơ/۴پx\$MhM,E!T<5kml~?_x'wںvY?~*`oꀟ ֏law~؆f8v?˼sc߽?-c\;I|??ZOH9z(YH̑"IZTkK؁:]y쯩|(lv{v%ᢜ0v]e+ E4=6ЧQ^luq<q9v`5}+F955 &up,e܄kB)[&F,^L2FthlQ|($A')&ɞXw:,7kzu5,̃ 60 ͢Zs}X&&.N0`0i7qh:MI¦#xz}3}zׯ~"K6wJ_.@6c1nѪ##WszC1=oT6?#1=8wq>lX:>O?l#Hce#Q+ֆpV5p:/-\:Ys-,'7^ҏq/'ÁM@M<IxokxaH엶}p Vq/X:0с>8>d< mq761U)`H7XetD:<86."}@ )u. ߧi\`0hQ^2\Zc-6k x%GL哶.5P4Zp/7(\W氘kda܌̓]KF4&LY+NMXMNF8X&> `, )$*eLcX3sd.um:QZb =Fc4/-!RrK-kē?c,@vwx{8~ďrZ{p,8t1Yk-͕87q_:pXd7@w^ Kg;_0ABtTX=OFL,sFs?8`)*俙 <=&ya7u1j"@[}؋{>hыbҾٛrDs%b=GXFGa:>VpS6Nc?77R@Ku,.1hz<מ0Xw~7;49Ql , bMN.S~Sؔ8̿x{_xoAҺ͉w`.Ho6>( G<gԜO}_#&? ;Dq? D;k'C^čǮooσW:~xBxaDXRk8I g篘YvKNl"U;<|Vϊa&L>ial4G8 ͼl G4=yX 6Ir d 'FB7\cock{޼~>؟QԽמ^^ˇ/|fVjqpc8Fr{6i#!_`kna;n憉jgA 2Q'Ks7 JXwhb~v9N> PQum Ŧ.uӨ q 8q貞I•c#gdG}珺F8k~/SlLn~pݸvI?%Vx?8Á-Cr孆1;Twq7Kۿ|Soe5(cot rc)`W F|Kہq;';-83r@h&lCpȣCN+a܆ۙ[8!s}:saa>>G67/ܓe/c}ӑd, Nӈ8]8ꉇ~D?k{* OO_/U=,IZ3iE$o,;MMp'錃i?77 vl4¹)&! &nʡiGl=>=CLNiAWfu7j5?PrS&4tf6fq<w6'o y{MC(Y+߁ ~ ~6.ڧ0@olo~G6%g._}N_#&xth46U˸)`H7?2/'ցr; c#0FLescu L&G(H VTMh m<6FCH>va͆GPBM\y^UzٵGRWI// w7NtW-`K_Ɵ |?OOU'6~i1B܄IΣ#wGuia@Q8+U[  a Z3PL' `XvcI0dݖ(1s5Ws3 6n򥾘Uc*Sؔ3?R~~OL[I_X Sd6_.wxʃof|qr}>o5S8*fU"Ȇg+1oy~Iw`yp]?D6f@s+Fd́9Osi"AylƙVe=(% ybUH3`,sXʁ,:jɶ/F(!,X=sģ/SC#/}aK侊ɛ0TP*ؖMBPH9 :6Qan w ͅёAބehFX4eg9:.%6O▓;T^o@h<{!qQu3g6?!sqc?8ϐ۰-rAV`ƃ|jo }_ %|?>ۿ '`8 9b>*'iQn:<8VT?Eis\ ;Żv`pmBp{ M^ե%<5e"lC4]^h.G`MzW%X̓ʣyznx$>ď-pi^'ñup}<x<?Cs k60 (OqK Odo覗s~E-鉋B1O}b~2CYxJٴP"nGz_0F 0&>oLk#vSySIէ?/$BƲs:. nOU|}_o;gJnR6-iw41sI!M$?=S8sOlKN1[<5}wVfl'8;I !IpyVt}YQsOIkD&>ln8Qh91%=v /qAܧyS\}~HgWVK_0x/N282^.7^Fp/G9D&繮ah3j١aܬ%׮^y?V.g0xg\2%rsJ;ol W%]pi PqcRj% 㜛-,si^7c8u;]">,66YC.c}mq?ltPR\%9pU>o{t>vϿ?}/{{oF"aKܽzz/?(:҅tl 9bF>*'iQ!L;q/ҁ;E1eS7< _3w`p.'}SrH9HQqojZ@L9N9j\u.YqLuqyimkbOsnLaS~ҧ)Z#4|z? 8Om|/~_ԳW~i1<$x#k,Kom|½'$gFOv9ybַP8ĖtX ~ 1."ae:^9Nry؊6q 3qy`U]wi^Cr_И{;]*!d}|s7?~>S~I<(س70 -2(k-0tpcK2,s 0c>ySl5L XAMOXa'0=ڈ]7^S>0:t8`4G=jilB!H;!=G'_%0ᩲ͸Gm;+y>Ջ_}OYX:pt{N}haIVg `6Q_wo2ऐ#ިhNyFĖt:<8N.:M=iE]=WCYxE>~4`<ˑoI0tȳ"櫓(4,|دqFS`n3u@G67Vus)Gq$W'^Tcq9HA/jWG?̱Y ç >/?_;̕oAXKn#?t`XH1NU)5.>8#~a~bX:pXd7aGQ@`69F-kɲ~y/P&=oPq<{`{K>Q5ܘP $ eD06fomZut&n:F01%/5`q' qsjqQ4gҥ}/ns#8kos]iQ__2Cwo)C~k_3W\;/cҁ[_x逸aŝROHԊ񨜤E 8IIX:p,Ѽ:݁s9y"u~멙'嵴 4`fϓr%qSXezk̓RHb=tFjUH#I^._=f@NAqj`.ݐ3-๳[Sqg~lx >1OM/6_l#UGN`vY!l: 蓔C.spNbxpfb\d7Mԙ^K)Gt)X7#zta|[_gfZW 27#k\qj"t3[cg{cgtm?>=(et`+܁|7 =<R-ڎQU0rgͯOP'Ef?dvD#ؕz4"5L' ;{ ŚG8x}rCӃWuM}\=#FLpf^K]Zh.G`MzDNW^ }:@Ʋhx;]h:%2a.ZCXxIXDZ8ˑoj0bȣ 0Na%[[y&H]q׿.}> P7Vu#ׁyþor'N^?c<<42Ov1^|`"`Z}d뿻z|V ջ u;W_K @o Y8 ^V0-ȝ(t`8raZS9p' NZ1-6Zy{K5qf=O4 Ȩ2Bk8#M߿n)2+Wc#x}j+"07^w~7/|٫W._? 'N\KHO_<--$sX 9~#)Na8ksz]| 0rZ#eKNo"'9;Kr.lescG$̓eh&s}_4lBaM=!}O"–ؐWDZjH9$:F} \/#u ׇGBsB$<ו9 qF-; 5Cq䉺 -bP'Ϳ?#s~uu}_\=/7qEDT>wp[Θޘ>%C.&*4p :ٛ}:R1i# üwC76Ye]7Oxtx==8g\DN+u{G2zbjqya"0p|ݏ\…O~gFet`v`\;?\RIԊYYUNҢBNj3O?5Het;<8.zsqorJ]dP7z.Xƙ.7 r`<\7a"8`RڸA_{E->2ciH9)L|sEElu[%Ue5<x|J74lJү̀`70dqza AqMbx Rܝx3vg>3Po'\s qT?YX7#z0 7 r38 <Ն27cC⚄5j_Ïx~ޗ~﹫O ޣo{0ҁs+gw6{G[e,y)`Lq* Qx<_)[KNh"19IMݸl6oLÀH Oȝ=&08H7N`NL{a9q2΃Wurok5^a֬^_w Gί}wd,ۍǻul?3N_Wṫ,@~Z%p ǟk1|n{[fΎC^~l`%IÑq!AjN'>YKnB7d速x2s鍝!A%:aYxE$9}q#9f&2mE|_vxSKK , 6,}vat`nu ExZ. ZV)G8O]KSx(?!xNP?X3׾m{k-wYQ^;]Xw1fxYmKz9JCk9&Bio^qF&L&Thv&̼l7v%TWB8aM%ж#ue<ׅ@`X&-Hu &gDtC\\`3srZt{vaxi%{A d?{Z}eN+C5uE5k4ΓyzXGq6V7H9r\s㫫С<0-J[$c'6e~nJ谸TOc DZ?^uMg0nFF˿cu,4ʛX. _(ם~#~Qg?g|iUf^:pt`sI|EӜtp)9{'( 8~FN#L;dO"/'؁ 6sj;|mF džz~ŘE.dJ8f]}~Yt%G%zRF9?3@O3aҔv}!~" ?Q |?O~)I^otr>V/]J*s纣ix#9>b ^ԄaSn~uT<'eunbppndlloֱ>.ew F}x^5oo<|ZԽ/~#^{/~rGb/x:py_boz!*f`k8) E؈`GL3`]LC2c6pw _#0wy|ܪ@IDAT < 6ᷳdI&8㔰:{s|\JeD]9v`2su4d#2-m~)}\PoI. J ˡ9Q{U;^Tkw}kUڛjE+@mD xK2_Bn]f>m*P@._ht nnB^`+ce{}\u-4,1]_$b.sWxQã?GܾotzW~;_{wk>x+p[ak[# fpűHH^[毦q? ^޳_G2IhT¢W+p+?E-k'.by]?ܨHWNTqe]lNcGmZ~r=> eגt]/ŵ7Re@ -k:6y= Go}S'_;[B&ox?< V! 9׃$O Mm!KBjؠ6 n$" 8agE6LI-+ٍi']{93161&^?0@h1+Zr~žTC)*C'Eॳ?wٻg6 j+p8u՗_sx}c:|/8)c~=?X}-Ѹ/"Ol#ު9oq:+p+?|p.s2ub;~P<| T8y-Q7̆}{xkKS@_=Fݸڋn,?ٛ[:r%lG7 虴noamސ5梉񪃂@·QKo~pWBۡXز9ƅ9.e+O|[w7[o;o0WGX5K|;Lpvb1jw~K̑c vRk>* ,9 [|j|Q`zy2@Z]t d] yRh(#dH\YG161 }nahu8wH.x"h_Ma&2m?V1R7jWu,F@qL7rqחc k\ߢ }i 9~IhmO\MvOir08k 0͇~_<3F0~qݳowlsc>W@Wn?yo/ l/O͆B#N#y8f7̢%fe~ނ`| S-Mz}k..X` 3.tz|L,,GJ^:|W-uY7ju'hnc^7&Z{Ī`w:$Lx vJ57%kla}\["]Q8 4X汉1^iŹâ˃=Kx#Cu˽:\u]luo~?_O|d دkWu|+^ )F`nL3xEGm']Ǖ\}W`a絭.v\%,X'P3rcH^Tz̸xQf4 ]_ko/P줖e_k6y:8]jB3R;t , :-.bIo:?%c0'T]=@n&7 Ωy+o'h8ΑD%>?s_; mvG[OK0  1P @6xq1z&)| !"c~i+i`Zwu2:/ң}U0Ҿ\#kAU_/z^e>:y'OpO|{ne֮{Uw>vEm A^þZǬhff'Gŷ2$yٶ"7Qc˜w6{=W>V`pK.^fB5m˫[2ܫx6e,u>ܤw0 *8ހv{s S-R 5 >(Mh>.Jn{}XXү㇊Zc?W3y6>#fTKclxy:g|p'q- {$ړIxh_l{ ;WcfbڋMGbn1QW$Ms=(ach`k vdD`HR:"TJʫ15@xl~hk?`[a6:vG|+87OOOq%`l-'wjm6lj!` =6sblmM \p͖QY9 ko- 2yiո;`iu׾sơa1Ry;%Re$bg&rǁF}Ϳ~*/>w?̝oٻwv~6W.Q߲ld"kBq,S>TGfK0c\|:g:.q;/e}k..^" si1ꪰqǛ[Vl5*F}ei bSbahk=6`z}5_5Z}gCLy,=ΩyG/4x 58qZؤPy !6:98D0W,<ۆ{cRAɣO{'m{PŘv1ǎc-Vtzj@{C qLR,<i7/̇뉦eZy * 3P^rپEV4,s=8c.s'c"1b#?G|~;_3w}ӷ|v!VbW[SJ"k`* S>z~T-߯uf4xU`/MrooK,&삇{} ̺>q}jEyAuyqIcAy'1Wa0o> G6(t\B_ٗ6/lt'F *:)Q0w/&JN=RxPըxa@'+')uPP1y|園vA]{^c! [l3$Ko;|m~FxW?m1WŬa@ XPx!n Edy`@X \`g,>e]2&Cv++5K7m덪jp|M7mlR|B3ڗlY 2cR ձ}<حw׿I/|{ͧG?ѷj~7с ђHv c=uHѢk5yE+}5'%t)5'#V\DV51~ 0G k霱V|{ v`?)KJKT<Fy:u{_w|w>?w_N|\BǃX3UO K kD E#ydk0rqj"O,?h`_w )mԘ0Øo`i/{5W`p vxQׇz\(ùX_=}Um6j>c040Qվym kp 7RRd*o.e6I<#OhTCb҅8xe P7`R˜5=<iJ`3cWC:q8o>ӟf浇;5{6U'_;g<];,!eM,G݈< Ks"5u՟PUVοkL;P.xL˼f$_3ǽI q/sl_E՗ 1bK/Eh5ު77>zƾ ޱ.x{p}\{-y":jX$|H"kz~vo} p c oO+^XK5+p+?E-+-\r1(1x5O>2HrT.MP+&˴ `i~ї> 4 ]_ko/P,id킧NJeGg>poh64B~y($r,4r @ (v#kH -i8Y!{LN0~? @Ǩ.cO[Эoa~C{n}`4N߈ka ԖA[c߱?n7؃;ApUXq;5FU`5nX,f=xfT_d}KViX`0 zJNk0c?G~¾xn ×ξg~S_;X+߱V {mpGfD쐛tWՈ5vYmq[7[yMNj"Qc˜wä9C}+p+?5mK?t.uÇԙ1"s#%>2iCIakր>0Am88 _c}uh4HsG,tcl/-<dSXxpkK =btBL, =95LᜒdMN50"p!TFv od)M̏`>UwoyRys׃x(qxm!/Xb8W!C}{ɾ3WR>r9tpМ8^c|Q3.pi_[yRy,2,/ȫWF.|*D_I_^/̂7Ιئ|{w_~~{g_s#u#)Pf8X{rb&l."m~<J3 [3`oym+t=z؇ :yPw+p+?|z_*Py::1&FHAƦY\Z։<3>clb8J[}2Z9 Ѿ:Ҥqsci&Oh nѭ)A5iJfD."%g.GΚH*7x`. f8}f38, `?-"0<wN9|1{||wJ0ۃ/ f*XSoсs,WKruY ]9bט&o(%5˂/mAš916 ^rپZ-r"hF^z~j/qUŦ)fƅYLlL8 z׿|웟|?gҋo #ͫwSvm Vߖ,W;XEbʇ*F6?#Ju@m yp^-CPXOv+Io_+p_+?eEYxؼv p:3WyAu}jL*PQ}9bHuj*;Ez+ϒYH.x8褄MaQv&JN=Af=d8# GΜ.o`&66k[[w?&C2#_o KH0_ "9c6TV[α`'ky> hD5P4dǢ: ̎'))p]/zuV: r}y[>z띿e7{?.gw}Дo[؁|cX\6|kM<hEu%}S@?oȋ7“=ji-@J1) 6 nzEw?^pQӇD4źRp7_qG֗ֆC`QC-6MTC6a^#q8OCx &};͇Y3&r8kw\Է{qkke:k{Z)*33CO 6?DgݙCSh$OM[#0n8ǀ 6%xmf"4ñoRiG>ɿ~Ɍ~d> Ɇk c.S C026__cuNf:8g DQCuX<;%RNyqb#;95EZD3x9ϠKYgl]5tgo<ÀoW˯8?r뀿w?lkj]U%scQ6Oi \K1zI>ܚ(Pq#fAA9(/sl_E՗ 1bKGlm%CuWւ%0wٱY6HHˇdI4P;#E隼?5[/O;Kܓ9y8g Ԙ+D&Ls†1&!z F +X}!\h@tAlqŋ;cll!}z'7~aTqδԠ641REw{w ox #v0uy9'PĔGfK0c\|/KXg:.E) (^@ k)H8 |-}^^.pu+.n9_f1TG8 1ӹHT_,vmXkg+͟ڌ}u R b2]Hlsb{Ώdթ-Ƨ^;Om9o)^xpWe$ K(7IfN4Ҟkad\` &u&lcY!ఽP)g# u Ʊn ty/+/x_7?ܽ{z2o)պexP/ײrAr_yQ|Qsg_8ǟgwЪ=)wgjLCHI7ah,9 [f{=xMu ] /$rftx @`đ4̲̌}aC )$Fz*x<Ư>ܲ q@ԡmV=͝WFcs O;v(3z8 I lt ؗkz#0zGpzvW_:q/¯t轳0cT>6c<,J_M~Uƍ~_ǤŮq\1{kfWfV`p3븻 +kk]!7]H^}5 ~ۃ}k..^:~!b_Upń9Ma!9(<@U^HN>+,kk> ,^T<56rNǎql| b}! cmFLѺZUE_^÷4@I W[/]/oJ˲MG8VjP s O JV+9HcH-o =YzK-<"U=CÅ9-} 3A|8D7+%džQ6y[<<ܝFDrxKUXjo(&2_S_#O猣/P^r`<,as5 9t۪7r6mX/8%|aO͂< `qxXs3FޏH]*a;E݃}n|7!V@/gn= >ŝ>f@%K\+F`>e?Dw1@L=0˴_1NM&SC045/5}@LR?K-־XBkd m<NTqz(]Y>XOlgf^A!G?N_x e=(y AՉm2H vRGPα`'iprK@ A-xz*Mٱk@i96 KM> h鋵Fͷz8iLkd^3F&rX^K՟\byPcKϽֱʚ8W',܄vDLLiv(K̒˼y$njb>rdLXOyz98aS1?Ns>3mߎy6lXѾbbUwϺ⨗8#qZ'Km5XWggi/&%#j9$6/^RCroU @JVu\ږ}4SH'cuh\/#}uYl+p+?υeW,\5'0,'\= P](̧}t^iwb5[l_>YֱMy7[iK&9ckuH;8>clb(wq{756H^0l?Q;N^Β//rDrP1o8)Yby4a]=‘v c)v, W6ke<ü_,ӆ7>\0 ӸvMaQXƺ/7q:~Q_YRݲ eV"sƩD#f+ьayD ]F͉-4nO;#m#5 sF}|m&Rr}|Kjl""1H!"Ol#<p |Z¨13xio"tdǢFa9P':FRR:a켁у$m' K65bd_CXX]-pWAXkՆWW`pk+/X:WPR18O0oŒ\15ib3L;^$5/MJ˪ſ|_H]P1 j#FaS]/5C6"^K%;b^hc#?DI^ I!@YgBw)cCb3P`GA<ڇpy)lib?-"Rm>O!@;=kΓedbp^t$4(_c8E3/yf4Mra@#z]E@IDATF}:oyuV_όސ<dԱ+Ea^K1i(iNk0&ހT%%_z,f,8f},Yr_A^RXѧjyƷx1aQwʃ|fW;l)H8 |-<ݶc  }̚O j +$+^ ؛> 8O%}i/Sa}qC$]m9[d$8$zpWީ3Mɪ#|LPӓXLcu(y&={ jnAk 6U l\V4clA`;1C'u!ϑM]l7TX'F6u (nb]L;ߤ05,ۮ]Hqd^U ֑IF͇ 6T8O 0 f^l<[ʾ,Za9ʌ4t^T# z%E>vuCdNG:3f~t&  {7苵ca}G3x"Wi+p\{ w u Ì<yɾvX~|YA_+!/ w}f`^HUM^ ^z\/tcԱHLH –yK.4`=qx`7Ic9' ?طc SYTd+p+?r fQ'\c&8o6Œ\15iJaWrWlŋt [ \u+H|~_k +0\T7kq.C\zɫ b&fOV2~}uNuÀXX+ p!mDU2ѱs*ITbkbx/ \5%5a̾-ih_.eT OD69հ`Ej·.9AЯn`(dX/-J 8 b$=Ub뜅xV$ϡ<闍JqcN6,paVb\4l܋zI1 Ӊi mͻҵ틗) NE}/mAA ҉G?V_DծR#Z-O<\q8sn=oC: 3@3M@6/٣A~}S?T¾WzQr93&K6775 ުZEҍǢ1ם֩ Hi[3`mk30c@zyOz0„(1<8w>*Ȅ`b1C%a\G{uW`poݛ]pK+N1 ƀ9OM&FwoZŒ\15iRb!+15BЭԸl_(#ӡb٬_jƾkfk(^IKۉ#LZ ,̨WuVdpMć(ἆ d58vx(RKh>L# z@#8[[[ǼMu͑Q8OMt y.;X^K^!&5hXÀFl_5鼍rf$[}M<3r{;6 #ƨ#4bP'4xVH.1y"Ol3XA nGNǖZ_Tv㺁]AC݂b>B%t_Y,n2gr7EpqK-y:L4Q00 y贯W?݈#w(SA61T1a9}Ų'ؗ[&o5֪7Yum 9161'jUJ;d_8Ew,v=cDj57qOaM4rP v)Y (Wwl{@$0l7z:>{,k~c,/VBXu3 eXXsWXW5TT'a& cWX3)LλJJTm/5bD ]/Հ2S /xsh0 ]ʾ/L H(L>ڡ,)%y}Ba>K-Vh)ۄŋsΧ;i Jc=X¶8O^k ĴHE&~_k ˷W /gM~[m<3tl6YBXh\y P,edbPj $t^ztQ}m? @ԍ ʾ#w?}5鼭cMyms]V7/BK89}ƴĝ S{{ɤGrJH5+ oƚL΢3GF H"Z<ס'\c&Y0}h.¤ѺZ >Z9ju&G L6M8=4scE!/&9z(!.v#")۩dp?819.i.\ּdGuyMW]17 ZȫoZÓGN:vlM: Z;B"FbfM pit4,MN, 0&2Wj<(:򨳥qf8ax!MXLyh/Ѐs6 ?L۷ct46zL9& ;b+p+?U=ko( ͬ'p).-̀²,1 ƀ9OM&Fwlv cAѬFiRby")SLcPk^}3DR} iLT_A|'EӎAGD_A5`J˸X6Q>zxO8=-> ֆGq c᫃|b8{ƦvOpTXXO^9J&^ Gd\q8KCLbc&V/o̱ViРE6񒏽h*rl1PΥ.m> hD]UبOXF3#c1a:A#M NXz)O5,:oEC1K.6e{sބmitaT5c=XgpRZFZ&Gv實I}M&Ws{^!oPI" \HbKfMy9L C=q:km}.q%}j5&4&,r/Y & V}ɠ[}CW6I}MFVD5s9Re5SJRc11A- }x5DzM&‡x l1-,:+g8L!='p0&oD!Ŭڃ*ӐS|0`"]oppl=XbYn^H0Ma%%Ѐ:2FL@6u (nbܦ0|dF"F_Nq9Q5Phxl| bK5#`<~=/tU+2N]ڜR Q,kyXPȣӭݭ<.sD c| k SMW ms}]=fչ+iR |ᢦM1J $<~(dŚӕ4`#/a*4Gg\xa*tO7xO.8 i_hyuIIg]@`>Q4'F}k;=0vׂlWc,Nw4P=Q9khx+1bikR\W* Tըwȱ s6`0N6KhkXZrّQ5y3.=LFضJR,5[=1w:ѠbC^+)c_c >Ur7h¦i(ŜXDZl}Av[(l!$c!,eޒ<5y.م+k̹FB5ҽ4ﰁ_X+Hhs=: VbAк"t]^& nh!wy Hp^W UF䥉ebJ`,8(iW%O=[ Rsm [(,:g]+P'^+ތ sLVzX_ _2l-d.[JScrmVn.fmZe <88zX#?6 :xU^G`u.[1/}q|l<3cc%`c} Mj[WG$_aCu=k.yY \,嫖޻~4tjR_~ >Wc>Bˢ&@$!!}+pS+?}غfG,A;PРhyvdHi,Dx9SAG^֫xQ&ИzIXw:PLNgBP+FruxXj-|Oae 1<;%a5 +n(SO1HX6^G'ٛy,9e;,Gq ) 8{T xGght=8g Cmcb'o s&XbY‡ Q'Md𷸕:PxqPj h})qΪYjP}yŋ8 a@#Melܧvq,#ik♑۱Y0F&r+=XnauOl`骕ܛfՄE9C-JR%챞@OQ38^˨^kĴκ4 ,J> oWG\ :1Ȝ' mf%8V2Fxt6QZw<Ӿf^'Z=Y:ԇW Mct{=1kdUĂ?t8 0&$:iUJ=X -OeIO'͆ vLS5*P?(Гh2"gၔ ,SmE)(Y>=}\ř4Ȳ<{6y:XIw<-48tpu5M] ^; PLcj.u(x<4zo0߉A Λ`xl| bK5#`<.R f~Z r9S uų4y;Üo`a-Uz'SMW ms}ϧRGU ]Wr#iZ>W~V`p?k.\^B!08+XF:a5j4&=t%/tĴ3UX´9n@12ϽOuW_RciΣ.WF}ii&uSbv'c_jů̏:T?Ю 931-c[sݝ#b ^-f.]1P 4Ot좆@[\c:)yͻ4ѿx!=TU=1s$+n1~Bib|)E}YJYcrm00n34f7_PqkP t\:Y{Cl.]ؗ>skrW>D_#O猃/KhңYfDm5cɋja^d1n]/gl(hGr籶(ϖ*v;lVа^w9,} mivLtbݽ/t5_{Fq7Y@^pAͧcRcuӀ`yA'cIV_όhoTM0-ܖ٭ KgffK'SEI#w/HY&{'S nN #CKAB{/Ukȋ+pc+?܍t_fA-8×FZ b3eh.6R 0-m>&yC}ͼvqh.f1zzpj7\ՠ=/;+:kmU_$M|O猣clb8OϦv`n/YPyjc%P̋NS|cWk]m64دz/c!u|gR4`PקJ؎'|Q+hX^'򼔋! vɃV!@Ǿ7\ݪ@<.dz} ,v<8>ȓpm./̷ Q5t%/tIJIΨSU58&Ju>,qF<`\EkW_X}7b٭1kfCk3 JgبOmf"YݜNh4ǿ&Pn͉"/9&qzE@CE"jTP8Xo:/<8>!7<ө`X<r^e_+uU=0m fU<+)9_A1\'00ƆutPj르GR4Qϲ5Ͼdo%aPqsR'u5&4Зj@[>t>;q$ Uľ,fGr.a/@Outٌ5\k{{^s9Ƙsoޗh1sWe 5.Ûohwt3t<^ʈ\P04] \Uۨkh`ZR5hm%u`Ԑ= -t`|"tx۪6nq0PHFtx]$)X Mz0 RVrbMdz[jj0qEEp֮^H1b>.6xx$P^;pfkR$7q`Z9RDecfb˘qƦgB%E\+eZ7<ς64ᶡ=8`3V8ץ"87M$cԥ@5uI1w)Y¼ރ?5?\){EhIM-dlg}觱vWu :`d^}*;_l?o@=@'4=Lh1|N CjS@]cktNC8*/ll1iR\זCs=n[m74XWHuiͭjL9Ͷn܀1z8 Vz3/~?ubL]"$d@0АAbCh05#r}/ Ziag669W>sSC4V2W5mupĹ~8p>'o>D*h%/ s\y#5q:׺q.Jp l[%__Cur.-\r׺ a}F0^dOby"gřb[.B{<][=#u_?oy,9z=݁%yk<ѰaP=ʆ5%PidQYEp;WRtlT@8(96>/N9ʏy6S5Ync1ai7pИڍWuiB.5Em1K `0!JGw b,7g_0 &6ELr#ckWpܺX3܇¢f5__^ \bTҚܿ,=ţǮKBIg Xϱ ^xxɠ3?8woao~a8 ֹ휸3dMvQΆ\H92vLYSۍ~xDN xsY7Fkyn]P }^ gP/MI8bY5FԔӹK9*.3aGeܖphlKsxGEql0bؖǍ&G{fI5pa^0QKJ{=' I?gT4usoe_M'6hn#IBK⚀M<ɓܙg^+ϙ68>`lU m^W "coG Yz\Rųn/gKά+(`TF 8c0C]7A~cPRޘٖC9DF9$Fh5-]P$e5LĄ9Ïf^ŮlZ{w:pxxx N~{]w<& ld]azu՟"'yXQ]q4q[DuԵ>u׵nl!j3Rfr>գ\W\ Q9.%C\.b2g=kiKs( tՐNơHUI98.@slaHQ%܇b͝(y6ŏڃ{~݁:LP0 T4KΣrh`k5gsE u2>?uUʁ'`moa\ݮ!9fTN W]@zm<%ǹ. 0Ǚc)c6RN]\K1yI42J1)|H֪ŇnĠQ48)Ѥ7#&#~|kO旚n t~M%|?<0Oxb| qnbaiW]g%(Y+LJ͑` ډ9CƦ=va7A;m!chsgvY@cj7 :BYWpcSiVe+ z6>+SVa刅r*ܫ1U;;Ml *"`:iCj[ |j,K/Obݦg|I Hn}4Ld !/‘/o{.@//>?jKiIɳ;mDuc(L1(v(yTWѨˈjPqp$gtUWH9x0K!c6"Vjb+M-[j|]9+\BCrh /@1[k᪫E(h`'Lp&b'Q79/xuǺhr5Wb`FB&+xm bW~r6Q=;_`3oՁ7VFC~o}ܖ; O_b: ߥV0 Ǖ>pƁ1!Lf'j Dyj#JaMff8&}nJگͅ)pYë. 6RPےCZ, 0Yx\e2cVHyԊ<͏@ϵVrƖ x?p]]f u !\qV Vq!fxȘKc?Wk潿;_DocsD87tz漉8 8ν-*8sЮP394-.zs28>`c ['N3r0~(Om'Sih|4r Ϻ[@@^%R!azgkc 4z.aS_q9/n~Ό{x /5}J{ wm|n@L {H|ago?8O,|,p)!ˀ R}kS]G5s<+hg _cWDU5]'uiYPdZؘtGW8ylσ rHMuǺyq?r @NYG8zX& ?9?H'ܳفk6=@ݰ ]n X/J)6q:wxp)H9kspL8R3kĩFqΡ<SMʹcrMlkYPɎ]) g~Ϋ.x"+Ou! e$^/ZX"˲$?ٹ ־Y_%_իѷR?vP 0zr )GlI=fdB G-E9@q+EZtȓӹl8`P#sOtkzn& UO4sU !ň9 O;$<Y^֤Gx@IDAT0%#5Bi=B=`ź Z_ȵ_XǂK?v<`G`rB&OeĞ .9ȬzMrW3ØY ,+7bv.c<̅k:q:~)zkMTw x1zh:./2(6ܑI,pulJ7Lit RC8bH xk}X䰍1__X P}Y\T e\ż^W!cKh^W~,bX[B1`Y\}s[5xnF>3֕c1QD%5nMBÌLm}^)l1S3@ckaL͡|ԥ4\VV2VےCZLjxfuu̯YŸ&!p7WRuǺy˿я0+aW#&c*ZÚo t~M%s!Ns[&ەcG/C/7,i̱*u_aѮ T-Y+""r?d%5+aG]sYKi熀-fn1a쐃Oۧ=Se*4v(<iTT Ǣhld XŒٲBqفTY͸:{7e_GߦmIr;nc#pJ9RS8ŭ}h>?SəؖEeZ0C֒Hsڎͩ#Z9f{&I ްcէꪕ_'uijڧjLw+#>Jݿ,=ţJ ECu!aÁՃ2(7y}ʜ>x/^T|6Ӌ"Θ8iOF4*M`朂A0m-Hp~ jd!ڜ_E֥DPn4Qi1Z h[[ݩF`Li1pug?uԇ[t}^uAa.SԀu]!@AV{65l:蹖{f^8ǜOfԸXm|a~a~={ܓwI觫6'c1zpM alϹ堽dYcqQ[kјgq70͑WNR,1 R~Wc-u@aV PڌǂΉ ITLϬmG0GS=b8bVp7pl:0>WL2 ?riEkU<9BGbnkv~𚍻i;О VCN6pr<*e֫'Nngɱ*`Gu <46e]uSFf׼9Eqjsa-.~rTTNǰ9 y L߂58g)7a@KpmTOG.=` 1|7H<i%h0^ k=/޴m7G?ECE{4􌉯/ F1~B]Ȅ{]ZWE-Qsu 4q:}Xӏ m^WCPT "co4ŭ=^RS̺ΠN-q+ `~]x6qhM[4R yE(sfIMD.c]ov[x u ;#t @Ob^s)xha8o'g'} {{w;pxzށ:dɛ.DYסqܚy1U,mK?]3t l/ւ(N5׺XGJ蜜C1.rBPƫ5s8u~AGux8Kci.=-osar`D@Flc@ a!uiJ,8b%  <1(Hj wxכ!2o@kMgqƪLభrdsJ\4ŭ>DLf'j ϯ<y&l3S`moa%$kjsd *6\VV4кHh$4x*YK^^ kl3YH\?zZ/˿wwލ/ލޚ~ILϗ>OSrpVe MC]7RlU+{ʣ]u֟lO@j9O]n5q:}g!jkjA{l!c~T.Sy,1üV%pO{;|c8KEͭ (n8s}؝1z djb/e_h$yVowOmnyϏTn'mlr.@0K|Hh|<帇u5O<7uk&m-G&d pGcKIE͕7k4v Ks,z@]m92&5T=뮢ɵ.I\_`3_o㌔5]ٴژ+] }Bnkv~𚍻i;0Y85LH7MfsފZpPdž⼮8\`:/]4h O h=cE#c6Waqr);Xk0f9&c6Wõ,(dǮK`6w d{jC១m|J;PЏhøE()D`M sk6燺jVE]TOB *3Ԟs'H'@~ ȡ<u\=yXV8Xľ)qToX/W>RűI[@k6b`˘+걽0sAR.7.9&:L1S<\-%/ɺXn v~ͼV>?wÿ(1 g@&5S60(=G\ JCJZf]9+Tq@ _9-iK@ѧ9p!kzʧ{I~Q6 ~B8qEZ=9⃪4jF`lpgHL s B i![u5\i1Xh97G}=޿cƃjVSy^_O} FtCs 9py&p[8lei}FWMs0NRs 4 أoUW($IsIrZ]yZ؈q^>-EB~_j,jC0"8GUOΦ5a`Y#~Q۾;R_T'o; 86g< ﶈ<}sb MҽוP`sIŔ5*(PU_8mp*gW%܊}=ZmyzjXFѺvԝj L,۰&p&:v퀭ǟnqJ];r@J樓1qxNA1c|oo1|/p߽yv_#~׀W,mӆqPl35FJ|I.yYHXp൭fӉs'HxssۦB3O<䥐ϱx 51& qmazĘvzg}ظ?p|/گ.b kǦƢ%,b_&bzCh͟k+8g. >yK=qw :pxNb:g O~uQ`\\8Y6Φ˴Ub@ ) PpL`XU -\z`.dX9ע U]Gf\$c;ֆM{/e(OF\8&7s@'Mm~|̽wߑQ~e. 頭1cOQ=n U! S_.j\u7W fsT"]G;I2;ZL P JHƁxZM~Qs<$Б8[DƖ  Xyb5ǠQsT8@GLx"o3 ZZYG%3h{,< ݁7[x :g]LdL OCs⟜{eZ&umZ`S.z~ikq4AظH: 1cWɬ2XّEqسo"[X+ y=`9.<\p@m8gc8 1Ez,_f5V]s-sQ0V:{Pr'Xa9M]ڷ%]_ poN}.1w\۰Tp sx<@GLfp0H?0? /!vOy""2ymx0lp>lbSuLNg)]c. uEI{Y2DQ]+hLM9wp9'Nn8p 6`PRVXs;C+p 84 ҩ=\~ Aǻ#&LD̥碍 ͟^3YGΣ?u#+݁Ex:3Nr{t1mqg?кF׫+ ]\T$uˠIܶ)̡uEA|x&6s˺ꁹ 3?@۹Em״M y G\b&̫f&Ps~iZ9{Lǂ}gZWtgVz|Uk|QYl:M~qx2Ow~rOGS,H1zt]/r`?1[b&j3a-1yr Ć1c0c]%]uFrf:w[$ p z09|ht!Pz"ŵ_caxﱮNN{xQwL5QCm 9𧽞/4O$CXq.c#pͺ}£6蜛A{p!rpH6^sZMD#7V9d3S qyr?uq]&6ٴ%f:6NMEt<*P~=WĠEY_uGlH#ҍ#O\[,4,S+}*<&c1bmx/^k7ԟE&i:'Hn('2Tn'g%=wu)ن_/.8]ҘP p}K/UDdr uItn Ϙ3sڇ5xP~n؁LM3hOk v"b-rcc\c 92\;5F?_с^wϿXs Dz.CHm/r_͹$GJ<Ɓ-Wcqy-G"Ě^!Ն,IX~ 4!/"5g&Ƒd"fM`.ml\= ,f< 9:ǜDq/ԁ 5sNle;B&za8y`I8ES:3J`X͡jCʂ|'I2vR=m18sh]R7n8sz`$Ǖ紟kZ#NI]گWkgHQ icS?]5!BOԥP.E/y$/ҎS3I)c497`X[1M؈[?{AI M/"XwQG`:kux4qdЙkcCo)5!~Eև>5m1u1Jؽ;_Do8e䦧.yQT:E&XQ9P 66cc?C5!Ub֩GuiZ/piJӺ6g9c𑷔p\eGuqf!9qt WV%%53d' LdF%lJ``Nq_8/4K^O궝p:6MW|/?{|;ϾܜqCQˈ$瘄wkUiTĪ3κ&G!]xn]j8\VWNN8ǚ#ll-q\Zb#.f'VѶM! YSuVcW|l- &r,,n5C/Ն ]wGwJO=fclq x@w^ l-:ߖγv2 Ejwx,M^j̺<.4XWYsuMݖ4ԏ1c}K/UD9O]#o; 4q:}֟/M*l)P) ӆqPl.2ks. A[C] h_֥e;O~Qs2ו/:l:qstPҏƁ-Wcqy-G"ĚBӖ}Ŗn.u^R )bغ"ZxX \x1yG.^8cJx/^L|N^1؉Y7jk\DQ 8M9K*5*4XSWy"N8Ԅ1G5 b4$e#0w ˃ Ǐۃ6 xϸ֥$5.MõcΝ'9s8}@k\_fNAqЀ_xA1\Gt%yo0ym~`;@;Tg@:p8nA8s WLU&8uMCܺB#pK>U3{\_0׎sX`mN_!i\>ڴ#4һdӶ8ɺ:.3J#XYOJ]CV-Y2eJiP }Yܕgԥf~ (B̺|>?uĥrG+?#bv d w rĠQs%$"+%t$2Wh y?9?c. >p,KMsp{d/[ GsL;T?b776~~Nu.T+`DtxPb("p0683Vк$f>&Qm4l$pKB-_Ҙǚٖ,1]Qcj<9UOBj&1)Vup<#ՈzKyՑw;Pv;SM*DO=1x}er&j)z3a0lp>lbSuLND1]\1[QI`[I]/_Lz]/Y J֥6¼^Z`6}8Z00jc0nvnU,֫qE3kQ;dnXcb 6|+;B_P#o7뀽)|>vs.P/~~>Ħ+u"g̎fcb QN7 s'JİC5Նsj. u\qDztUR 6UrdÙYv')a/6s-XncS0sݢXVҖ1!lP'a-t}+?A;@þv4hiLmP9'v?No oe&?.f g\"]2%SrWrDžYx֥XV=y Q Q}ֺXGJputA΄#!Q\4\I{. ι$G]'n ~(08E<0؏[֥"UDdr$5[Q69$ciWp9dpԹC \ޡCL:_8žseW/_$wjZƣ嬺lh=lja|Ćk^3DK`-p}urC UJ2./yO/|[ll<1[7G̶?Wk>sgi'sM?[cx|݁7wg/_o4{!SZ%S$!||܈fŹ7|<7/ 0W iz,jÅ"qm*9GB{1婍 RnÙ5!9v1)ʁ!JZ\'Qjå9cͅ>A cF`R"NxaP|.1o}̯Ο%N8`˱ܾFYWYZxn]QL#ӑ1,~ պm }1-@?#D]"4.R.] {]h)v]4T>q+|:5y6lH*dszL39zBK=bOr>,ODФc:2<wx{sw:pxF2 dC\3>ՁMD\'fs#RS3`,>cf8|R$ >Zs^9׶B:PYO҄ m|ʭ9>'F@X!X5Mܜ;E3r 2tZ0raoVW~?_݁;0uI衭r g} ֆL|ȡF k]~j_Q Rs&Nng]nc#pY4(ĸ A ڃǘ P^xd+c免\;n{Lr8xiӺTA]9pNEL8b6^DL!gX8?֬ 4\#3gx J/ҁtهpjMGRo~CK._lNq>? ۶a8v jWza)dM2_/Ao´ӹ% ʼn;VNلǺ2Pp$5g9gu-|4;5}8xԹTJjV7~~=n;Pn ps[왏օBePVb< 9SOsjSe]SSng]K ΞR~{k % \弇uVֵՊBǖ, 83Z{3>"Oۑ,%~J4h!cKTsq?֧B.6gl ;`/Uþ<|n">ldq^g>w`!'sxWVynKonBEo +cJkNY h\r+_[WӨ~AS;{XZvtkf*4EhaؠI$$u.@o@mPycq|}rova<KNs.SyL@f}UNJcJw_>ZOWEZԎm./ r!|8uw~1w#z{^ZJ+(Wᱏ|<1lmlt ?^Wc=pY͙9X^/3ϫr^E/.%|fٸ}هV/^ ]vGy`+Nu~t v u\%&#g] 64(OmP_rEr5^- 1b>^)㑮M+wŕ#&ےF|a%&֭"ilcXe 0ݦ~~ӻ;x~ȅ,wІa>.7 @\cjܬ+ c%U[J.sA`.U4[ۋTW98_/:g;pyp|vh'\˘I>/N97:* c̑1L?=al@cQUOΦ)֚3g|яrt~s318iė7KG%KIaTlhH :9tp6G}(;?瘩Xx[cWl?AN}3M]q&eƚKy}wu;pxݼ{!6/"#板vxP%vuf]E|S=Bs۸ͫNkK9onB Fկ@Lvtv&p|n] &6r O6) bY3E3)d4q 9]`2ۖ4ԹO7G7݁@cġ-X"mya,Y:w;9qc-ycr86t")pgr}nԀaa{C]IqkcC] q$GR㤱/_Dz]6Nܜ;Wr8GEWACVW3s~r~zǘ^i;m:13b#| vvx{sw ;pxW/OBGeﳋʀ{ivԯ7ń1y=W"G…hAXy ݺ2a1X U]n3OCm_cmqMsTb;\%`,>cf8|R$ B|M%7 UM=BF\qt8r9ę.%' E'.kZ"N1\`Ri&: b Y?wE^X10y T<%|}ˈ<ӹۙmlrNRH_Յ4vW* |77^yn&[Le7+juMݖcԏx0.*"Ĝg.x]'ęP+e„3¹&׀݁z|{c?Pp]G/G;lW-6 ^py^L ęmћc4eL,CVcvg@;:w;WM+`Mb13hӁ XmA MdJZ3Z:g 0ﳟd-qw=9@}5 ~qV)sjr06;9|<]X`es>0\&Nݜ<-&s& ·Alu'sڃt_\~Q#nj?b~@ mWS8 }'1cK|w| ~ ;<{_sq?hv~w 329_n<6/>.JQ \Np\l 3԰;ZOu/ ZT_mpݍ#XXnmCr@ܼ8 c>-|;@{G.Kz gSI;%yr1[|i⪯v㱮$y:ܺ|T]efc0V~ˉ^toAj?UW=yCŘ؁4چ$#m{'/©¤qƜr>3u7jx@8{lqtPs.ԟe <8ν\+q}ˢm#؅Kb04 =-g~vfN{ww=W=y\3& dk928~"8E{n]Ԁa[exBWcu\m^!h<+@srXX2^l\W}Oq${9gcf}9-|cf'x1j:uuq8#PgB#sԇ!冃~c UUCsy+McO}*uL8689p^PX 1>78{Lr$e9N+)ZaQ7]ٴ6?Xs{@ǘ9ƻK@9 ;<{_sq?tu!=`/[???x!f0% κp\mWE/:/V!f,)8DDyj#zU׆3,k,CrDp6ؕs)`&8Mg͆3>s_:P53fsyqʁG; LM2f9hð=(5ss{n *+ MDpusLSs֡` 1|WzL'*Db՟d.E/[Ri8X(2Xr%?̸#p3?F |DF,[LShz\#5> ~_s݁ey耽5Oχ${k6uSDׅ&sQL)rM;L@5fڪ hds+VϹו"fSQ(>1(BtVNįޟwޗcqXAc5xzqv_jsg֥uyK堈uXW)BynKXsl8&uEyBx s.8qC*>WSu]4C65XcS8?GO9udΣ{ovU_!ED)Ņb ?چ$PQMB"iZѢ "6JiiBT: >=c9s=y>{|>s3לc9xg=tW<lg,2KqXi+sj)q4Eۭu)7K < ?}7>g.7ƛ4>%[oVy;^@>3;xbA3f@'@s7` Xq:7{!֗kBS'$CF>1BkG+H+/\2yiv=9[籘s+o`a4/% x3欵5OY^k3@9,bsaY=]A#cz7 8FKqs652vV9Pmi)9лO9lL6SyaS[y9Gb[M_ WorLXXe\?$n0l1p؋͢ǂ)?9s58Ŀ̩ ;cs!#sx~Eb >P5wZ{ +{pV bhpC9?pͯ|OÉ7~19Wgxjd7x #|a[V̊Q1`{pb8?7^;lxҨ:-_yG0Wa'ol_2!a"c|ӋQ  'sxK E;!T[95[ڗ^ppg77A׶Cw ^sMxP*VHpC 7`3(ozi190A-CLxi?o9&^:aZ.<1'Azas`__ϭ/}f-^:/9b<Ŝc.^y37y^C"+&1_{6i^p9ѵ1__1p2OLK gΓ.9r)Xm16~:-669y7F/xclp9h9c'N<0I_9)LLSl2 /y58xF1f8cV[y'ŋPYrr2W?q9ґ9ӯa[ljbVϼ{~6h1/Xۭ5*7QN wW7-P?[zmn?);vc/c.6-:/IA<{l{= baz2sN/H7Ŭ[ Яz}6 ؗ;.—1>2 <8?>FF ֜9c6 -U/!sr.Әڄ|~JM>e7kSdSIzabN(p H ̇.Drx8YH8J68I 9IX`7.xWsɁ6X''%'cMr)8sX_X[KnZ5 eӹ^l| p/|9@1898U.ڑC{e{h&8s?ѓ7ՏkrJldL1˳IFm2Ҍ#s/!l\vVfpSැԟۯx/x7x\/JLp%f@da^Ȼ#Lۄ#A),mkV+ps/#[o _^kgU0^G%r7{?dI0GM!x#)%߂/N6mf lyєS U \}T@+p eէuSq?,R*঍` 6yOF8 OJ\ֲ\驆'9߃7a Y+p3LnZekA{l8i):iCyj3lM_Gl)tCjS±L}À.+l*+O×lρ]J^!R_D|$@8k~IsE'tC!8~ Wc#x3'Θ;%Y;9ZZ{spc#,K#uIچ8GjDwx=o̿^cÁk>돵-x%EKnf5 ZVP7/__G( ;;69ȋM˸nnb_<寍YV&+_+e,ك_y%GL9D<ȗѲ`JQipunK(n2݊a2vp|mQ磒%3=5_ s<ͥ%=Z']|=|@MlAy1o;"8kDjb4J,}YU7CsX8 `&71% @nm[P#s*W#q|TP6_m﷯,xH2׾ `8^oh |ϩQ%1Xcsm9_8cy`lv>xzl8pZ[‘iKț~N3S+p} Ӳ3 T7ދexzͻno_\!b?1޲{a;k;=ZV k"@2 .bXroVxqfrdXM_չr,J W }}V% e:{Aɾ<0'|MKIjM#czo<?;38O ܺ|[ލ ٷ>g @N׌]4[ m3i{R'' hpŤ4VX&6" gڴ2D!+mVUi#k_9)LLSl2 /y58|iٙZV0?ya*@+pA^38/Fa|-ƃy3 3GH!'8Xki]&kH$whı+n R*vk;bxY ]e],#I02~&=2g0 \}5팭@+ @+p ن',,,n))^3L ̚aK_Zl0:7[]f&ZX8F#^ɘunvp0qӐ;jLҎ>N/ Wde>p%3:cGkd;-X&W4̿q$|q?zZ?&+-緇*7W @+ @\8lunvy8"|N@8²a.z y3g374N1Cyj'o'?#+V10\m :CPS2 G_G.8ԗDC #d<|cG8m,=Ba}6 @P[a3g,t\o(W#Q)l9$54do_V9G7a{ &s|W| ~/چn[zxG}lXZ8mrX[=if5 ZVhZV ֛b g5•d .#۾ݿ`}ZfO{b10#480zݡe_ m 5mo_UH9*8 e#2 |'M!+jS±LVs ,8E_$47̖2S'{HSSޟGǒ_1b[ųF|Զ~]x`Uϧ.4We lad,#yKB7shˑQo<J+ @+ l+x^ ['fUH<]7^q<ḞG,6?+6 C\׽dC$?bmM_Tvx Y8 ?1]q:^9̧R&6RrHXkh~ɾz)٢B18OhቿĖ|r|r<g$zl82_82=\baG4}lhxx uWjZVhZE'vNB(i[LgNO%6-틫umb GL'2eIcͯsG k3l9Kf{_sISb_8qz{ 64)7>zwVhZVY37B[qT+NkbQpgm GfZ$%\!4`Z7g}kQܶ/:l髺Bt*kxKax<-,M^k-R'r@Eq=a=JX㱶yUx/1ac}xʲs6 OHٽ13y9eNA[OO/=Z}$|mZVhZ^!ablOb6>n%TL͑B1}ezq]eiI k/j_k#!F`G^Ϗѓح5,f(WkjW{oϜ0^IglY 9$ŘrH@cjځg7H}a#]b*7<әrJld,_a6xFyOrW:]69[3K걬!elK?Ac,z< 7"}?>}Q{FVhZV R`Ο S\];0j׌!AZ癐q}8}<12>'N nL]useEڜGo5Y_Glt վ <-DZqe.}^t}I*r8~`0pJ+9#x8bPOZV:7;<˱065c6wNƜsTܡ`X:nG1#KL'o\Kwt[>'1W>w${/2רu;]lZG_?Ga+ @+pcʺ;͖ƔSl̀ O6ôs#5'2'nqK{ۗnQx:4ɂ_q:O{0epVh>o %sH{%a1Hƴ'}W^d@8,1K8?9#bmr|<`8rQ@. ͨs6 }&9[s #c'hSxq/] <. >[VhZyuu0Hbyv@/@k}&(=™si6s="8|S^ņsJ+)0mx׽@+ ')WcAt>lcxShL9Fy3@;mM_+NfGjNd(N&FqS{ۗnѢQrKh_q:O{0S;cc:y_XFJp;c6 i=JOפ^KeͯN6?}l-O>8n  ++4<#es]dKH{:t9{qƟ7z7Jp^L+ @+x)`8r?֒1L͖uu0Hrȹ# 0UVϴ*o璎Hl눛[K")9\a9y5Զp}#ԙ%l"cN~O7$16we,̑#3%&Xq=O~۰G{7Np^P+ @+(P6(_ϿI$n#Y[$IDATTCȊMZ4DoDti_fgd'U.kf,‹&F@ =™?=/]o7ֵh/jks_<yl&350n(/lSZ|7ZOZKL/bnk̐gb#uM<\#y*19[oxq??Uoا @+ < qힶ;lnW'8_" }p 3Y7M,[͆Yެed~Z:"奬ZaYtۗ=.s-Hε/s/} yh]cS#c׹QUZ1*frG{W7Aʹ=p zgR+S/8%XVf+7n۫kZVhyZa霯eEs͝n<尰ƔSlԘ7ɁtnvDm"fl1}xQrKh_q:O{0S;cc:y #m%1HFǴkRy#ܖpgl?}-+"eHwad,#u9eKw?==Z'A$<˽VhZVU -bخqH{p [.׍?f 9w{Z+|q^ ?SF 's^flc6 )b^j~ј%CƂ@f$c6w>Gܢ^MB Ƀch_#&A͉Cub 8X׽gueK0(&~3oW&VPo<shZVx$v>զyDETٮW|/xޗ6q\Fń3oij7.jGy]6Zl?<50n(/󘱳zW .X3sڈ'(8[lG:WRx+s{6u$|wIh(9?&+~w _?>O}{{ŭ@+ #\£'ZKH^6BƀQ yp0r Xy \RlHe: cE/4Vn9r{ScYm_Jvt])vʵ :7vVp3#x1AFȘuiYo0d bMO'"$`~5v?̣w_VTo<{hZVx$>/c0m Ć6g u8+aϛAmi_+NfGjNd(N&|qS{ۗnQrKh_q:O{0S;cc:y?uϜ0VCi`|Lha#ÿ 0}3nm8_b= Gac#}8m@?ʼnlxrOs+oZVh zas4=I79l6#q-5tӽ+2Lr&R{&ƪv4UGNiI7Ck1lsh_G^MB Ƀѷ̼/"mu̓<2's+1X mLj~=~?/)c+d*7W @+ < }M^׍4g +x86|Y&Nl ~7 'ItWj/rYZɉ3j~վA9TxQTj8m;xjE LἮE{VDa6v^qΎ7*DnXm^OSyx-NĢ> 0&8qIKIh%9?&*8P}lZPo}nZVhZ@\?:fbYl) *. fd6TRmmL%S$sLo^f԰q48앷nq!`/757mtјN+s&zV:7V ~Z11 jQyhrzH{pʘ=6km:|\ƟiG^[5KOgZKL8̑Y,OamG&VX"=oZVhZ/yAj[ V'b:ĺɫ7ts5i{ K wpҗȜ (IVhZVh^hpC<Ƨ^t데^/ft^lL}>v 68:W;cԘډgw9̨yM-1(Uyn3;k `wY: Ţs]q^j; N q)[OH89gvl{VhhT~doZVhZVa);#>%Z_/tt:eU55swjL̀m9-8dw9gmԹNsXOyRm9$(&B7t-kpz|'r*1XNXb68߅Nt[@+p :vVhZVhpCcpÂObH7'~FJ9QrDbbD127s,c3j/p?'{&l7}lZ@F @+ K~٧#?t"jeSd!871Y6t#qBK-&Ըږ3HîJXLˬs/UpQИ)y\75r2sK__F6oyS`m,׈3mOc;oY߂@+0,ڵZVhZVxM݀ Kc ՜ U7k9fy&PYc:OumI^j,C`W2sr!ibf]{šӭm{.n`|Wb/~?ט`d2˗xzrup6$G+ <<5JvVhZVho^cϾ+0&loăqIS\a:WlScj'ƥMu2w0F_pd_z.$[ZqXbaٌG4ӟ~8ۓV+ |4GxlZVhZV(M*m~uB+Ҹ̀:Wpi" /18B;K'\'@1n5vI &ɃG~sęFk/2's+1X _W`sVx#K @+ Qc~Wc1d@?1 .p^38pSWba6b`C̋K}@@cjo{4m]^_a@zYQ74bɗy-2-VɹQ@+((GhZVhZnk{=.SXnr~^\bYHkhLmeWs).lKvܴj-1Z(uScj/k~+]4^RܑO9b|J6N㘱˜\C8?`G+ /7q-p}ГaϣOϜ`ϿϿhZGS|?uW@+ @+ <9 coJUy#mԦJ:Oۍ/O VCj>䧃;F;q.$(*.}8 ~CL]cz"kꚃXsM\ƞs'}M@~6 F[VQU@==v_@+ @+ < SX{w bc"lq97)|ZCcjNyj\k1;,V֖[bCj3Nab$4slx|Q0QΘnk^\Rέk,F+ < Mt\+ @+ xd `2bڌ\bn'@1 '678lsbIz[`/嬸;Hj'0ͺu)pݿȍl⋟17'Ư6lj }qlZVhZVppCeL߄3yS7 lŞ ܝS;qC‹9=Syj,&z~yӍ6P.sum/7G;{r<{CL^}O6[.l?4}lZI85ݽ@+ @+ +?~3O |.uNv#(6+\7݇>:Wp .z oS{,Q` ElVx<𢡊gu+ @+ x;n AA~:x6lqY6g1 g^òPy[bZ:W u~f/p .4_˿3/ >;X٬מfAp2Vs{/ Wb.b{@~xݷ@+ @+ \U5r|J C38G\bM5%5o>-ZAU;q40w>{V|rN o'Vx "VhZVhZS7>ڧ^/vWz6:Obx0f:4\3c p Wd1X '|;6>MQ gܔ:ZVhZVh\{W UK:_!xu^R ?ܬejZVhZVhSukqO |M^d͞f61Ըڄl;,Oph>zt6:?K e ِT6֌]dkNp|7ߌ 7Su3WثjZVhZVxAw~ |ѭ/rTM#ه@댗~3f@^l}HPLClvo=z͑ad|rN? [jhn. @+ @+p kO녧ySϤH9J,qfԛsε/Uyc=?snfğ؂^N|Ml@+d(O*[VhZVh|OSn}.?^\7ĤysOs@+P@+ @+ cn |&6+<?IuYE:W\mun8%I,?x>k=SsgF<F+ P@ @+ @+ c'8?0zWp1z!uy=7^ S2Ɵbo '@+ EiZVhZVhn!,T~JSQoms֛; Oq/Կ?8  VhZVhZVWw4"n<_̕È; ?R|İZpωp7x?>@+p= VhZVhZVF)qS_,˘\mrtn88'qx|56>@+pW] hZVhZVhn!sxX^8C[ۜ7^O̽>ωğ%bO9 (ߟVhZVhZV+wd,./˅c^P|k x ? ͿC3< / *ߓVhZVhZVR7^ /SI`wZm^`Sw7 }(~_MiZVhZVh\>]k:SzѭsگyoW>`P>^ @+ <灓uVhZVhZVIW7^7B)zlAz_ N<~G+ ע@|ϹdhZVhZVhymW?I?/۰ {@+p Ts@+ @+ WP5?qSq!XO^~OWl iZVMvU#IENDB`ic11 PNG  IHDR szzsRGB pHYs%%IR$IDATX W{lGfgwo|;ĉcA@I -ETPE"*dPK !!! D5@fBpۛlR1G-EU4`V[BG:T鹳;PP vh랸pR9sǮZs15 Xo݂O7`:d4 2@|'xy-P(^XŴ=L/"..͜زFf_s;ZdS$`]&/oz.G1"QaU f+^;]UUU"~`pquT,2l}(*/($:a[&K"8jƀ9 ~:8_J,Hw( [ar?/$w`^dz%.oFZCM{_qvn")k%kVg1ZƼd?%WLzgP'e&dDZtc{tgZ>mr[֔}Ra?xyg}c2//CXyFrlGm/ŏ-b8:5,5k[B[V *Qy< V53lPqcyUaKFo ? ڝMv惓'CM:ctazڼMy$1 . h]iFpŇK@0g.'^rD) ¨V4 Lx*NFٌ]?0;y|+R&@4nIǴ#rsmƃ=biãy+[HQT|,Lx==7 {|4땦n TL cHkm!+;w0Ś+#E7ھG0tO 3}5 奦E"YWQ@7ݝxY_g\.~'}C(?UJ3"r-#/h 6` lrnoMD&h[Q ҆WP툲^۲~]7>D݅OP 츉Z d&k>}]|3tū OƄ.͎;sO} xZL$J(txtlpP66Ũ' ?&)Xr-g ; hxjA.Bd]- >M"}ؔMeWؔO; 6K4WY ѫ RD&SYL%1lw9?UA>AC 2@60 Zyӑ"D K0qA#&`^FwOkCڏtlEt +Nx*?nەIENDB`ic12PNG  IHDR@@iqsRGB pHYs%%IR$"IDATx[ pyy,ɒ% 6p 0%4B ӖLHiL'ɤvH2L&L,  ,ےlٖtuU=++F}s"?Yew=O*u:sRģu }C=/u΋y?ҕ x1Uzq$cBY b (J-zjaRq>OxIdW}pmyA',α6{ ]hq [ Ӿŀ2,%1S}$$]i[ŵ\cв+,[yYnHN`_-@c@\V*> \`EF^FK[ '` #ehKpaWlmNq|sNN|<]@a*^C 3`>+)kL?tPm"#d$((gh3N~[b!Xha=R\@F_L|UE@=1` jn)2 ʂqĀ9|;?-9^Y͆jĀ#x ?RCje?Bn!x2 ȤةFVoPt#ykk44$Hq+TV]d _L1q7h+ N4Y ,7J)j; u?I_܁_\bBbEnQc}+C*;O'Mq[63L"?QiY\ݠIN)WS @1!-1,DU[QOR?>uVNL 1@QR4\>QvU?8>!3tiM)duuf¢[4:e݄`NR#%l_[l&m˹;^vUŰa6ӯ%y<= lٴs˪a c~9DŽtL-^Wh_z*kԜ1M(2QnLLxKNLy,lx~bnѳmg_ѲNV;tenӣP{ƔiYFN8E攙$oߓ ţ~p{+x@kWڕ^!ĨGphk  ` 72xa>[b FQ670.|xP.O7 㩲J%ǀHB5 (?_C:) sqX5DZƽߺy[9A _'"yo>[b\K no4_|Ȼc"R# !1|inI%eÒ0/;V+lud{)}Kd75E;߸mAɶ/sR:AdR$/q&ŅtP <{(䀒_uBY^^fS!n}^Χn~~Wbj'>`s-5吪45d3` 0Vsk!hxHKӁ_XftC-J~51QС Ь:v8/" VTNmþ[bP(}ʪظ e)Ӕ"醖IAҁ9ԓJC~NԻWPg,9QK[CR" @"fmHQ/n`h@_A3XQ|ے n1o;~օ."GOM#CۇM'#@"2 grO8GLjV;o{a|^w@N}}_>^CrĦdo&.A b&|t cGdU:[݇MlI~#,wNky&&[Nǻ*{'tD+\i#iX r  '~F !R[0Ip ]DzTy \@jZڭ7Ѧ+*aBQWWNL[ȾľIЮʢJ/&T \ЅbwFb#Zaе/mxc`}!Oډ "z8]Y#q]g`hd ` 4^J D9xTp K.) ^p_oyt㜣2g?6] tC\ w' lG$Y^>ɛ1 SZӿ @ d/N`~5{ZYN^X\N*#$qB8`A>W~rWwW_z :ȶb 8nϰ:_Yˊ1T^cC%!@KY`4A9BUYD֍]}E f7 s ?,e!*tF%,(_IB5B סALc :NhL=n&JJ嘀 \-[PPPD?<ٜNa^thh\k䳒 UyW`ưtLC7 'fN*.X#¹}u]>jӆɫ1 )*򑫩O=Wxv u# /Sg ~_fxΝU=|nq/uE1VA6lqny T$o^(mX%f_!l}UOMwC۹O U>Df @Qb@FN('~y|]# ҫ(LTe|4>2X?Līf% ͆6&; +ye_aSIENDB`ic13PNG  IHDR\rfsRGB pHYs%%IR$@IDATx %U&wy{TU**Jd,[}1lM30_4CM=qcamc܆o͎ =-.T%UT{z[97w_-RUy39'"NDddJ2}7T/Dq8~$*1㳓~pbFGY`ZJ}Q ~#Wq<x\hDmыҘҞ 5'yoԊ>!=~{ 4#c&)7[͉{Tz(._[91Wc|v%38.cS;.+Zx9Z86_7+ؑs`?uCx\*Mĥxc:x9:[ųe7n,o'^6`ЊO>xMGE.}'FM+G, [Yb8DUßﺓ[ _#-6=+)~ {DT/R5=&E5q_w_8|) Pn4"(e &q~Orb5/}͖ MxZQ6K-Mw]9s).ÇJp?H/Ù6-iyJtgCpitxx[=W^~zg`mQO9xMblMXosf.,%GD8vñZD:`AAJt@KI;#xQi:m[Oۻ..}}k ~ ;Zas0VYw:9 B`9ܥIߚywO޳ι<8XDowmɴv~=1x?|gw>)ſGݛ $aIn3r:ݹK%ٹKϼ6يM|S/[l T{m9mJt-vvٮDlba'p0]MܽnU>[үCqI6Ò:E7e @r|hgO=up=}a9~yOٴ~ ߒs6-.ەgu6ag""w)Zr71.| q7܃pQGp+qX:=KtuD7etzbnחZ2t)cSEo߳feC3"߷T9mKݵ+_RgbhV*JĄ^:O ޺Gޝrh;s&w^`XRL?Ben֫19lD>g{vo0_ fh5vЦ%enW~,ӵ'{8'JDWzS^@X4ϡ.q2#CKq%@^RgG]Bw%t:9ݍ{'gwU. z,͚M̍[nGΠώd۵Թt̾P7[Mսt(txy0~:t@7 8^@ в`xer톒-|w1 `]9mZ+3ډֶ:~2M]*2÷yoqЁ.4Uof;oS&Qv0ufXeg"t|td|qrl;%sڇ1[VұKw 53`]Wڽcm# .; soDٌ1z[l\`U/p}ƼU-ױ, GeWk=z|~+f$qv׍DG/6G7w`O~ 0lQ60>7)xy6"?o1sX Gp9ag@Xe 6ՈZ7Iyǚw5";m:2o|<EwF(,3SR(xP txyF~5Q(@wU8QllMr"վxr3vIm+o=f6,ű \|ft'оS|(N s`T_a[8Y5t @OvU)ӊ_ì `Q\/X""Ww\5dQ3R/ F_kmr "4Pc[6@A\౷x?̩^.*Ax6"l,!4!|:lekEWS4ӌ7\rwΌmj1sk9o\l.?>L=` Ao_o8زW_6VVo5V32W "%!5vrqmbr+%-FB`:eܯBsFȫeC#)$^6[i xPۼ8BBwwGz #d;̎te^YlE3Yӌb6`¤gh+vjo|ǵf ٦Wfe2":sye^: F_#0tv2!?tTV*髚[J( -<}xy_hUm_Qi'>szӵtTWWm5{|dYQ.(>sNz 8Խ wg|LuAO N>8*~lrAOGێ/4ンBl~W x6M7S'Ŵ~ǚ5Z2ྤ4}"Bd ~kwd^[blƣ_Yi<Ô `Ҁm+ǒ(l x[ HrU5W vR2G1,x+\#~aO֣a:uk:YkOsuvEM}z "|zaXɠY?T1#](<{/gC(_Nt;id?ԕ8^N!E>z&CNOj/ S n'7<=|?݁g%YWX`4>O2W3; 3=0psf/lE-z>Φs0]o0 ' y8Ձ(i )y_=.ѹ-[e6#tI`Itk}#J'm38xjذc񛁭+/_(Nb\b'7+xO g'IV$Lp^tw,jt|BT$fu8~QQJHdPa7}[4 D!~?e]s VAm//C 6oԀJ|f+:l*~mg'T]v5}~9yYӠ~с ^KVn-P $m:[HǑMfic z3rI59 3%g[$`sv@;^_ǖ:}VTCPXrS{>x3Sߪcx>X MLJ6u`ycu ?/z[ 6, ( `!3o^aYjU!V!`NnE- "`.:*8f3":o3 m˰z'}fyI:611#،&k 3ע ()\s||y#t@3m<̳ ؅t-ǧ "Oǒeԝ nj,GL Yg * f@mr2aZGge͂wNk:}=,O`@HI:TzHPRyR1k4ȃxB=k>nқxN(Nq;A;[l>a}tiwd|;uNp`*#J XР L !삌|-U}7=TSq9/-LT@_b 4ԅJfhi<ِ859W_cιV~=0b3_֞ۚl}tݵ j3;Y[ۄwCDITks9 6X%~&TgpzJ ZhgAi[ ۩}`p KKW7;d|K;W<̋^'>xv'pԵ :nƒ2ߖ I9 &2W/j2ԡ* J͒If6rzxN6A,Gv҆- T}K@vH?QHӝ Xl,u'AhT>\^,}a]^kxK[΂bF>tdJ uf:H;tm;"2.%$,}tmC~ۨ'NJi.R럫 ucbznƖ3& A<3 />`~|GK[9$n3)ƅ y: hLH]vg+CqP0kqv\:g֥[y͵÷a%6@ԫG[ `OUbʵ$w< R`-:< "ѵ0~% ,\M9)i(|\G 5tJZ[ӡ'0tJtvyLiכn 8`_w:xL'.miɧ39JXKPh;|Yv&:Hꥮ,P0}f#;5uR]~'U7h|'UQfuNt٭B'|XޢPG s`7)UD& WfV )yy nO3K~fD R^uX]l^^ Xy3m^TOcB<ȷ/WUzT.NL3%`kkK {Э~:.`5kD:2Im|& j3&m#x v]TjPPKKY[^iV/(az[ 7Ƞ#Y`IoQPl[yW~!:~ ls>}oms~A]۟l?}0mڟ2uGMGjC(_Y{$Rw(xtGfHB/m]Yu2Ը\Azl'.b&w˵:HY6P*zSH}m6̻>?%+:vX6/0ں@a@#1:{}9;L$@B )qNh:]f;:Dq\=R9{.KAM,֕ j(m^6O~U>ȣO,_(R@r.:*y,,PِC*NFJ^MJ ].sцNqJt42|2S }pq׹vj?]Q@,1θ~B$ryA`?mg`ˀWmڼ0~+ɹ9_  m~} ,zH@jLʹ$y_vKo>%A۸6}W&Mff&N||ï`|mT>Cl =.nIbGg}m ptg3Ke' ">7 g;#Y \,s5]3?^ۂt?V+x(*ri_}+R+l& NA E/-v.)pe@ȗl"@CgXξLɀGlDdLJW~0 AtLl ]v4L7 mc  eFJ~ʇuپV z@߾pyuQ_N?kBߧTDӕ/nѣdy*0#zrj֮ f1g'kcx׬ßA\#;pլ '"<)ZQ'38ЈwNRa ::i- A:| AM@ W'vm$s(}돶a!i͋ZT)8t[K~>p@KJ߂^W5i\ n 5_Gg+cari(}x j:6oQ-^ Km IՑLH%΋kĩm P-Clr3ʩPouQKѤ34SԮA}?tCη=lyt]V/sG #ܣ yt'y$%soO_1KEha}&(\*psN)I h: :#,W-  @z! GRA.P(>/Y ƲZBAWJ9Bb}ƳMkCL/O 2l\)=Lg{r+z9n.xD?+Y@RZ9tdE/v`4Y'' p-0fi 6j_]l"yaK%2n> >A:S*.9<M@гiJ;}^Z8//3 hsҼhu~ZSZ0 L>uF:?~P+$H[]?{nPkg}q}w/O N>u ĠVn<4olZcNQY E[ #04Bw32S R(|)Q~XfQྟ*m$o/@%G_} o_@V0|rzF^Ğ-P`q}/Eo,*&8J}x$T L(΢@IQi^(b L@xdHg,(U@!Ll=>Q&OW Aȏ#L~B.Dwgw. 0"?A\(?[Q6BAϧ ʻXe/ }Zߥ&Ǽ8q@&N</or:E2#3> 를 A9l<TٶI}Iozn8#}/H] X xJKʳ1"\"*ץ֠A6(XXW/um_n_?9xzlsL= gݜ װ[( UWo! p.aw:Y .ЗC ~&6y{In閦+o'z`|\Xm3WgP%AኹY6Yk|d&5uN@!o@b2`jBȴ"lKxdۧm#O-E\`y hdʺ/8TAWڃ|zȣvh?%ahvi͙9lZ)iAg7ݽyp=dbt'O&_NZ{+,Ex"_ a# Q<`ZU*iX D<.p"_jxnq Y>>fJ<~:ן~ôWjDiVa hUj׉}p2\~{~kPy.O.va{ f#J%}b^{~{%SW\)}EG%7Qa/\ sSGn xSιn vuy:K盯?svS\f3֊)j4<*x0 A:!E|d.3AoUoM^1x(*p[vo98e:~KVP ]pvDr>O騼sF!|p1zg.6ߡɫ:v6c !bLSZ>Ϙ,4T;fLj;S/u:q2o0WffVpFeXn~ߘZ788whwۀDiN5M fT:@  7 "甡-Rtg(vz.!Us6`DҟyTP$L8FοPyr =n\Mxv?m7[Xc~ǟi݅Yvtt✸P'w@q: ޙ [;@MCݺiHg(jnډO/VOͶvqpK,̛<:A@ OOnZP/tD=Cs|JW MHfަ䵖$WT,-&u& 7/'3x1:ܹ߱o.1 ;xwbupu(*~řpΜv]!98:t\oN9k޵kOI\l}[>ToJ6UKw/梞 pk2&\1fql6u>mPOhDbOt_ƺdg^PO{ fc ~k&1p|}[7!^tHo֢7>ɳ_Z@*V9Eiu櫱h!qZ{MH{:f1\$Ոn_b?OaryVV|!F:P;)^dȃM@`FKt{[OSnJAۖ~#_whh&_>Cl4UO7Y) N20 (ۗjK 0@̚Gl^=fna^mufྥe7*#*V 8N`@g7z :w nY4~xm|lV6>'-my:om6Z~qAIwzTNL(wAЙ:tU`@=lPtP\42PyUnM# ߴ܏|tLO6[}ps$>xf% _2H08zn5ԌҡԤ2v/`0)jw, ?nrz<gahoZ*fN;u6K🎪D U5}@5ׯ7;w$E]-{?+SͲ%a`G[75xשf|^;Zo%~%e`U6:/LQ- \B+,3u #t vͻ !uBʥfvS;X&A#A|`Bm}r,ޭk{^ua7!A}@+n>l.4ԗ$}FߥD^:&+aۨNz*fz|3~ x lȧ 8Lg'{>-^3a\]]xO'TSpwN9H9m 7兀 4yե@ tL)K墦>_;vc=VtyZyKk-#xoZP|yC"pH:R^Ա|+ہc T&- ^d6xTGOv\^WG8v6+4`Jf:V&eȨr2;yu 潓ՁBQiZtm7$<ۍjvtZ+ UX/m|ba^ ^-.R/Vq*vhs|Qpj2t$0t tA t2RZuѫհ *ح a@d72頪gqN'N>7jq_ѶgNdFOAѧDkYZSa@kl\ķN:y+6+vy$pNHlRÙcuSt1bvcG}`)V_z=DW`g6.Q[R$Tܣ*џra9u) F+AKtuңR/}距Y=ҶK V{yz^@UPy߸1nӥ 96d!)x},2> n@U`P8L~ 7; ?>ҙ.~EY9#1j4;0Vz;9:m; 챳iv2ꂏL _p |σM+em ,:_N>xh^_ͩ pmB)IWj>s]s~A*ؒڳAf3xپTFN&՚ Y 3RΙ?S;J  [|'vq!hIvȷU-8lŖ<(Ȩ$~[G`+1WWnȺyԑ$W"a> K2^6Vs*lpI`\(L0tN/N:ɻꦾ\_ԶlW8vF-]~@IDAT:ڍe0Vi,; jڕd/PZ8u: P@HܭEVs*xC Tm`@sβ4 Ꜭ+)KGA޵+M Noɴjo6IჺyeQq</[83<̋ : 4(#Z'3;>pp\ ss@ve k[m2vߍAW@g[/HV bHy*r[|8NZbݫ<;46 ̓wy=Y3Hʡ:@`΀vҫʼ?8M^:jM솼ﯵwb| ?~G]nInB~O p AL`*y)k Mڃ9 ;Dy 0`:Ty:,9zސ̨SqA~;x }O}8p m!; Tpb7,CՕo"+T=Bc,.~WYPOEGvRge͵j!6g)58'e=x8;w7CV|#>ڝ1 ` $遍 K`IX]/ (dCz%e[|DR}V/EΖyffVq*nQe@p $OۑAL_`:ڃk ڨu1{ˊoT&.ׁnHjs|nlRvh L#yݘvrW!X:9'ySᳺ_`Nt+4ƣDsxttN$Sy /m< X*PbSf}pQ_aԼ :ڍv0 wP= }@*@m[dmY1%tf+է:yz>\*5.+-a|_f% q19fo~|曆}Lϟ Tn,VC*84>6HBsn,Uѵ[2A^ "qf(yIY8eۃD5㳰|&? SqbBY|վG7Qg0f\ӉY9 JVu>l]v|IDr-ˬjPz&:}vdV/֑ (/P:v;N:Y1#.U%4O՜ |?=>mn360d2Ht *#(( ei.o~wq:;gt 5pLQtpa,wLJHY?QXQg|!]@z\[`myNmvgP}JK5PI&U M=g_m|1lsl3my;Z4֢  :0 \P<+cz }23uGm𭒙_{YsKy~{v4x+=5,uK8C`-t|J!6rjfgxD촟2N;^ʰ |E96gRi5[޷?xcɩ}SʉZkz{83(7q ~9Jc*}̃WZ 5NAf@Z/n%lT<?6U&YG燪۰~ќJ?c@CAr솦l%ߜ<]X] f;B)X[FmI. ԇ2*(p3#pR\1Tٻshh^ӍSON6k1<M y yˀ`1 q/ nPvI,`T{Ba罾[EókA|g136{-6 xvhyfp']fvdrj}ElZݔU D% `_L-gKo˻`򹁝>" ן.Wu*~_f 8LcZ|nF/<|ӵɗ#gѵp Hq05=񟫂UB U:wmpmTs[cxNV_2GGau" cc5i2́M湾xe][mK -g_I/~ 9맭98t'<U^x-O2hAחB*lHR4Gp@$ilr4tӚ߽FA |Sgo=fߊWw[8Aw ͺ)-̪`W J1ew/2=q"60qH06X}bv 8xa~YGln`ߏ!7<|r]t\7w|(yo׷üPVyn+'0rԃWAD;C}bs^#S l/E;1Ik9>E>xNϟw_| }6IAsfA-A2=qhx)m^۪|d۰53^{Ɔfšdlr m 0pB<0r_"=>At c53v cg+;y vѿ0;RY_)#bF=˵%D΍xB@e9@)NO}}_.8NvvT#[bc:7vsyӕ迫'vȳc!ü."k)x~ >H]zv.dh{ҩ#flyܴLTmt̞צ:Y;82si?`AY5Ks"φO:1鵃Cp?2g2qCPDAC&6FO-7-Vb#|mdIQXg^rVXsojwKǧ+CL/:P? K5zA:VRv;p8աux1qSOzbƖz3cqB[MSi̘,.ASy~šg@~{|3;gWG:`.%]srV&17t|ͫMhg|혮 P:fr d@@+34*zw29X;~Yh>qgYtX꜊yuP T,8U~ARC}6 UՎ-6h[ytc\&"nLmԪNrft7?k^R$ @dty:0Z^?eQlۖcƖ 8t/Hlvޖ`_@;q/N"~?#̓f 8 T'is*nN頋` J!q88gǾXzJRy<$wTg.dzb#n׉nzhnf=jz?a 䶳WVRYSelXfgQj+7^& l4'UFheKՕؘ~gY_2~yx`C|n\V@61Jh~l_ަy(NW4Lcm;q| h+Zhg-޿S3OH=|u49-O XtIgG&4W"汵י׆U:}(a)Hڞ<2S;~v,R_z ϶P֭O-hVJ\;ab^&=bBD֟GWȉ˟[rc2 Mu:N^ؚ̗NQ1Pli,y?.ҙ׀;&`$gw¥?%v~X<:E9ϟOS@0 E'@NA_ʱ*IGRm{FDy~5ܯd_U"!ڳ<' З;]ۈ~ 4礍o_UuRxidxAd ]p\~A]:mC^R 6, r* 0V@Atz7:јWU 6{uY | Q+)/ךA^[:/%fANC`ȇkIKqg93P̖AcךيnҶQ ,9_ l7;l,6-7A?WGaSd "F .{YP'w*Q.!)^_|}}{N!s8>xN;zPL9(J3 gG~fR<$^3J"|^+kudJWl{9^iQE^Ϫ ~?9Qi[6-o nw"%jcdvS`>@4p@l~ze/-g# 3.fj7#@ ^Ny]Mq 67Ll>r/ը 3+4ά,A9kdX Cк\?mʕ\> z@Ek" kKz8泓ׁnXG g;o&0O{]Yy{  "N^XEBÇ8x//|5. YVOoЁ×rDY Ru%,&^:tIW:DTXv똡i~ '[/ | {m Qg``^6/eR w֙T^z}G<9\t,WƠ.tDlYUDD&tv+W^9Q*rG (I zIS߰~,f_MV.绱_a>~ ?^q 6Lfj:gG`J gA-V⌟(&:d\$s%Qn-= TJŬ<9 蚵/>Y2o|Sv`EY_O1OW7_ܿs{-P`-;Ss/=ZfSGBΚk $2Z:i,@g<\IK-/^=/4sOϢʂ?Ö s}#9r~ZzҴ@q[]35 ٛ/ \_ه{x֌a,]c^o>m} Iܮ$"‡. fE{bX_oJשr2W(zKCA u~o_Aa* Ђ?G:;? f񳂥Mh*QZPۆI4 ڱ\KHKiȑsu+6"3/koCcatZh0 $:fJb6[浳f~qʬkM=f֩h,ȡ;U%2VWZyqՇ+/OUYus-O>bH$ 5:.U ~{Klua2W^dKCn>Y43̴鯟5+fSX3ʾj@ UF3O!6Sy2?"=yN_~N dbW5i:2=?rVJ  g@J Y!k3sQSZ?:W3Κhh&ξ|u ,Y=\,rr/?kr+6h`Oѥiʧ4;*9[I^;?A),n"f\^VŔ҄z,> ~j\5 Zy̤;kFq۰oڌ(Gj @?B8><|65 R/fM˻s,T 6P`Nry=~* ۃFD#8GkkG;W" B'Z Wl_5+]!f5'N ?nL#yz Lퟴui6?f)8ʂW]E/ M@퀌?pޕ}\d!& O& 2ӆ`wV`K<-@UЗ 8|jQ2֍#Nq9|/L%6 us1yت϶M<Ϝ /KWeOe_w->b7u:p6$HGp|$1ʑ剌Nz*=u׼%i/km(Hʸ@u ->~**ظ|33}ѣO=hy:D3LW*#V%{E@1g|v@< vQ䪫y`E%~M`/,rZqN^#, #<_س 7^{`3` LjOu[}\)V2!r߆Q2~{a>~gZz9ߎ]^:_ 882a&zƅ>pv-וALvro ; _N`z^3w|XGH d?q{;K%]ىm =R9ެUy;S -PV&MOtHBNW/bw  VrB-t t;xt5 CϬ+jVsO?PFP ߷{ -_8/2JªC\WM?te"n~W!]'$GLMx%)a9ѧQѯWuQ;X#8GϜU7\R+G=I]μ3x_΀,F(B^G4W&tG;mǷck[*`z;r/h=td'O~߭j6$@?$GvV@+/~Ar `)w^ ) p7tW2;lANyVe#+ /4+0'"sd2h~a7sW =kس+JP߿=Z &`|vpCycglPl3{Zǀ/ae= > Ct_`co4o|aցZIg)ϣQOn~zoWb0Njj׮7?79 ~Q+\ t|ڃ#KN З/HP|.(`Zkg]ȲRgvZ- ecu]4 i/tbE{Hfapv3/аJ)YBrnCtY¼kedWCS*k4Wh&2GNOϛͯ. 1"Mg?id/w.g&0Imx]fFYWan Ky^$h/q_PEY:3+i+}GG@t%t~wbYW'<x|ՏrvoujH G g܊Itno A4  dz <`t~+OGzAz`P_PY [fzOp*[NgT u!?nL"#8;X`044p~lVn `)c`yǀ;l 7DkPLVhԐ6;y .UtBSgx/ t/r`,\mcdg\otƞ8zcύ֎MwQbԠOd |-iU aA9$q h4|AKA-jDN5_ Zڜ U UjD /J=3R;45uVKOi\㳘^ WS3@/-Θ_'72Ëp9Eٿ}tzOF&o;іUL'nbQ` Aa`(~{0u-)f+%?Jׅ\z i}Աs}1yti&3Su`xЧ;iإB&夿c |#ƒ;?Yp6. #>:cDr`ˬm8,hX,:*o^7q8O7dz#Ṷo b@(ȬliS&+8W.r.4,>P ϏC<噜@6Q`p^:Y nD\xYzخOoh&M+&窀xh#!]ī d/x9D A篦u,Ldּp^&xO.P$rH&+:M|7<2`b`O"<Y=tU ۆԶC,Xj{\ Ԇ~i>}աX1hõW9ylz)_`JR Sςg ^&ΏA[6]-QU~0$ :iĽ| 6' gJؾ 3<"t&Kxj < l8gljsв~鸐afhGYi@Xy\ÞpfXd&/A[_@{f*g>Tj y?>` Q]Hs3upPHYH+ +6iW:|{;.< q)_6MS%VLLg^ۧ< b3'?.;k~ gp*:Æ,M{lv@YxPtݿ6Os9(pۀ`預$-H ZS@{j 4*㟂G??a翀ו½?ԢD9g&UuNu/Jꌮas碴:=68Ӑ|2ds.4379.G/ZS:5oX!mCA |#b?_3t ZKM\TH7nwgjQlyi k&&vnߝ]t±cՓAn 4te\ڂpR 8S)eJ 87|ÿ|W\uUg &ysӉҴ^f@? M>v_sغ3r?u|vm.] I)Y)?t;ѩCv~ze/XV 3n/*{G#{ײ % B3;^| 1.;ED^Z*gCZ+[TKr@}CSxe,^43uNb?g/>^P&fwV ,4 uu aD3B8Owk3ΜN˖ZSoy~UsO7%6K?/ze\Ǫ᧎L'&|ZүheK~_$ڈ}*ċ]D'BF.+LsR!3;pIt h_VrS|"ۻlߺ}k']5CΞlwvkls p hu@ y1-egx0\,wIuba.SMx䡧Nl_mT{ƊUǬ_&aaXV3Xk<85v$Y3Fzqr%r̩98&KY(gNʈ<@C99Y P?ݹ0{qAl>s`w|lOvclj|ޥ{l$Hu GB[£14F3HI8W@KH6OjSC{Wuk/\,~wij2]> 7 +֗lE71Yƫ[/dqNfm!TN??(Y"1dYnYrMLnjFdY!w@s]7'kxScӫLwDvӻ> ggM肑`/)W:!ѩ N:A۠DtCKm@1|j9(vH5r:u_0Taq)[. NӡxDlp-<\ -|@$6l]}uol! 3(]< ROprﱛSjAqF*URp+Y&=4:Xy㺁o^_&wh=hǎM7ñV2)0^XމvwyE׳qv}!Xnfzyxpx# m ._Q l XZmm,mÑ2)^Sp@b<׬@gMg?oHÝƿusEX+p~39d5mxq)_kоypԿ~A1DNW\_PڝC?>zA3''ZPͩՑQYi2+}Uun k0?sc%t 0I\pT@ӧA/#5o.c7+z 9[y ȱk<N#< Za?N g󢝩 Ϊ񗶨.\XI܁zoHaHa*P C(Gڝ0:nPjLtrL*J?:rA9.rqҜ M6^A:8&Kppjl%x¸̎8ĩ3zXˢ|§_m$Y^Qа+ "tKbʢ|$с@rF`%pT.~3C2r`Ҷy9ř2DY;1 { 22 5UeVЄy'7Uh!ǣMzq]'2KKwqm*93 ܑ٘APԕJrg`CYT Kݍi x/u%9< NuP1im5sHC34|-:~Sp+_&nh@8/yӃrxc6e9/qÜa`(55^N}y1šjmʔ)]~a9i-!9NgbO`t@`]5#=Ӈ[9e BM}PR]v{TMO}6=s]y=a6Ĝ f2; e$0Y &KRmg8UTjefxuj~ۅ4Ȣs\^|⭅NZI3+DcU][[ @=i fOzy3\5.q X;3Yp~d~L%gg[%6ĢNkWXq&W)~riP(R n?(ɵL A$1[ ,pb^5Nk`Fr(ό t/'N4@he"r_̂D؍ܽ0>zcǣc!bt\>acxL>t$r8lVm:6" NxM.ᚥ(JA0w/2ECGEYJFHe:ng.V!&W,r @>̯+ Q9=д=@o(L,ʀ~s;/LP+#A. 4 ,Z=lZd0s\u%;;;,U517lp03 8^ J `h@q>uv3`k e:!uVC_)GQyv}u[ +ubv Rئ\d{EKc9]/긨.WX.2b;tvPU +2eD#XT0M@h5p3:)*t=:/@FY^51Bɧ1ADNvZk6k!koyRSq0P%zfN WA:kӗP3A seq1nC'3;xeLTL /$>mDS^Sm::aѩa4_@s2 c8Q*{4*_E]xlBlh,Iw:f_#Nu e8UwC9n|Q>H-R(YEݝLEA:a"4F3K~iRCUt463кD5wTI\bݑk㝀"T5P&gh paau@CJŞ$jP׆>[JE p z@[Lgf0uI`y{~QIVch_UT],;kKBRԅxfecYsy`#ft(QMaERv%ҳ@_e(M*R'`_0;@aT.w,IlxEA8pssʡ11ʚ㭅] _֑wrpm?h'_Un d7x Igr>`uؘ'<pO1ro&0d ne$O)B J(HOLS2^ mW<~_n+TOd+m e ȊmtgX?l0 )8!$:I_TA0pmk1|,}1B?Ǚd> Kb! ?dG[i+ĨLf)6GYG?p#^FQUI PGY ʬԧbY'oˊ d2}ʙ \ "P=ГRjj<M=Cfo@Ua tVP=p0pH"=cWf~=#_mX|ɱx"5՘K`Ey>/+6 )uVGUpXGx R),Rde LP!PTPPT##k~s?!mf.=L ^.E, JFT z=_[zIk0$Pނ\37 Ht nUu>IO^o%_*K8<6~ kwԲrHC&Rp@9eR96 Vy E,-P#\/$B-@$3qpQZ`I@7g,['+AsP:ܡ2h,Q*Ds&pM3_eE&O /jΪP{FNvdw@sSQ`WWf)4H .F>Kׁ:j`c # d:~l_`k$u$i@e^z OP !r5`h}Ukr)$1Cw23zteӃrx]ҧȲ0H[ o! L<mXrspzFRN >/GvgΑ`G20܀&CN3gOdՎew~/.PX! Ϋҩ([ui($xzRg H vs\J`q#Α5Ca0؆|`0"3IokN%@ӛѡd>@W攐Cb`(b2{P?8/|,9;qV&EgWru&HYip9*uK4Fdƍ;ټ['Gp=y+ 64afY|3:w_iTS ZU/3V (Ԝ8qg?țÖ.@ ABʧ [:pFϯԎ/QйK;w!Êk]rPsF:C%zЕW1T=_PWc;u6l^A&tI^$٧ I@cqO ,ƞqW ɟi_D>x&eg b\]Z5HWW9I GLt̄}&͔ M\=6ւx⌯mTYq3yڗ>"Yr 6w|$?뚖@]GF[9],::q 3; "0YdPPԩg <2̉ENMvy}?>}Zo 73> :c N 8NٕY >: 9$.yپ[3H';^YO.0-n#@C p@'UԤjW?9zg=:b+B:,lY9;}>a|+M8K,ZJi9,Z1$3tGZX:pc L? .AC鳧A18_%xZKp^&v%|-3'ttf5hK 8:qR8JK; #3GCgjC+50`IlI: *'ғ-WK=->Cg֭$=zmf/V7$/ַIu$N*:9:5_/c}LlGWgr}),plIϳ3x5Ce}[% x [݄4fjW>]'.b}y|8gr`Gyg7:&Acv%l9-k`O݆@nm6,a}WEUe̩A#]INd5 e2[`Y po>+r8+y-6ޡڗ%WI׀ޮcY>D|S)K$ tBn% X  8ٜgsV ӱ[>x:U+foN\W;Y@saP(} ǃ=gZg׷_ >|Khn"_lQʁS_xc#/ ?#.?Y_-3eܼ9/knTx ^Kp /e 7qrǞ|>u|C'_G,+u5P[ lCk۝cm\qv8UB>8+5@ Gknu5t4po9e*#7_c6pz;qoȣ?킙~EA [ѵA7aps%tS@ΏP u =fO.O\鱝OϚWA҆qIENDB`ic144PNG  IHDRxsRGB pHYs%%IR$@IDATx eUx_%Ukɲ_? -_Lw tFztFgN#I N McpC$=Bh:$#Y,ֿ{sε{}^I[笽\s9{w߭vt ,YأYf]QA_s%iUUUU EPأ߆߁8)|{f|ڱUUUUB@\W?mX)}Zu6qOtGn]ǹVVVV mp\ 1}ѽX??W0G8<֧ gY> 3!靂{mOZ@@@KPx ~|Ǐ~3a~=W{[Ud!+{-]ej \hmo?󉣯*Xk.Zqy>RZa砸7 [n;ll@Oݍ2U,?-/@WL9!8ovZZZZ^xfx' `_qO>\G+ݼvqnFVVVV*6+RވG1?oqA`7 )!(Fqv*VVVVhŹM?wѝX0cp xozo¿?s)^6VVVRx!ջ|),ȯcsoKx*6|?r \hGâ.P6qw#}s!>S#I/K; \hK2Gy}eqX,;ŸFy#]+[khh$+6e_;,6.Z ~Z@gndž>T?\}} VVVKmp)\b GBh₮EVv_l~Яƿ~w ~=aZkhh+6U?yaq1o"n+y^#T7F\n >Swޠbo?G97wھb-V Ha.&?yWb!lwf\I,_{rV"E]w5#bSpE~cމqx3^&F[71( \ h*҃Gw1 = qHZ|e>n yl**ԧСi!M̲_0>H쇐_Fc1.s=_{k&FVmp]H>uOcf L7S~}~Ģ#-,a̺gT/!~K6;F>?^Ρ8$9pm,g~-Ch2B>)i {urmb2"^ `d2y}{ǦtxǏDԿ8{jRةQ8S0ks3M(Fk/e६6̥? xq -qA1cQ ]eP/3Adg.ҷqo'~Q5#jE|pH!澉M^vjx*6/V!/0Y$\#&4r,@F}McɅ4 >\X8]睄__2oE"%67g'z_//+%?Ő{my,c 5SgW$;> ?M?71ZڱUTmKY_?{豽;,ߏ9 &Ssbrɧw\HN|~wo7g7dx0ۗ;y>XAs?̠o S^a}ّzZ@Umέn+=JCvE[tvGgކE41:)j؟OՖ%_r#O`C_̍G~I|E{ZU%\g5x{q>_@x3nM/~{(M,K9]W Gqoy{{_tȁ*bTm^*y]~7@2{\⏛%u~ ̗wKٜ,sD{NGd?g M? [^ yQ_Iȅ>'<]mz7&L_q4s}Szk|ɿ/RyyьzGz'tCc6O3^ P7\۽sd[f|,Cyr';Y]mz7&L_q4s}Szk|ɿ/RyF#a:Oҩ|_\ggo+GYV} zW xӏAeNr_[{`#y#oc}`s|K4*pTmJ`s1<7qFh[O҅$-͓MUcɅUVqWy%ʾdKIb| _4~a{cy';z| r᎒}k;o=NkyA}Iy,c,͹xR)7w}෰{0[:W Bcy|&|pvI݂~[hj%@Ï%}74î\ض># ۋaS\ls`VqV`8*y/'= n*;k9zϞe}vkW]tiCg^P_b*6P|?'zs=:KYpqe>\?"0xt=(U9߲|MH =;y_ۘ̾J_2a|+ދOܷk 'S w{aT3:; lػs4? @נq[?vdvw_>})}Mowr?|i:~v\wPlHO%=xX߸I=Qlݳcon /qkr=vjx{-iNh_v;Mq9Vsq,}r:×Cuxo!X6mBh\|833:Ϻ[1[ָC@ۻ'w="Ǘ^_m. H?vp a)߇)x~XVnY!} *plcz=~3ͷ?F@lbu߉;OZ>o&'צ;;t_ymlVVUXx'y%7R/?;)xlx5w K\OwL_P)e~omᣓq/ǬJdhovl wvjQ'S- 8l6l"8xb b?)ڋXϞ~ xl.Mq/M+d#뛷axqS,YFh*_E:-|>`ɥMy Dg*69⺇x/~ɫ;L81~5s~n̶\LsÌR8MΦS\ mоE8׆>}OX3{`*5{wuݝ/߯g.M*wޘ -nt_F14lSX_ wg>Ēy-m @lw.v-6_w_w= XNpGSMe>ea3tݟ`o9jeZOONn_|4]}$Cps[mB1LXg<|@% }J[a1a eRq%9z7¿+;lŦ`Mˁ{ tJ˼ڜ}t3ϭoea?Zg7xWV*/ 3NNgo{\8S~wkmBq pt'g,(xjɥ>/y .Gݻv&bn¦8r {5~?>vc'gu2=:;z~*CS\&=mL߂!3:v ],~pn.zi6+ԝ?sx|HzpHlLE+B;v+@;s׎݇w j)xh}sf \I穳{&UY(4ڪ&A_uN|~lZk |n2Ol={(!IP[Jr[k8hG+_i3N<|ĸGsxqu,m>kt'>C|_$;_؜}~}2[û{a>oZTpYg|&3/I wR >ac28Чq7ӯ D;g'S|v`2(`v>t|MرZaqUͻ Z߻^TZr~ɑק_Ty s-ɨV!6>ڲHTkmBYL\ص8mW\g' ә6O[o37 GW-c>9>,tș8xU;L(^\{|}_&|$EK)Ѕ`3&(!pW 'ɾ3 $Y-}fz'-ikU`+6+T4'1XG61qc!RL h?~Eog &߹suxgPn>sf2;Ma /Y[Ύk}>Ͽ`XF;^~{fw`״{ h<5yOSjIјw|S,H; uW)}<|>p<+qAO6Gͺʑ9#':ѷw9CnB|&kGv.OnL?ϟY;uv:Y§PN|7+}~ /ϸ=f +vu]:xl2{Omtٻ9*>$ɰ:fl3{޶I)xZmB-b"\->pXDfx{W@8L  1ƕb3^^KӟPX6};^y~F;w:>=w knq:sk~̧Gz\}:Za#سkgwy8OLn"zCpvU,~'7>Ą?!k1\g by.Y/M.:T*h Ǒ/>:darPm3 hٟ*p=ɹl3 >0 m.8͎o<<R2+=l96{l@oqNOcaVcR Lv>bv+lO쎟t{ !|v2v*0LހIגuS%Bmٸ;Q_lmv\_ OMfgמWa2h䍖dqDK/U;;&dS'WWW68<˷vWxߐ'f/q?ȪK_XXr/`1kZ(Ca#v/6C\MEK3DlP&<! l cK6G>Ν yõX;1o|i|vq;l$[D)9f#/i3q@1x݇ƫ;ONN: ̺k^ G'?1tm.lzKơ)I֋77>CkB6U8+_bƃ̉&/-tr39Gù(;QqC\E\mY[/F C9m1yolU*8gE^ bXK!-lHvC‡c}.a=}l;>w]_&xjsopIN\C0yt!hOĬ䛸D5W,jlKp -Wes.F1XI J52:+$eA{f˼sKG\bS7^ɍ 37^q_][[PYwOn|s\H BaM,{,BΝROҲ_'ǶX*p>*6+T5 SM8,g8s`/`s;/pv./D o<:m8R}`j.D ':OlP|?X +z (K4Ve02' /t9ӷ P|6Î3k'|'W՚WkݓnD- d6]$CQ$g8\orJ m>'XK}7U`*6+R??|xI:& >sx0gs<[l,6ٜt)^ &]փ㍾dj K}G}י\SH:}J_@2M dgzo A_:=ds6^)evxvsvWm/f1eűSTD"1Qd=Ѵy@o}Ɵy~>u_Ć F|}Y>yy kd޷j[P} 2Vk^D7͟Wz>[$]*y҇3~56&i[c3J\)&+6 hJr7\pKO. UO9[mB-9"S2R'l%EpM0:'.gg$,<}3uKÿ` P<ĕpWg ъI=96dJ OƢ>5D$O|͐8$m#t8cσc7mkKǩ2q3w6ņ5|-%~Ѵc'~ߜ"jIdX -TpɹSw91V)lԵ*h p`yB< TĻ8q/džc&-h$"sC\6 !]}`5?Yǁq˗CX>k@r?e(u#WTqzvf ־yeJ8Ww9kb/cщ癭hV@Pf]-WIunydt m1zND|Q/6aU-t>,õ0@5]}$ɳe T-+/Qc= H8Pb!ᝏ#uz gaިv\k\'`Sx|s3ýGCY)5$Úd$zxbrɱAQx:'ڡUTmV(+jT3|̥ @^҅ 7\f@~?gK: ɩEm_?F<+56ZήOqFB1C 4duM>gap~f ٤#DR?_ c==߻ww&DxQUǣ6fS|ٗ<=2B$CHq&zjlz{-euvF4,ahVm@Pat''E z !&X͖'[l 9l'~\͖O~)Ji~j40v[cYZ_5c"h<)'`uf_6%|,P$mwLUS)\\\~8%sUxϋ32} 3n=oǭS8?|r}v9Ӷ}'ݓG>tzr'|PdI Ëԏ/۽⢧|ES4HcVm@PP>1ד-`sBlPC'O s Qg,xP1̋X~4`~?^6t~Ais [m3u;=:vdh e0RxrQBǮ' e3" ѿtP a.NCTqrt+ϻ"s!q]uŞor/YŽ_| x`7~p;`OXLv`$CH2Lte ˲}a2OלVoXT˪AY5L$٪iU:|ܓ dt8$녃2 [wb_6)f1RLڴMx 'l p ?'00㵄hD\$ӡlsL_9ӳ_CW/NdIŀ'"6#EqcnL>\Iy_suF 2xO\>|b}܈w ~'OlL[jC$$ClIrzs ~-9?8zz꜠qbhVRX$򁞳aƤcL¸O/ }m 1 q'q溰AOLAOoyHSl ~^۪EF@Fm`>βn rFXh\Q@'y؇bur.SF2 c{sxeoA17]s`M{lmɵٕ pCp frx#0"Z#z5ZMƱ)Iz5N IkOOpu8MQOL*}hjsg9J׳=nq\@$^99'06$.-?\X [W/2"?ׇspu^8xEKb\rB:⡐٨@I68$ $q l"NU*}2vaxNĂrLo*x7%w_9<NdǾ6}Od)+k&f}=[7O^8כ\t fNN&S!f q&ۜ,*h?CQ SA,H\.+vM|86SٜK|q&~|:٠>y?^q&.É}(6"F]5T y0]'׏G93$r&*"q~}T>ʼ J"{и L9Fxf.wxyو)1nل4( V>9l==ua:6zj[k8hGxhfylW68IG_L- _OīrE^sFg[ HG ȫٌ+r8TsY<c+ q!ܦ* ~bru~Rp$v=s|6hCa\|oXȱ<G3Wԯw]Qy% a\rxp :%W#} I!9wJ=cZ>9'!DŽd+ E=}+m ',pE~3&~  OO،tCI}g hAtD\9"s1/f.Zl,Oh{|͘RXBG}xlVH8de_cj9wΕsKyy?1^KzzyriŮAP<1j zqLzj)Iz)^2vk8xM}of3y8miU`+6+Ti?E:9٧#/y30Os.r+٠y-A|[pUzi+cCyG6_d`!.?%'Sc2K?ѕyYn6(Juy= OB¹OF@@J\#\"8A?Ԓ<}|S`x#hu,p){[OOpIh}f9}SvmRUmV(}8'2<1IIד'mPU$aY \9~?/i>7!1ٌ1͍♭XdJA%j0Ff _I5-r|\\ϑXMsϲuK1R[׍_ q$q+/NenAF׳̫rǁ⡒C[Cё 9J.:K.6Nw)6w-1lc+l; l{`+tmR} !|CI0ӅFa08= Գl9clGc8ě(qhH6)gE3XY nPWXt \Fl4.=![ .ߒk$.\rǾtq:>Łr\k \/2C\oȖr|Fv4d5Mk>~"/4Ӫ XόMj؞ uwbkZ >}ЄǙz' >c6apy3 [u+dK\z⹞KZi+'nSpW`\2h4ga {կ:bu̻kؤ#.nF٬n\O!5ENx\[n8CH8HH`Ó{^& yQ%06^$?bjZțz6m9= R$Cpse; lk`r+OxJG>ÐdhF%pWqp S>o͸o 0 Jӱ|q&|:٠7.~r-)Ǔ>IOgod3&}7zF2d9&lmQ^+6D{>>Ŧ z$6Sn8C|p^SKd\Vu?zra6?OXd+WǕxRlϩ2&>COCW-+JA9CF׍ \93P&B` :l fvq@؊KvGP\o6N$Lun3|uC2%B)yX$CgjjU? uM <R7L_OWq#n՞+p*cŽu7Cy@PT>|!?%c"|1&}8\pQIl=|I/[lo3h^1:6Aa@$xHR,MNrLEC/E~[5VA2oJݖd腇~fnҁh/G(/._x+2}2~C08{jtnI}~ymK=r_d&녓mv  荱[mBIӇ 'b2  >Ȥ-o>Մe¸O" 4'P$ GX/\љv8:KxrWxk=u9PB.ľ)H^8C0XrĻ>z0%/UA"!x㚩 ̀O1>f.&ǁ‹FhϜ@K3^6 &YE6e9+oKXBI=ƅxGcZEh)PL8R6eAVyVN8>Ij6_7uP!.†lj"ec,RNs:'98esōXumV@Pph"ܟl̋1Y"b1GɆ zrcu 6>jCaɧM6b6DL C/6>q2sAD|Na˛*Wኼ2WS/Xp2yNA;h? X!p"4 .Ӛ^L|+25-g >AGŅ)i9V6ҕ.VzNә R,[ [Q>02_08}8gԋ3#NL2À& sVኼ{oI88c\qlı?3MN,Fky&͓2v-gέmBUa>>|c8iqpq-3~/)/bBsی4?86lc\^b]'7CZGr9Su?RD~1G}<)*nq$\E% 0{;Gq^^\^Z0vxGşG%8wHB\iyl^j^oXc67眈X\/BfV@PpDL>i`@bO6 C҅ zţ eȑԈX<cAI}m%ǩbyfaȫh+lRr ͡S>[mzx@1S@3&'qp<>y񰼄g0g aZһ^| ܜH)oٍwzOg}t k~Q7066(30 WH\:YŦn{jQ[E"x]2YWG}[ -ñfqɡQ>$hIk; lo`z;zzm`sc69- LIЋsҕ6\ُ|gx"Nx毼d.3+b#"^a+ơי-MP0^4a_aqZK8_˳-RuS.`qmcM8a 8DNV;$ qImKas6f@~n \LD<BfuJlRr#t|Yeo8|DN?=S77g%=rMh <3:I{@0\,8 D,9)^r cxZ~/{\$J3e Z+2=/F{bH :EbP]R= p)bR?wrFB.qql U\#W>a!y CqD`z=d¸Tz@R,z?`?{u^Wuǡvua뛳 8$/7':/ۦ*4 n 뉱\IxlR0mKkݠfgrn'wڞ];[9]={j=yraoZ^h` ?'fQ%C3s\ڪ 6qBW))& mʣWLrf+&b܇qO^rAr̀GlR͹z,xSUUv/jwk_SmNmW xUTmV4 fS% N 9 (';s&||6qpPu}2~e+Ǩ R$:6k9z}ʯ) \pIb'[Q7*6w0Wg>|odSq'qA.lzf &%^ rՋ?t1C1m!2Hzayp kuoOZ4)mǐ'!;ÏZ@mЯȂ>'pN&'Sge/9<}O\o̦Ȉ+OĉB&+]i#+b.lPy  7 :=әf$FkMI2Qk@&.ǠO0}kxҢ }&GY2gF66U!W]E\Soy-\YI~6ۈCfM~2lj; J[wzewI8>Bv_1;x m]h?`KOxKri@: 'Fٖ7Ŭ26C <1T\cQ6q`)#>We66o%1hjlJrE$zÍ?*.i3tA ;ҍJgogbAùf܆X+߃en jIr4DW ]F Yb_d+h쎄d2bPf{{?wfga\!hh@pr>D'OL>INvt>iC'Mr1@\Ħ8Kqx&S(Ob?jȋ> (l˛*O6:(_pAT]7Pni L&bx>>+ <i xbΡn 6[\e8(yWoOpq1vּKx&z/%3f+pFz)pU{wMmW`\wC?ɏ<5*Z<*6+\gN1yRD2ҹ>Q|&EXlKg㲟l 9WxڜK9. r8qE|HΞ0nJyQd;ӝCP/_E͒ O?/% ʐuAg~p݃0 U\1z9&&m|lz1W$CplIz6|wF. Kdz߇&w`XR(Vb+q>)24Sy9qOb|q'F8S.@#w}!9._dUO}<#߾-&aa9Ŷxq# <>U':—Cٯj =G!ul=s%^-ŕmbuW8/ɕjZA5zo0FƸbQ#y 6U3 Cׁ!Kz)}h_~U+Qn GeОκ]mC@ eLɋ&}*6Oad6( c,ɖ>(m\>˶`"FjWx >DIV98:+[9UxbI'@)/ }zZه-P)&^#6r. :?/SYsG7!Wi'? ɖq}i%b4Ǝf)J́8OMrSwo>n8Ln5%~ħNCnގćۆm8}S|8E 1/f ။ .5dc!<<K~\ Gg@WQ{y%YK:2fJƁQPE$a_ 9sx/Yto|bC'sa|-to}`4ɮIy&yXOP7D.,C݁;O7zɷ'~`mr3/nYزD_Ù_if'L:CAr8kLĶ0*\12_~e˙00R \\\3r$ͱ}W|Xm=(]?Feq|r݈Ι -c PV>0嘅\>nz&^%˫⨸r,11! q7_ogZX˥=?3'Ofo\ƹu`%& 81QdSMPOnEAޘa*|[EF1|/HxO$?_^XI i30LjI.<9ţ&kIUHsolF<~m,Zd(ASCO.R.lu.ل7C\)6]HD|_v}AzPAHseAH\3i!M~솞+C _p̗K\{?3ޅ1rs ru9IW\/;df@Ot /'@Υm'}09s"N% x-NAXp"/ah8Ixb :z c=&b1^.C S)a:6pÿċAg縊Źgs%AX=.t`r)_Uc/l,x(1v uo}wC~?ݔ.ec񿡽)_6X~|`u!}7NO=6ZF+67aD'j ỳ%&Ϲ؈&by"EzE\Ul3_s\>6Ŷ%ȫ\rM\,jST I3NY83wrLlyP)ɁfndΉ h%^$i>/ On˸ [/ZCX,(q2C6>o|{O=ͦ ~ pKa~3{XCl%zsXmB5Cs8b2lE0)sDxt)0S;G'޹ۉm7B`ؿ[mNNNx`/#Zl/'&_[/s6+\ɐRPFl8E)/mW \>qЙЇyz-DW,}*6c3_s\^K)m%"q9),ya '*}u#Xj[Ǣ1Qz\e¸p%\7_g)3^gxg>^8 oV,RCq0UFy. /}O eb8勌b{;u!(jD2?1V{^yR+Vb3^%sMLleS^"q9O\Vf t}#. XryP;'@|^-u`Gğ1]>7nMj|`'繝Ξ'6xr2{'B9d+6+\`M >TN6VLOkgzm+pq=8Ƹqa\INR&=T ߂狸?O g ?PĻ?Am,cŵW^{z ?KC=~SkF`M**6+/zMnx$!gub":ӽE pi"e3`*xK@1SIx[84%M_I ᙯOqyn3;9:6rh`!&=%]aIYcq'q@,|=l̀/H\Agqشyyl/ʋsU+ńZ&f@^^7x0~7":˥(q{Q{%VRXT4QᑴPimL6鋗,rs't&V$!QmЇm|NM-\@Z<D;=gnqXgtW-sI1B׍t\~]`6qCfC1qYrS%?8+y==˓N8k`buEyyg (0կ\_-PFүno_IܧNmBmIq"r"NOPI DgG٠k7Cq3~&SG]jMƕC $LG`69>*?4t]G"'}O u`I .Ƹ8ydA4V:x'3EY(R(۲e˶h[4i;A M)(A#A6RpI(ڦj"O%KLQ%|;sH⧭%3!m0F6CLh.TB5,G85a歱Z>ǹ\A ƒ,ĪÐ6( !GlKxr: }o2PqEwmWH(ri5&d`ȃ<DtOߠ6;@犾;"~VFuC8j.FMyWl݆c#XoUcyl&Zl9 6c؀#`0׹=pz`>Li>|q)]OaXȎ"VX/Xü=Bv<]ATsq{H lUc\dpmaM05u1wݡHmP.l\?y;"of/O %1l*?f\1y8E=.=D?1uHd2n|;6u7u  D7^…,}?wG& ec.mX^?r x(6/ʦ8)We޼N|a\qi#@=BY\B~z~Cɩ8U>I gBQh#$mdp #!orlyﴰ Y 3 E6|V6~w[g Y:1_x{`>L k.!t(B8 ;bJ=i -XC\9_,$>mU^ O! 9;>bS’HY< 3sTPdRvJ]/q7fxO>U&@\yʫOټms:"/ս-r_Nj \$# SeIa=|yNY#}3~wmޤ:sx@B>FE&<|rau|ABQ_å?l80/Α'&6׋{ؒI IԛJ2yLUHyyg Oebߨ+z>p֝ub˫p~Lb+\ ݉=a6sB\0Iv A)POt(J%@b>QAE!?\6l'osX`|ye`Bo/;x]XCQ>{ qtYBE8xա(>-W,Ķ-&62tm@}A'^6\tzE?ZTz1UL6cQMC}7yHEw h"qye,Gj-؜QM 968'BWq1|AXdS\ƿHl|Aw&]l8HLߎqЌȹwzsx e7.߀7.CQ+xϿ7r~ʦ<>,\QC( 1csb r{S xoccy1~}*N+ VN625WTamCm8/F }]\I_c`!Y0-:x cXe}JڠO m1 021>;?Dž:!37m0D>?r7s9( Nƭc"ϓ+s{p}/ߵxo/qUwMK/0"Hwh-6d8ܡ~w.6&XɩO[Wc>1hdʈ\se䬸J gMk?McR}e()9KSIm 0Ǣ={.>ҁ@-qn|<8Q~qUx\l'ڸlC$^vT\͘jcmS FFXd0:y~GƆmR7w0nԏ1<>|έ|ns{|n^_Kp?קµ -N|18pb_B_ X'0_x{`>Lw˯[\n߰-w+xqBJ?njrJ!(s7sMx+#6. VeYqǫn+?l({0T}̗Nt<2Q`0Ě:o > !lC\=[g.E۠7r`^\ċ#P8Q 46M&;75dg{f}, M9i+{g//Ao8{X,V{~` cuB1x[X #qou; ޹W i8klj$xkj Բ)pi Ax <YmE1VG?`K^ x`(u$>*/jqa:PR/[WcUrjlZYvrWQ`>&^6So6j'/a~ȃ`'Z?$00e`BWa=rGn!a~KAwao>#}؀ &!Aྲ/bDwXw.'e$vట .ɸNCJ^qa#Am"J^ bQ_9؊]z6(RG{uXACF.:6[f|7@W^eCy<̖).qࢻ#6q2N%y›~ 86pI;x-1AG nm̺11<#4ca\冀}/^9x O=~aoW9Xе +;XI9 ``wWlnbm+,;9 .Wy[[M> n^\eCPC}zq솔w8;^Hݹz6(Sth8? sČ!Uy@ mp1pm»-6 HHQWtOY.i>>` ؞ۣoS|ϵb0 Jm_<_xA,4sN0acZ~P/b0޹?}>v`yK᥽[.'.vڀy`bㄿK/"rQbvFap|J|[ǗÀ$:\4\Nqb}k%G9ᢻי8붪>]܈7tr Ӕ\`~4@O!lC\=['[x6ͷ]xr/6hm~ %@.Ǻ9| tV'͕3wxY>}eϼ߿ʻxwz+{`eXxg`w Z aÀr6@PIm` moĸºt*}ؚ@cTws{qb߽z̓z9^eB` 3Qyx7{g+xGU̿=|Cܸ.F:6? 6<(&s}sSgn1Fz˻|z_~p=r Z(zӯ[AkH6aoyF+%!mȹz`>L1$Շ˙-͒saGY޺zcw=ŽWw^ݓ]/VwW,\xyл<CuMH;%N\fW\p2ed^9Wǘl`PV)O(yŸJ\ot+(:K\x|5.qz{\m^#kR[6i mr)?e;Ka\n䧮3vwnc0G'¼HOYϼ0oƇ YXظjNYQp}l@>*g<;g߿{| WvB_5ZcZl#x >jCg &p1l$KU'ʪk(e{m'Dx[J(_\tH[ژ~`Q~3.Z,6T3~iGxg">3=e'{a8ELa/:?wPأ>= `,5`젟}9ٮoȇ3gF@qɛ^s=0&xbr^@lCĂ:.ESkN_jս_xm+8\zˇ@IDATr!gd}l*>6`o)rK:rT/*K{u`c6軇W6YnR|ԥ v\}:˷n~q\29# cFRA.Usyw-c27WiUZ[W⫼?@/#k=V7c<[5\l 2W !7hU6qlny _މ&95?g )|4_?ιc  Mlb>caO|g\?t׉ ]g_y^xik]oIc>征Ew !AY-WqvK狩a06mSp]~pY7^?{ʏ0ںY(q x2' )/\ p]2Nn6O?tQqEf^BRIl0Oa> } Wҗd }hu2o?'Gc~{;}bU*>i_zj16?ko3& )8·<]PNul˅ϐ:8?|Zs{e{u{$q Zh N]IJ+g!a o2Xöxk< \SwC O|zbsyWl6k= IN,vqGN,H_ikό\TD" q/p AqBþ?@ ]}bH>նA.NRAr95ƾl7ۆls#e?ʦ_|B6 /SWՃα!~Ӥ: }#2whq3aP]*c~XR \C1+ONm=zc/Ͼra5ƫ{Ȁ,0p~l*c}!Pt3V%R6K7{Կujw.~>l.[V&,w=X//&p6X C딣VL\큞˗J ֶSWv }x_[}>X]anPS/X~.ڲ  /(bg!N6Jّ_ 0]#Cʫ#|'/UQ␤(\"fyjI%Ǚa@C*XWwc)F5 UH:Ox z_Pp$^(t璿un!_ۧAX+#1"/]_Y|-7ʕ;@څͭcGpLr09q6̋[|W߃F*+W.x=)l8$0_&g= _M?p?Sˏً?68udVf=|l-N~7"apt6vJDRs6/UI3g#[|l 'ɸC0 P[K^q쐞:PqSCԉK7~ԏDC)ݱ3wm,N1+O-G?q-v]DŽX9qK, Ol,AibWyAcH5Gmon>rlj~`x7?_xiً ד/խ-=0O&'~ݏv }k+Uذc(`s{{q=6z>ѻ؞PsIczxy) {G.IbRቫhl7J\r5%2DAGs+q e؀nm  Ϗy[\KuSծ7?`n.u['SW/_[%?V1|R5m^8 8< e=xp '[1wx|Uy1|.yYݧ7?|u ~iowWc V !ٓy3ow=N_8}XoCΙӋӧr!R L/|q&i/\nhb3}<0 ]хxr8QqQXX_N ^oe^K/_{6|:uUe5vp >6m2]cx k|,cM֤ )s?9 _>"8FvaxG| P\P`cmw<ϑ=d/pi\wbK;mk/O L@Ǧg\!F~|~] ϛ - d!eXk9p=0&z,$sYƉ0zEFapѝ<jn!'Nyy;l`:b3{ ,:wz^^_;l+/}KVuۀ n{?6bu*Ylz:s, [3^Iy\h(\J|ϴaЈi>~NC_͗,>r#qZM7[:X SW^]Hḏb@HVhUr9}fXz`>LvY3]͟2XX(0._w|9* Fcb eGjcr@Yؙ'@\cK9w\y/<=Ι [{,G-q719[opř/&N:sꩯ&1T+Y#WvAl,Gl9Uzq@g3pT2P]rtЗc&GlMؤ۠כ}P 9>~vU]7}v~>CX(;zPh˱|z3Wx(} >yw[aqO[+!66H'ja'.|خ*T!}b\o;(Ip{O~ _}}ᅭ/N_zU?ʩKWkq߇~(7׸.JYb^DzlqP ٤ H} $EEU1fo]U>X̛}5M|ſ7wm߇5H߳m)CH^r=0&~zXY NHcs=}ŵ6gw!6r/K.F/˫pzo9#0c+G;N_o<w&K;~ᅫO~WzuK^̗7+E6{.W It;< dOcdoc={U6'X6G1j@=/ ;epɈ| q&rXb1}ʿǧf}_ pS|O ;on=C^O'^uc_r,ē7CsX\0la0/~j^q) .pI[zmaxGȑ>` zn!q30I0\._'_/˯]^k٫3,ss_cc1ƒJ/a93w̥.W̽P/M\L%l$PY.ΜX<;wc1}m5]wu xכzw&}VaS~~F?ϧ/}?}8$i2fWh˱||q z_5{rq > n^pD}r?K -&U#]< ߉H 8 =Cy.7S/߲rx-ޱ_drm+닗Ӛ3Dp4vq=l@KKOo}|2i\*7J,[p`BBy]oᑰ |D@z> 5^X\ qwl$6g8%BpQ^6\tgUlG\#brā|m0聿~:oƋ*έ/\_S\/_B P<ǻ1f'\t66ng~R #lGe.ęl>O/>ӿ=zͽ{Z,%+x=}~w9?sn;' Q3;$~vvco_K?~o7-2@)^~".h^Dч}~cE4\(#ġRPPq6O la#N#gWO7䋅sd.Sǩ%b3@s~oge54!8z]ͧ=\QxSdfc9L08wFx)rq޹eA;sg}q/^>my]Əzobofbg!CpCg]o_{vr1u60¶wmO/:`}qk r5Θ<(6w,nG_# ޻= !WpH`ڱۦ_wC_CÚtݹ/p\oymon<'_|)C,Wc p+rl=0&t}<ѕ ]/mA0lx΅s;<YqU -| }J^k%.g\U<`_.#Ni h.{sw}mݷ}|aL}\^9+_~ѽN%}nc2x5}y;#xu/@26Ν;Zelv/ܤ߹5~L!ιJ }xO_절w<}׫cLyJ>eRYq |?󸻈&Ӝ\.#Bfg}Dƅ;3^_mH8qŸ!eLv!Am<= 75&gX}6kpw[g瞿yr{OŃ "7 =:vrsm"ʹ_/Tnc8ΕX\۬j\a=1spS2&'W҅'^܊Wo7Z0O/^5XlIw>t=xv+a|l^8/ ]wءEom.~h|o=ůo2QTAb&CH&2f}s. s9?Q>0yI: $B' 3\g8/P-Yԓ2ɞc>TuZOIg t8pri6V9l . eqÀ N\ȫ;Y7PaE%s8W0fUc̹L[-Ȝ+w0&]. 6yGOg.l/.G>=?y ?2aeP`\< rf/8K7.w0ys̭l08"u]OlGO6!劫>$@]r&uR[~r+c\68U%:5k.{ .\՟H%;Fwjbŗ ֵվvpxWgu; d7[m_ocͶ ؟sR2,Y6~8^*rV?ױd[ ]oƢOh>Vm`xͭz lЋc\^<= 4Og¥˥zrympH .pI Ñ&6뮏 O9Ew{\cA`u/cޑ2lFcJ}LX.[-.Xy鍵?_;| !x `b.f~Ovޅ%1"C<08iM6G7 4o榋s?±͍/3yQF<@Grq !cSl.wq O H?vLH0 5=ĘdW;t8YxԻc< tjb3&+;vBĽri ]Yٵ_e|vrgs+~p>)C,cZoOp7@,ʕڹ_ }υsYH}c`Ap0 s%^+q+,{p9/~/*Wl/C X_yaǿ$ɮ1enV?Lv4qᝯX,pQcRvQfujgڒ|hզkf&/yk$ \xF|*.6pi2\@|E 7pL8Wrz#/c_s}a}V\X$U|lplIprz}x{_޾_x<\2,)C,VvmE\*uOx0tR8_Q|m|`G@K.Ó66OnKW2_DGpS6*6\j1ƹ\@̓2oLfMIY"0C6=q$ ՕsgO|, "`ع{E ^7𷿵lJR2z9e(jrы& ,}_' gf|֜<%sK; ݽitLFlӍMߵ)-?l0(m 8'o<뵭E3{9ӇL{R:*'I='b>%*Ǟ36h>xݧkk27Om>z q 楝>hx~i0{՗v>;v>hy+l: 1[gkɸ e@,Oԫ>b<|t/`tFf[Hy Xbx;jf a`/WڒdCpԐ";j`u8:}`Z.߆#pt%<@ }b @\sxyTGp趽kI߈^:CLKkvl l$B⡍"˴pI#p 0ӵe` *$Cm̘U…'%^b[_F݁8r/z ~W_O!cbns1ѹ{(t͙!QJI`0 Xk+y/E]]l];?xĦt eO]A$AsS)Qz?c8\orp:%?iplVQfz`>LXD9c䔎 ͻmRa@|vr!c`>I3(\Cy56TXg.MNqY.8DLۻPP;#ywɡ\ؐc|ҝil,uTw3/.-l#c^o; O^PW?+/m,>Ɯ0Gk˖r!rf#Uj=;j7 \Y?ćP<ۥ#'r.AAl@H$B _qf܆XUC͘8iK.`!lGj^rF8=..q2=+]C7cm$1.ߜ1b/8|.ӖqmxoOr Bu4x.Cɍ=?G >*x{0\O/y,8et~$eXRYD^86 cbzm|(mN`V< 3jL1or Ģ iɭ;PR/J2бPI}*pUxtKam']sum !X j%| |pqEL}|5<&u%zpI u1f>>q# pi o<+VrE|t'b6כC\+ z.\X*|ب\>.˴U~,c2d~hL@fS_Enp_yJԱr|O^b{ۼl}]p΅`|3tXBV I& 2 W2QEnRpjD#f`BǏi_onˆKb` & qIGW.M Q-L6ϋ6% =\q.'.5Q͗ =tǤwƥ1!ubqmӈyGs7N/e֛36c\}0\Tw˸ç>c>b Qܧt|Rӷ~dk}rn㪦\>KD*uI6S˱|PrNQ2e|YO[dQu~^0Y*!M?CAN.`@k|$0ս6yˤr1{= "Nds }B=l륫m˯Dž\9\sUwRڼ|r  pq CuWOUj2? |C]ob *r͉xjSk*>s9@O= |T[cC%u,t $.*o] swprnQ oTcS7/&J\ū>][yWyUԊ\D.lhamKwB s9:Fg=o|yEN:PHu%AH^r WWB؈ƜeW\/6ث!b}|u;SDŽiaq$wƗV7(bU|2/rKZ"+c>U5s5ж %ID1L֊a$mN;yͷ09ķE[ F('8\$^\tzڠ6IY6#>xc:K3>].١Lpѝuݤeam\5=9ɾ+Ac3ճ&|M92W7Fl F0cA4kkxqZY!UD2atY8ƌr`Xl͉xj-h{Xo ƭr\!6K[HmYCZn9{x;ڧ%[!@>by 3vFriC9̅xo'JC1Ƶjlu HXou@}}6|U>1WŁ8>t[ @xʕ"ߔ}g.**TُۜUjQ|,|=z>Q>Ţ |{g]F76TX.N% $d)yQI}ڔ\C%̌.E}26nkjlmP:z QDŽ!GXYr|%?K(q;=kG̫z~e\Pa|a)Ad^#@kDF=c @9b6c[~nkb3=z`>Lz> \HKMy×dȓGr8%>J^"EYx{\U|>y-mHu.{2uL4995'z<׈?#JczL=0&t<!I, k#SۨrE2XHXkeZ7/'Jjd=qe5ĕ+.?sMp|6lFjP1- @9 W~>428:q ޶YA:6ek.X/\}6@5=C% 4Eorj>=s|) S^Hwɟ6TXwqJڠO bu8VIձ(fZOʹk ('vl| IG";ugqbYqɯs(Q+.@+(:߸κ#_ݡ'"m26,SD5_&x|L0tEwۼ&[9 $Bj4xƤf6ozun3|\L>toSJ:YAk=RAE4٘4LtNj9^6x Ƿ\)1%kz`>Lx{O֜(HEwڪalc%!X,c@ń*k rM\t -]P!76=n01C_*q = 7u >S\Ș=.ulOF&Dl|IUrόr== pt7077P#0b!sF|;3| rubq >_#S>\<3^lŶ?"P,^)ЏUg^B/,+\8V(LQ\cZoyn%'f:n.9zC0|rNs1a#ƇB:\cbǢ\6Cq Dn7.̸46TXw̘PrY,m |Ɇa:6> (,QOo6^2ؗ}LlDŽ>;HYoPwb)mC\YRI*P/^6\8d#sYMz/EwoHrU p>!APn3"/)ya8 kH^67 !7As9wdF\U1Cf2KǹHCM6o'zT {̯ *L.2f6o2|t(g^?& \i1<% d+p1fs$9E{`>Lx]wHʢ3\)6=#OO1b,atkb;WƄ SۄxŒl7X:Mx*f]:\6ߣTBPaF|(: Dki0G߇\+p,W ߏ>n.9ѷmͷc09Gr0)R !13d30 ס6dLe%ڸ8tm Wql!qqeZ4 c1Gj󡲱|Vl|q*PeۦqX6my -*zCpo ,dܮr>(M:HCM6o[UEHszoWreL.%[ sf4ݒG|n P.=|;yzE& RχP!^(cIAxr\8c6q ,ftkb.`Ŧki-"NhLʲ9W\N |}.z c^ƘxUxf26Qx_X?s9UgdMpkaD5cV;~(p.SBUy A=rl=0&t=[z;':Xa=[ XOY,1Bn@)˱a 1ĉ= ס6q9ǁ{fo"̭b}`==na'xfcx]<9 gK.rq,\yF^MlƤ3| %ni)8 x.˜ nSȦR]Pf3Ezj;:ov6Vѷ R3ߏ~?ccyYlKG!ΟOKv .=*'b12lXMlճA:ګ8u;X ~Gds9r4^+DcI#ϣf>¹؜sx6z[rOpdlq|#//aJƬm 6e]P mYzmi.eVr 73qPPVֈk^5:4}~{`>Lr{O瑓F`A!yO(\XrR\xxqgh LcM\XyA;yсzU4(\pu@| 1Lĸpp5qW%W6jm`lcpQm'1qas0!^qAlp g.]+;1 ;_dlls.:8^!R/.q>%[6_9uNIok7.+J/\C{f~0Uun%yBI}kC<(ǞpI1°#jDpIyg&S3f1~(~XwPOԘ906Mp>@|6ȆKbu})!XT%cBHb'op1nP|2<gCRs@324WcOF )Cr,Ðx +q\T)\930c56hO+3].oϘM\G_,'eəxn^F\txe?Ios@s =r!Vra~\8$Gɘ.ņp;ȅxU|̗]n0!,K]3똏]CfR.zF(6~%WZr-Ȝ<ߎ)Pl౐!Å q|sFp-$c*3ü"~/OqKr01#<SaN~0&2cҌ?kcXsq"P/[e-Xak jwd)6N~$Ϙ$❯*/p!x1_#j=yȭ"vs,ڸF8ܵ8##'" qt|+_ηc0㧼CwKVC%< -WV2],#/8GbD1eC<*M:6O*پoOޔ~w8G1 6AȱC ʲaB Db1Qm =l5J\e˶%UʁUæ=c)i.WqݒmXOC&\Ӕa̶qE6jֻܷ|;:rKױ%z Jrԍ3x {X,\;9mWUQD^ qƸQ+3'O*:cf@JleE-r`!Õs“zm N6̷1*b>28E<tdK([e?:Rܜ5 Ip%F\>gWij%ϳѼKm$<'lW\=<1\G0O~3:Tgy[i7ZO~j[ yq˘ mr_Xx;40mԁ 0pr9^0Jgpzx2e\&\tI[>m\6bm)yUC3\Z' Uҿ/hՃo*WsG5.ٷ]\1CsE8tg< pʘ ݹ6e~t^u/0@jÅ⿖-̧֕xb)tb.*)C07<' %[oc)zT0PL/,/M,>{`>LNc4!ӡeu%q-2L#_VC }s .>J^poXBF.ڧqUx'讼l) Eۿ`_u㯠|]-'BR}U1ǐ2C]I}. ֥Ci!L wrN&pIģ_Zzx%Y"kJJ?M^y B6.oj"om=TZ{+ğד H%~l 72&C\[ko|m@cO0}= Sb5f8qnlrE?vm8E;qgr/\n-ƿcs͡6{f*!tl+myq`)kD8O<)PGOu lm_|4ز:;oy$b"h%!E@JJrX)T~\#[-ۑ$XYIȉ])GIIv,ʦ H@=~=={M4@}^{o}k}{^t@3FT&f\oLo2Cy=,Kj]Npf,F:e1%$./ȑ.eKxoOk&ҏhzIU_9D*<,*nk# OI'.\o2Jy%<%3_h!ٜ ~ >CqxCM73WG'3Fq=!'cCdK,xDڑǸO9Wt (/~^@HlPpOx3nKx_iz2'a ~' zû=l%}أ̕703٦"A[o-]_p^ϝ~PA_z9'us^ w_+El:e^K)$0CbUl0$((SѹO|eRg?T06py3Ѕ lH&JKOUǡZ(ᬿO;g5?P>/[sF\63=/a>xCĂ6ҵl c*H8uC\y37g'|".[*pSf; !xT.. |}'cLE7bY]x‡0.m-̿L/Gyj|ׇ_A#hS츾mPM*NĦ?j\ond+*[UYpAt.K\$[x 4VY;d"C6Q8R&Wfw>8-@_3Rl0/9'k?`1/Fñ&C"/d(y+tM>d)Q'_vƃc>ёҗt\98Ń`r=YF^HEp۵OOxƱ^pNҹ+ aɅx`mpu.8 \@!beȪqP }“J+m!%~#Z`ٳWIIͥ^]Oxu yai&˅Y1 els2vz#XxF`02*]F~s'^켼ysM2iӝcʆF즭Wي P?Ε73'䂫Hd6b3/jC\%<~u'i(*\|xbUOqNί|{*<:?}*bSW^Z|QU\_'|%ʈܿ\wp{[\ yYȣmq9oq}f@ɲM d.Gcxqdi}+<[}Hn\y,)ȋk}V/kYtʉ9_w_Oli'tmzR: }|3):v8$򜕗-r>wXoF9Yo#̩ˇjr[uF93fK91Q0+>`S~5O=of8h*..3WopBŏy^ݞ"?ן=x}cmz]9b\.fcm|N|]8҅ } Pr;q['&>Kn#yM2]eq*xu\O!Gmכ̣mfD[r>C}EF`0yC.yޜZ*FOK^:AU!cɿO[9E^%Jk:.SW-6#zԿku/&vXa?n|Djm]׸Zrznm$.86C.q.Ks."8Ǹ [˚Yt=K+vE+8h>hԸY5Lh3<UB +8`dz p'pQ-J^1xK&c8$56LJOdi20A0,Ne+p'C< QC,^0Dy,}t \B\6^bCDDžOoZ{kq1X\cw9H9ࠚ:8[=lJ8 ^. "LZ17o $At\vd87jAb%N(U ! URslCOq,sKm.?TxX{$ZYOM֓9)@IlL"+l)'8^g[zkJrB8'*`C}eF`:9Wuw6s#`JBZqɇɎM8(U4V ]%>/О~}&R#9D$ȅ7>7w/|_{7"&5t`36*M64wNO";m뻋K=\L58DjW^p8 .* N6(٣dC$f~3>kbL#VoTf> cxq,'f\Or| ǣҧ<UΉ~6.aN6+3;,`W&mď!K/nXM[ Ul 9[=›So ,  .lKbcm% .pHF%q\O'k`~ _w~~}Kşk Ǿ DŽqONj1ƓZC!_WE ctݲ_o}W26{mc[" a0;m>On华 a766X<>K݄v#Un*ޢ>Թ7bN,-Vf Le(V^/86:c6ǀ>eo/8n ̅ǰ1=+E v9!:;|Kos>I lU`-lqmW`2WKc 6@tyl}rDr'*pN2!ɥ2 (3eoЖEȹQmR~\x'7pnc~w}ߎ?MO2tzQckոsB)/,rVv\c_sـ8 ~'Li @3p}-<}RnjѬOulL\lMjec$Ke+b[,W8mQI.ˣe.sj9Qr7S3+;`Oy?Ësm9_~qec׷FF aj(o-m0c&a؀,Ulu`.tk,FN̕٦E%-ҧ͕l@l5^x&~<'XWk}v_S9lNyNt 8 U?ub3`YGTv틳7]\MKLj{.ℍ(} |?J$"zBzO>yxO^sOrF`0(^km?tk읁խn~3g@+_Ô kyV60~o^-q՛rXyC>vr迸<7֜1*W/~\鳕zd(ZD':s3)a5x\63>>fdf{~{,}g.n>GW6_Yۼ4CPmol=p2Es9~xNnp3zR<][y %~\B֛}xL*x^|6,&m8{_:qw4ݪǗԬÝb_kyi<}%cU3g!OwNp !]S-.eAdoqYJR^y3@} ]~-UqdǁW5u$N̄Pt^C8mƭ<6"D%?NJ/Ad,7T n@mM96x~_2+0Y ,!>*.[^&(K偋A7\p۾oGo0Ǐxgo]W B g.a7hVm6fJ\-s A5H@0>Yʯ?+M[ptY _?,={ݧFu/=4>˨hnņ/9^pzu\n9I8 GHmXK. ?JqKG0dw~S!Gf@ oW9.€\ t/zeXesx!–_W ~oht񉳛_z'n;K.s[x`c_;o.UnU|*VxawW J'L}ب—Y(w}~ciN[ӳƇ4 l~}ȉ#c=_osP|KoicG<:]ty-ʈ c5L4mTRl ]esl:ڋ!ɼ>+畹|q {Flx)<}63#<ݪ%|abKԢT2Dczɧױ}P):Hշ槮y j  NG|Ay&%l#|&-wZziŭ:/.^u6ǥ~`b3G|`%l Ƥ<7sU8DTG$hOʁ1aA6(Y_J!ǁkz[+~LdFGO j`v0(;q3{;ܸxcx͎G977z[soVW8̺;cSc:wp':Lɨ!;l'WaL6\o-.ƈ˱=6Ry/L*(l~͔6辆 ~_m6X PۨYp ':)bS Z6*O6~^06 ~#O>trS6>/0,l+0 b31>r:MKMG>.& \Y _?̗[=x{^ڞkޟ&V_ho\xN';q XbaoiB>|S5橥j3ȮGxq/ǁC17\m=d *|N6hŜ eA3W'{ pFP 7gZ.<"r^Dnw_^ Z Wq^*ɰ@f!$SQvɹo^8RRv,'ڭul~ƿeg NLzwnrFrizB UZ% ];w{xDOC'^]'׶nC1ƥmUm ncC`30څ7p!#ea2eaeof^?jqDJkx݋@d Ͽ|ReYd'$ԣ-H6p6 y^|KzG8|:ƪU`h9Yx_M"j[h3|b|'}Y6ZW7][ç Xyt|?Շɟ;v!TZ20g=0aܧm# 0>.o5 `xZQ> 3|سږiCSgS^78j#d_VPĩM@m+}d+ȫc[^ĴT,[N/42pxW`PM6D~usaa11곹$%pE|(//kgq`e}68TO>fwx=L7 !fM^1@)D>4 kɲ5ʹ3Lx̏xL=7#/6p"%0[>}ls`\q{5!wHv *5 $G\x[}^5ݐǦ ed͑OcoT:4FmPh]ډˮwt|4r{m9 [{nx=qmSP)_-SZߗzt:Vױl쫸a3?Bt]Ҿ2'(~3v bYO^DRr!dy2wws_{=>Mx(b$b$^fa!oR!XǹE|ȩf:+>3GIhc{A~^77~O7?~f/fB)ۥLSKuRW!b)fu/`)مC`<6J5L˕e;.Xe@- Řc' Qh0l/> @EmnL Hv36#s|32bPY6ৰ uCyc@ޘ鿶Yڣ`bFcwl9RMv7^ #'xrO>sf-㓄R(&4¾lf`{7>Ըk0o.]+8ά5;1,;sd-rǚ_>zZ#y%z;!$!I: F.һqL6m|ӳcjfii1+fOI\xc~cxu25 qR~M&ڊ$<pLZzYXW ANCy]Nn!X-FmIo@ ަl8#qVG؂ƛ1_بO1́]Wu^>ړ<کO9v8?x43sg׆`{Bz.dQXSځ-'/'| u%n-̽|}_Ej~|Lڨhٝ y<@ gNĂkG/dW]pYؚc[s;<~{ (X]wtsEWvD\'QsQW@*?X-rf)6qlHWڜ6d|3b3Ndb TlD3l|_XBQ2K'6}~?KP#0lf8M6AFU'xxʆkd20¡jjMt1I>kann|}ۀ֗/l~yem7 ^<Ɨ.^h.>X 7 ",#.g|kLl/}70Xkf3'?ԩ6 Mx3=%_%{;NbN GF2s@ eֶm*kؤr=5.maCV106>ǹb\y !|J.r$ C y%ˑAL8S:"ؘ@%Vz "cy2^* niU\ }6.0L3xSʷ %['flnde6 t\\ !5W?ᜭqלGWN 8xs~ pgP[yq:T%c`JF&(㲛:p1@: !N,cxz =s=}uxv,4[ϮlQM5 ։vו/"ee)IG6ͨLO>!x\)&|P/[ŕQj"/:ecϱV֒\GyR-z3d$YOc{cCVa0Ù߄lar@7p8$a(ta+93qmi$VxqO$s%,}ye.I vwp)0m'^\o2^HhKSY/'%CU>AGo63\S|prӻ6O_(xQo6+ 8%1c|9Sƞc Gb{:{y[?|m5k_z_=wf߾]۽koՔO7~⏛wrq|IԳعI:iE/Rۈt|e0I7]Ba鍋gȒmV5!#Bv\j3\6/bSc1.ǾyW5,6x1.8[i%N[妵ԓogT"\){>z~7~|g(o63xE?QU7;ow(+`Ll0ݜ\0t.S1?k:ds6WWE+}1o e{qanﻮ2zqek^£Wnްr?{qI|mm~Ŀi~c6`0 ut)dx=@eΕh xM$.G,Kr[XP+Rzfmcdɚqy6 Q0 \lW>x#/(C,\P!AoB}uy!0 லIvLY1S),_6fe@IDATa0Ù潓b7j]M\P& $\TOR[q|-.OqlOE8:yBvk|&~C˹S_:/\8̩Å{rGv;6b"O˟i}CG%-Iâ@#iJt |qr8 b-{K=x#"qXׂ%J /ϤN6;tGҮ+3]S#,f d3<fG?dC_2W^fRlbByxq_ҸI=/ 9T$;AK^ RK ,oH֛߿sOU\gӠ:pt@9*bzpVety~\+oгcxh{?wnsD \5>CWĥg(Lٲ!|/L!7.:A $Yv$  ﺪoo>vw4Fil΅/wm}\mq}6]Vuȿׅ&Jߵ\)x mMДlj-F[*?'n a3e[KX_xCN5PRL k2Fg}N\ |x@|rE}LxIc>qxV$ad l +(Ci张NԘm'|+bs᣺1z,\pSG^?~旗9)Xɖm*FaRzoGE hxAlU1ࢁЖeƹLoT A$pKzx0El;&+x/ﻩ|<筢"oGlS>w{L%kx/>(|dCGCt_]\lE^B[!z"ف-r"lczBr D <`oxqFa&F@@ލ%^m(k٨4|L ؊mŁj"hG_ĉ1.}{_j޵Fr|ZTqYJ|e8(2XbHO4$+Aklq޽;.>&Q>2WڬOA+T_Y٨.&pu}6.YRL~+ǣC~#%>p:3!h2UIRo(pm\ ?PPH#0lP,.obɸT7(D3V%.‡dq1nd3,6xݧm" MHႺ#g&"^#X7/,~b6y|^$_ rF!`%>d8juD*Ɍt'"\3=BH!o<Ƹ +;.rW$"Ǧy+H~6z%%vM(Gwi1 K (*IG- 9Ka6C$, kTaM鏆-.} @6Q嬧"xlZ殥 ~aZ#륥=#sC>bbTPKSl.T:Ɔ"aJ|a+RiK~91]X|liŸUpc\oyƁSl<` P*Q$z;!|U\:ER^5W;<%[2xAz6n ǫ?u^4_Qc's 97q{%@__tHm k-,TfHh/B;k8#P@ b@S ZB @´ok8O8PIV<oK>޷>.NNBz76W-? |3^ʀ}!Xer{Ao?UD1P@-1.)xq̑WV*VF!I 3 !?\xmЁ7J5!X%e:zWp,l %6a8fd'y>+5`=~ UaZ#Ŗ~hvO׏Pu-[%:q\sf.6N8̉ZZkgw|f.~E3e ^ YDKpvJ&]kQ VS.+T奎6Mzq>,Řٲ# eiF@4৲ F.ɉcF[a" T|IzcXa1s÷l/eҵh&-<|]?Gu>>?AR(}s”xUq1d9y;I2{Fjt\ٷe%u T*܎W$z;G͋Q"eGl-0|ۤ맲vv|^Iơԥ\e䗸}6 ϶^ cs@q˲zxŭ˨?z(L?qMO&F$6~ k8hs%F.e)0lx U:\DpR8ιtг0θ }s CO6_7o]~H8pX\wc@H2\J<-H>>{.;eo2"vVOjP' ?(t'0a }r՛p86rF]߸yK5ܒtLmK=v$ΎnKI%\9IwZ_d*qe0.8HH8=V”lU)6Gz̓w}<6"Q\tPgBjUIvC{ʱ/?dycZ'\5zo.=A썬1D3uMNZ芳3W8G*fͱetJ!NECRKQ'LnO0IαʇO{Q0 fѐ{P%c¼F[ZrR?x@5ܿ;qlЁ, L +,M]1]iz>Nۥ [~._M8. _oY?Pxu#WG8&qdwSUwK*:_{| K2'syl^_n>O77y- R;4 e;]Rip\χ) dJ\&K!{xnɀ"7vl~[-n| $s GO<0k.o$0wyP߅zZ>$ ]9-P҅}-m\-=_~x}bgu0cڽ\? zV.p7CH%مs6W“r3l.J`.[#L 7.e*خtEhWмOjۼ хN ؒɒ}MmZe/k.մxK xbw䒢TP_MDڽ0wnéUR'jyk*?39/OߊkN\oDNFN=lkڂ~]/MPݨT?e\ȻHJyRkaL } [; >6C ^kN\?b6QXc˶+QprC/iWŝrdT@tH2=B f,T?Io4x += ['s#wҤ4b`lRߝ2%wE3Jm܃;Υ~,=3S#T+&Evax>ۃ<}N+PwD4a++4 B0[ %$[W\+ ~:H] e?`1M >i=qH&O7Lbj1. 2.l3`x+tɆ>ɝ\02%s͇yYB…?gXTJLN$<,T`4 D?Hx/e WО3e%S\#-=rR&orI_Hh\ 8JAJrt~ߴ޹\پ8߉ƭin#]-\jǏ9Fp 酶F c3@}GJA=`EPɦ1|7`a>`!ޞ b"V) \Ux%.I[]Jhq &^O,mx/rzM4SAuL-<:bfer|t6NLd:_ d=1rO  ƀxPM*VHX cG˹D\|t!#r\#A&oX RhV ?J@rQ>ۡ^Q~GnĐrc^Ȼ8 ^Z2k^{ d 's9CqS?K5&a+ I nnԥ zM BfFmF.G" -'oG K.q\\ᝋ&5<-т?sE!YA)8.fQ# {JssR3~Q[#/;'}ǏOKr|䓧wx-kHc^s0@zq C<t0 FOx#9 0 l6).o9Z\c޼/qٴT>O1oB4q+@9"-]{6jeCF5a0p˹\77БA0g4\fZt[\|iR!7 xN?oQت%k>:L?{C+_hvmk\DHT,Mf꽣D1.w<{Xp"bx0ʫG,bK.cNέs9N*uqmS0׼L5.a(+E.qy¯)B0 |4orM88c2P jaezM\$:FyZwqxCgnrRX 32v_giU:T g.\@~祋{q oKXWCFauc27/O{ EP•6(B`J pܹ=7w 12a0H-`r6zCB taRY @}osFpy3Ek5'JkAdC#a h>psxT=?OI (qB@,Z`dَ%B5ҢMjp"MT-w0`/crwnUk S8X@ &Ã"m{<(m̱GD%P!'|Ou$?cJPxL'z?|ViT2ͦdp0 L/?(?us՛PA}%A8Ag.ɵ{n>?jvYZ['O6{{q0" mX_b:M1gOFqѐrPt逡 <ڂ2$^ xT%ەK!lO#}08`!abhm)i΅6*Or>B¹Ϙ ڢlG+.lg%\gOً5,7g𭦵3S͑0cFM }[be%:wxPv1L1ڨcz@f\}>hTZrZ h|~Zah #0@ Q+1_jIA5|4\ʹ]sf+'ۄre |#h.F" qlQsti9H< íŭQs z`qDĠ*GƱX(%=Q.ˁxLk 3 "_}'E)qEK)xp)r~wG.>icm?-RINn00Ӎnҷ&IJ^։ޅUXP2=eJ朮Oq $\ 9ӇbV{f.<'1? D ų2Z\h|44k?zY_]iv'{NGaQmHP6>"EpO,E +푺IM۹"UFKAGA\rxWu6=1F3ڜ~᧣t>kܮaa?6cc17V'ya9]qSl(m\ݛ*N;&"(&[V^ Z9SHe[S`92叱_(c0ً +ͣyel8q,,\ J "&6H[ :dLGMrяl6ҏ)amϢRnsY= #0#0lv HN&}t=6N N. pm<=9Ff-a8Hoxj!P'XL8xT6wux5/OmVw̟mGNhLZ&7'h LXmU .b!{ld2A*0կ=0|$$3Gܹ…T7a4`l[9qԦs9ŜLpPfͅUxqy,qb\4$[(³n裾%.lA=ZbIDTCfl*nӗ:aq쵻k~ pW!9|`yt\|aٻܼ}0&P <-B~F\p>{}@NF\'&lZ$SE^wrd/{љ'nG"*>d؏})Wůai N#T95d3&/P@>8/څA5c\Ol=W\3|6ƅZ#y + }be/8ȧX[L,9=k9z :tj6Jsr o2,`0hxHp4XیctQWTģd02v9Uv::7?}WD%$wé]|gSaa72 $N|chÁ08pPmG\8\K\8 C⧭!٠L]͟iF`й'6B6yU|AO\0-.f׳gɷ -Lyji͵-KR(#=Wqq3mOGUN ;1Axs>7Z-Z;O 0L5`a2\5qBtJ!| ]eM#,P ES강Kl)aJ{D_tVuȸlI3J[o۱JSo j@jXur6kol5_;ܜ=:ܶ+_As-TxI-z5\:-.sq/@$Xۏă6E֚Nj.5gv4Kr1-cTq XJe}ˮ({{}<~hn0lݎPlճkO;OBSis$ #0@ Kޚ3`N1WsZ'eλswf@hzhS,qy M$b+2+* ѭž{3U!"2Ȩl3wG|`0l)Qق ].myyW\4[svĩ͙=-KgËxۀ>~ {İ,qi/x8:^al>j!BN MII"NOS9qCF`#0l&OmgϚ\5jnJ ͷv%!g ԫj&+%^yy\W[k xDv\Su`9zoj;<\=NXfP ŹGB"ӧN5mv_:r<̓68ql+KNj ǷSwXeVr>忲<{dzTsp?Rl{?<'KIbqBumomgO=^>Ð8BP][>ճ>T1ݧ ea&8</Θ/ys ۢ Od/h՝m3u/XɘM8Vmk*{tDzm_Tu:}D.Ĕ>lg;"N7Oڽ|M|۠iASͩ~F|%F0&*Ŋ0:#[޽i $."2X{Tqz)>}H 0La0itZ6΍\?|y)i,&abf55F.ꥣ A } ([ +sѦXVɁĤ=Sij$H#;@ewh C&fJX|eފ }Juy֛;%Ɨ m>D慓'SU#Y?cncSˣb.Vzv01T W-S()#QqD8F` IӲCY9暴LƂ af FPCHIQ[Zgqz^'eicO\9d=\ iWD'3=m2Zk6L[F/v6E %;s/筯vðXYZo޹5"t0Ee)vi'T/ʭ}(qL #0#0lv Ça%:aL]z_ uEs8c\L]\~\nx"" nPU;a')d-Km_ x Db9jX"*}KyVt][}R^cX (;M[pS⪅JYt̿3ufF`6;PaWEv.pnҽ E<ΰv}!Dĕ ='PA̺³MkH:e2@Ģ {QQD(eMhK&lS7tOlpWi6S,ͅjaFY9?y9#Ǧ\zb"d}pvO֡qf'i r{{72c 9>9~Z I:]9JN2 7#lT5uua?XvI\|TZȔ%lb $CThכǔ|IJ1IXTvemj1Swg+;|=1CD L )R8]9_iRvL [?\<8 #6(O\8ОDl\Sh#f;. vOmd;v.Ϧ[X _`Yc K24Ult]m[hM#`3--K=^>C;!UL)&û Ew2Zַ#bO{枯^a(F`p8Iq2s/ i0MPlS|q-E|=m\8Wl!6ek%s["CMEXIvZpK,ի0`Xm>oa;b(-|{^[Ď̀%y`-}Bhrؕ6!'šЇ߂I 8:~va^63 u |8@w(pHӧmx)&]O_^\7:Šv^lk`gqac"{ړl̺3eO#Ձ8N\-68qK=\Ul2)|qQ<ą6CKxNdo[,$|;D:6[ ЗX.)V;v70Z=y2LlzLRJ%GE%Ϣ_?&An{!n']_a(\63> gm*Jl\>S6 U(_8j(6蠐18T0.0lAt͸o[dc['F@ԻֳSEU|ć B\e6l~6’(˒SU l ڨ(0ꢟh;0ĕr] 9Rئ_NCޛ_v\aـ;Ap R@P+DXDb)Il,ڒ-ITeUr\.KV$RʱH؉C,$o}ӷ.o{9Yo>޽7f;v'.mp!ߍ&7>Kw߉0o 9::,:\wKjђ! D9_2!.K:!c2~J4[lɘKG t$s|DKFT 3!&il,}lDZ69X&a/VlѕJoX 6ԏBZ^4'Jʳ8U';N/o`^8~0̓ܟUmw`(^#0lY&DO\ԋΗ B5.mq#E\-l> 1:zJ4 {XIXi;>~OS6a^ 9FIw&Ŭy$^S)HxP"mlmK2e1?dtpGb="բeBA:zjҒ5u , ] 2~v;舅Þ X8[aעdt4%dA S3á]_r'BF77,]Hȩyl-N/TVɡ/o 9ڞy[ZQNnJaX{ K$.+!M~-%)a [?7ZeԩmQ 1tcnf (:6aW{oE&\ 'u,ŷq˰T~8XݮSm?潎jy\\GV.}c.ןЉO_ǫ_*n8Lwų7{a 3]׸c@#%i.)'xõJm3aBeq \$.mquofE=LiVO -[id HVVR'(7onqؖű(^ё4%v.^#[ _W?f|aV-=/s 86s "\|Ī6R]']'ϰed' H-(x*dnKPmS6b* \x2[2D"Zw۰J5ԥH“Fb!MOmK2e+udذB2Syd'/@Z\IˑЙpۑ0O ٬Yn7wv _Xɳ8i?m&&Ն^#̀Ǜp,Q[l>iC'>:y?9V5~6*^S/.~qz7$ S=6s\c@\QXiIģ, 0!p +lNSF`i@ Y-l̀rY G B!닫e)O<dRRt280pZ 2b_U/hS=hv`C)n Nf4FyHS 9_ccfБuXͥwTx__M;2~av߳75^uapչ6`+µkuZA$|َ|T<9 'ۘhfNVllM8#pq5$[,ԅ"S2jOʯ> 3mIv躭6 LDzW~V`GW+"=unpWSp))U~kwT/..!icign%3CF a0PGЉu;Ỽ!Sΰ̖Xam)_ns3d,E%<?aHO%% I!jrxQ `ȏ$+3. mm-_ua2kg;2= Xo:c8(L: -,4 Ydts[3)&ꎿTru /X=ubzx+-W^lе!_xѰE2S,I[ZY+Cr'=$?\G|ָ"Z1$GSӡ\V\S$7{LIx^7N]W\WvuہW_:{p}7o{ǹR _~NtǛ{ ן &7|*GE۶lOdr ؞Ֆ4vx}ӛ@u_Hߺn:8#0lQBğr=cjVG44vbS5 3Qc %6M%h|LRM?SvhV Aфne䝾;e(%Fk'_'#čSK ;٤LwNooQy8+xا7 ="0,6Mhm{V=+i#ɍX}W>~/HG}ޱCFa0 i,U`%]$|45bf lIܥ ߔ, y'Ƙm {;>pjASg7}8| N0Y ~o_sg}ڀ;k#0lz<X'>*y_ 9Q-lM4Y4e`R\n囶{l)R#r3&OJvfPuOI*P@C3o˪52`ԱH@]d"3v?VIum<lUPA1d$](`4)d LVoϟۋ`8tJ|_SLlnOo xwxz/Z{c~KZ7}'^bWO\(P8G5j|NX> a=~IHzx&D_1h_ӑtO}2[ BɋCYϦkokP7V=lsԮ} Qk8}{?0;jo,5Vv$,Rg#NYM c&X>z7OЙ&'r}ڷrfLө:fۗ <6`}s|Wۧ?q6oO>r7~ #pތx53{:TYƋ0/}-c?habJ[ɃP2X'!3L#^`[aΓ6gp{p WUM\Ɵ+q37HpSɊˀjIx )D#m[1sH nwBo3 7AIOM, K^n:{cq]|gӘRÍGS'SEp9N?? :hI<>gyD\:^P8OG` 77^%ױnP^W26 7k&!Z@SOG$-q ̖ht?eBd؂(2m6= Xo$m98+}4 &EGؽ͸ `2[`bo+45F} xԩFyDm膍Cawl[KȞ&`Ļ {Z y hl650 y>ʵź/GUl)i-JT vնpL pA[ ռ//7zG _8 o48mB:W.m`YM /fYhb{pN-G)=&`ZƧɟ$8tjm:M/ifXuͰ71 ;b-Ta:3n[Ng|̈ۤun{pEQ9N3S6-Hv:C'kҶm F[>CF|a0ǕlZmNl|{x%h?va:# xlΕ[@jn ⥜4$h\Ff)څ-T\Xj {]{–-ۙDp&%:(xˁi]x;}Nt.i1 Xwս81(F ь~,Q(4_cfIFly٨h٬u?7IBP8G`qt߹8#{y(S6ƁXۺ9LwC\G!c_4 |l 78F#\(b _4N) Oǡ#^.`Hn3@gD%7΂ #Eӟz[AN\nW+Fm_Y&` cN3e כ\g&:|vDZUrR}=w0f}_@I=Dz_1 #p+A59xh֝K|%CM_?{olxPi~B~x!/z=g8S %&2;,hU-)-.e[o˺he 庭,X`~a_O/PR"fgЙ'O>O&l9Rp,Cƣư/.2y9`J^*?Dm$XOF޷g;ǿ~x/pz:ؿ~pp:#*p4^e^c+/e)vlxlmUhMLL4LɊͼ77 X+6%k6;m ^lL]XL3B[3|[n%vV&/oAPoO"ܮv|ZoHģzz'6]>1C9oG`qКuӱ`/ow.z~zO}'cd3+Mj|\FF`(9Se*4G<4 CkZ<Ko,nUXv$o7 tcb~`BMz=:J}=:d%@ $^'hXNa'.01a6s\XPsQὯu5ē  A$(q@qTX4 䎥^p6]n)/f M^i1d H/cvC@XQO_ }>̕9pcGv9u L- h^*خԉSTa9*aS˗c.G#ga7E?yՐ5EXM$wѦ1L7gбYn-i3yk4g\Jޞ34Dc6dblF7CWLw|Ͽ 1ßECyB-?Bm\z4׆M,+tRB35>dnia]V}+R?|)|CVG>q4J{`Z%oZ"Ld밚Mrr=v LkeA~!uCLÖc 8afX3`Ze;n#%>\oe}4CxP둤#ǽ`icZ'ą]&lb pOH=KwCG`qQr=Ze@#( <1%ɖ٠VlL9f%_]{<[<_>}/BhKu$9 &)*  DbKɻMmf}e`\NNyXŕaqm*#b6,Dze[:/s uod;qxPxԋ[W6;Nx)hgr]٢(DDOfhՃی٦_",NUgnfO l3NVM)d.ǐh4ЫowG 76HeҞ}ЧچBݶگ阍rX~=E?~n?*m@s 9.,P\ c!hK׳OC; Wgɓ:GsX%> Y-,AOAt7P?A|o~1 rvG`1\ys!(!!€vਓڒyr 0]#|VK[qa2#y\]DĨubB) X(2m:/2"J^؛a[ԾRԉ؊~4Y-f5ڌ}2~ՉVLf!:H C]|Zcay%"+1З ^d}|ja<O2[ P >7>)Cy#F`1qΚNMLxVlْ):|>Rve ,'1h hv~߶hvŋdmkm#|-`Ӱ71nvn"&bh{;]a73+v7/ SW\6 M;3fԟjK39tw |L@~6kn&7-gpzj ^ofn/܃:%(7`MÛz\$TL5?p oO A+Ā! "0⡑ ;".WL\ܓf̀,G]ڈVwKd[,a a)# ZRX`i\G7t4GMR ]:ė>ƿ] .gp58K1nom8zo~1M/=~ .c/.ުPm. 9Ԗ77mgyaK:>VKFg(| vSBG&HU&;OoŅ8m.ZF`}7卾P6bcRj`;!tݎk&y~ų1,*^| ggV1t )~D\(ƘC++x|%y {,񥯜61 Qv{y?OGIPfa08 5"KXqq&  ENOA(#]f{N5;t G)m͑~[f,/6¸i#nkbeE!SOy{zb7҉cڶs6]?猭7yޮuc$aG ~!nԘ&?~_ZiYK~~y>S^|oxF= }aŰcDb .Vd$?(C#څ,O8ǣV,ق/٪}Ҩʖ} RZf> _2X;54RhZ Hz9Il{D/`:V[t=N `&g\S}.&uK~gH]#g]BZ۶jh?JI%q ͡]$uSdomY8oExce Zzn,/~Á˅kI5|#0l/OpQ$Sf8M)iN{rNJE=|7-ڨݲeL)z`~EHGYR̀3nL+nAtO{L+Ln30@o1Elؖo*VN3KO 77OLý߿/gjCjy픎F~cyE l61XZƈ?t_c#osZb&6^;9Gt+J5W?{坸]I[hxTr:xC=q7D^)ǃIh0ȫeNxp [Salk">!?#?0/Y&ZH<"OI`bRl3Pk &=آM& :`"w`#Rwl"tv^Kd1 9-ݖ=c 2+ 0U;v>oß%_Sō5$˵:yTȭE}37 𱿙miF63.ur3p(G۱{>6\  i1u !z&oK#X|qK \2%&1Df u?Η,b 'g)01+K\mHTAel I mF v"qz_cf}O]I< YF1_<8U$ģ-YiBMm,3%aKy౹TOŘÖ'/:hkaQ]qZ|C .Z;%2)Y3Xo_ `@YҸǁ2<+AG5,ğ.,'}e-3Yϋy:Nw[~†` xiھQDEޘ Ɯ>~/>x'nrvk&ښ91DqZ2+ F`0HI/'JjI&_xb6CIL-ـjhg$.[Ka VakxEV/\}W_%8huىZ|`2-^qq@ ~!e팦o\} 5 9fgo;8]) ,(:nbp Y;n{.rӛ#Z}ǟ>L(pF` ?Iu-O i2 "Pr(4h4Cw #ﶧʉ[l09ozx8kn߰EmLJ P^qYm ~ R* |+.BicumA>//^_=pk,wc_|vߩ{Ol2FّD H,ʢts$`wsKrI7(qў_q6[kNoހkDC}~=v._IINDKLӚf'BmN  Ӓ'9gs@wn&%CJV[@IDATn 'LjtJ–a ulSqoӰt7ݖ|'[aosj!hɄ1K{'}gzS4L㧫;OU._S06}D.tUk"jd8]D-ލӔMј~ET{,K7]|MoOZ?ppuWn->itF͗? FSNl5jMr(sp1X?Ds3-t 챭O Y|f`CGWfӴex2 ɲh×z1d _D\57_i !ꫣa{z†z [۸j2HWt&3&~j ~E6J>FCEf ;xN>zdz뻎m ޾:/ݍOw ~7, .~q|o j[¡ex #a0ǸisL(xDD@ Yl :M?oXd\f7&&.Ox⨌"ͱM%v,|˟kߔX'tuy|y*{GVƫȸ}޾/r򒷝[Yp~ Y&Pc#\3\$.<1K d}8['7ڷ<6z[68K }8a~gF%n`׽%_rT6S0Irzi)h8i1Ibr`,۶BG 0pP )xzЩ̖ӴEL.$IgLmy tf,+Bv,Ǫտ^v.z{-mȀkÒ1}NG3 mun\0,/M3,!e/[)Q/n%~B:pWLET(8EH28d6/犺G;5;z-?M_rrc6'Z>8cѭJ!t@$Dx4k$躑i4otc6F(7dEZ|85 XJxs[ W؂0,INm /y-hJvO}0gam~ ƙxn#(ߐOmM&t`9TMmǒ%[?D*aOoFfx(2SlnRgqVl.=w]K ~m!ƸZ{\{GַXۼC{ Տ7ߋ+1MAW6BpsW`$>}O@M.2JP4rk?Oa( ryo' rrY-B|&VW|P_T)<#{b\|[@Օ[WVbC$˅{mQn1&2%ߋq0:Lj;+9ߍQww  Qt0XΣ޶c40ab#BWKodcs>e_GC&E?CQߣMcˁq0w=(~)i|PGL6pJ>A]X 7ZzM8~ik[/~_;NoPB-[Gg}x4qPI4DCueF My2Y|3:/K`i cU'o[t<9O(G"$svHp؍-[ynʄOqM~l.*7b eà,f,# r3iSz[-5kLqyPFX(6 a4!_^;ow7-C| 7__!Pӵ5ymv<&V8Yh:/y{\HOׂ^-agə݀2 d jQz4e#`0v.tDݰE~߳pw]Mӏ߸/zƎk[.e|@9xox‹] R;l{|qvZmit5@M|2|uNF`LL9i$[@ēQjA$Zxo;%-`,/I~e~1iu5=CR/tx}N[9_62[4*Y\#ڧ%YEþѐkf(}I0j-MIc_R=yIxeWZoYm >;@_4$P!נζR73Hh^i zRjq>NyK'l]=v|Sol/_==ϝ޼f} Je]|8{sqt+bG*Ef'Bm}5r%騈Mxi63U tb-R AӸ5n+0m[+[馣Mۂ+ӖQ [K(Akwcm5lQq[GN}oY$AC.Ҹv!+ ڎ*mq|@w'mݛ:zfꛂ wnv*¢O@ϯ_ߌ)RĐ"qՑL 'Fu2\mΰ _`6dxb| u?}?x5}`ol_9~{oĿ ĹSZߓxW=M4ѕLtΧ:e|s9 1UzM|%_y <6B<>t-q22Mv[Xh#=o.'ia_Fen 2[&Ű\p#I$qY2a!.*0d99scɑ?64cSUk١q EI E,b:&g%xll~oآ3l=2e 70C7 d@-DG>*rsSrc\Z$M P-ZXƌ1㋪?‹\B<X6w/>o)gKG'ÿyT#d* @")"k/vHчN2xt67e+|Qn 1/m}+G6'$Sg}7]1@k*٨JAK旾lH>?Z2QdxaL#L"9@3ӘGs2G`Ъ% $jA$Zxo;%궄I~e~M-4}ҖxhՋ>c䛶#xZlӆJmi<$Lja$7< ,Vxx\NӼڮx3qM [Z/U_ۼA7.G<3M/#7p,81i k1r ISa+ױSǕdF~$k`Vv~\]pp5m>C>qѡ1=pZxü&xԃDCht͗1F׍vI:o@M>YeLIA0F`4LT] 2Y4ч ̖pĂ7=:".#N )p C.5lYl~m5ñ ƓAP>'x8dD\{1 ֘6"mU4BF0d m[kg~]KW/wgvV5g u[]3Lt/Ȗ'𥋞 X3ţ9B„y)~-K/:9_}hv/=i?}j/׎n?_M9O_F`WQԉh&f#o'ˆT0 mN1('G1i̛d6obqsѡ'nbH>l!d8)a#~/\)m~ o(RE}]'fJFR [|H@/ „q 8,3/Im7@6ʙF9 '[HCdYl-*Pm L%[4A쓦M C=S Qۘԡ<-Ĩmr~@$_6R!rKYਝDWQ*o6] ٦1hP\)0Z Yt9_2)9&@[I 'llL*=z|_ĕ&E? ?F{g_)\{xc^&#<Ї;p%M{/u1_9 sHWD<do? 6S0%.fhLfIx778eG0$؊Z~H,.ਖ՛2O,}4_.| F("r CHN I0x7Q`3gݶWeq6117U(KˁƵTdM嬞KLɻEmB,mxyd@Cm{Ir,pHϚ&#%ߐ%?{oh?dxp=- ?xqZ$-*Iߜv<`vM9%]p8r:ucf|e NeGN i 'NEN䴰 x  Ocb :ܖ/ْ܏ly<ӗ05[J'# [6.~ܿA'쪇WAĥG3]`K.'MOD8S3G3!39*䟀+myEnF1QpnEHZ9ѕ|^t%^˖'gyIQ--rY97i|%? uB D<8 O\|͎d,SkV{o}p}XrF"\/5rsNG`FpZz]7O*8t'̎ɌQBS1i i8xpVib҅٠-lxaqJ8I_|O'=xō)lQ}}u[lVg .5lm— 2:C9^ ~m}Ἀ4NSf 1TEr95MצGcT^X-⸲]%f4]n+0O'\ qQ%6%lRԔїJH4$t5Almzoc>nNHk22~#0l&O!ont\!Jh:-2tg ':kFJxsM <˖N SGAD ).3uԷdf)@0dprc&Ņx_ЧJPLmj8ۮI:6R_]N)f6΁±T#GA<8&N)YȜ/N ܔA%٧myTiqnŘ(V2 lٴ*v#`JF|q/!J^t8,fhe'Qg~ ԵAPfa0HȃO5fj(CC5 Mx#'\n 6>BGdGJ~W:dȆ43?' + KlF6V9Y31a/]G6pRO5F!WJ4 r2دUt&Y Lr=TF`L9-&}2ܸs B$[Ѳ i.x9l9mc|gNBx6lٸ-+#:L6xs1%ÝxLb PcʦbI'1JjЉm1F˩KNJ qg}k:2^κNYvIc_ԑ^˖ ټ$Pz8_6e smsم<(넌E<8]Թ5/+*XA8N\hP.]u# 1 /,JbNmqmp&觉 #0lf&'5^ Il:5>$d I TmP迶Xll|3ҷ/FPC9-/#؀%bXa40Q2")@FJvWer1K31Xg~-?b΂a๞K0]V!

n6VD2@b0t*lZdضdnoG'Įˬ"[KuC5 @<4QYA^\~ Y E$ɾݖ~2[o[7?(B%p!s[Cl?x\x/Lo"/[8?aMLK3emY17dOE>&04gj XJD+D N6"t9wJ>{nU޷M&Mmlb07Ei6%$D խp%RT7*r/?HӪ>>RI*%j.'"DJ`SHqmmw3Ƙ>}S]s11ǚk}ke˹|ܧ>dUFc.R9u;֦9ztihg&kb{&~^8@˺4YB8nV8pktF [1Yθtr(NcCTQd).mP/| LCҎWcK])i8 <6rx5k_Q<Z.s_zslٴQ?JݶIƎwY1 \pSN֕#G &h|cvNи%$Obz)Z-3OIS(iI)2`?pq52mɅb<XWrƏM$lw΋{)>&'2ljm_o($]N?Ճ'sr(C߁OZ~ᶮ*^.h4Yj\yFXlKhNʒ C ƴnӦdxjƙˌyI% 9Nf0> r\ ui?@@C 'N/燱t\o.㲪zuaGÁi2Nyߙ29FkJJ_ickrᅫ k=,j=R$æ>u-pkCWE ;rT{ts3W˜xs n2 >yn'^+롱Ԫnh%Ue8FbIZhlP`adWa h0h6=W<eS)99y}.UqS}“eǍ+p3W>OllpF?6Wr-/}㘺VV!oHiy{4ꍋOl59bz|>-:SFPg?,M>>XB JE2ʸDjزO|lӥaxF[HܔlS; W|G.҅GrS*<0gelw6\Er/.8 ^LuMÓ 1wPز^[] ʌA9'&;'HSis)""=\VZ)WN9uqIג*bipxspvWO6K yA&\#%c~sọmA^HO:Sse|2s\F2A>w܎MPoOꮨzHa\ :'.pe3o)^*v؛|8b]w.Ko u26^VB5jCJN_y"kcOt:tRiLAڇK۸s1$CX.?l93 ׬tRN8)_sx+׊kNYyVJ^,9j87~ܜ\G[m3Rt$ul-P;fy>g;Kc!מ{NGBo0  Yk^hض)& Ji 1N/bes>qFS=&=۟{n&<6$ @b؛@[I1 &%`DĴ_b`Ο ml1.)aD)Xm  \PΙ)h^L-m L6Cݕ:kg>Rm9'rXB c]˖Ko%̕~PK%h!/p.Ib\K&,'e'WmW<ͯl  yyÅن|Qziway‘\ ;s&x:ƽK@Ty$8u`/@>ӑprV Zυ9t<ּbOLO#W=.x<{Tu>MztUwB6]٤_ϖ~v$6mmm<οOF1'r"V/ƃbkHot+t9*vrh~"3FzڝpW>3|؛h̳oⲜMvte"h '29mSlK4ϱ޸ vr F"L/7[amG)M!5nTw!h\M>⪍m5.3>_tWnU9c+ɘD #ўOcT.NXl EG><07(981}Y|w}*=@*|ZǶZvo%Sncuo~) 5Vmg|rHxϒ䢳 W> 忷̱ngb<]l=PsHe1:mѝ*8xὮ*^.UhwX,>>Qf 3Ro3W|P 84|r}b<ӸjC'7fN\-ȅRMic7V.<; J6<4q"e%r#qJ ӎp1_bv.??]d]ta|5COɘ[lNΥ3٤/۸b=Tѥx m /zs \(j?1`Ͳ~![zzҵˡA#$P|dswIǢ<¹v褕K c۝'$ޟ{n`N]m I)DaLwm-|V`,iً%?5yt!NW,WO{[!NRl*ߞW<ѽpc=.r\Bt9װ7myD%qqdG9!wʶmkm2ծzRD`Lu$nc)6 sLx$Kd<\Q6?sr-6Ri~Wa'ցg,r8Js t"_ (cdo< Er @8[J#7$0-38) s'x kW4ᣯf^:E\ՏtL}Ih6=R5\-qECZn1z6\h- \WSad'˳,-c9~<.xX/3YPXOvncDhNɓMa's N'}`CɅw;05\9.spM9B<ʫ \1&AQܥ3bDi$tE! 3sI~s{ܕG*S|'sIaln{%IU܇U FstqXK c][ˌwlQ1M2Fm{\dz-*`skG MdII5㑍ԌMOYě@rdekYϱR<VI7ԫ]zPsbQ>ʈT8|pWs3Wl!1xEy-[ʹYsk!_|1>׀;ʹ+eNIqj ~@z`Ɂt#nXDfLP\J FF1'e)UɼRiLǧ4~^_&*Y~.fJU}s&kG[mK}ڽܞiu\q㾘Sbvu_٫&<ssnj)>mBa==Rac:u.P T9$A9f3?]F-ͣiNA8Ѭvrd0-Kp|pjXg ϕqD4rx9 kr%'`K)[s\ɡ-WΩlsӅ z,V[5HHY5~Ӑ9WP +~y??OT-LVo{Ʃ/A6K~$o݇sir xp6Aal[#^g?{}]`+S ]CmDќ FzƏ6Co.|O.,6gЇ\"+EȌg=xa\[Pyu/iN VJ ׾eغifv4jOÅ eÃ0-񶉬siLuKǍ5\91- Lso䲯bUŁrɇ( ԬD}a;bz.Դc^,˜'IիᤰNzMʆP=t_k&/p6 Bq< ؽ)W+.M]-! l_-Ɉ5۾Q-Ϡ 5U`UfG_  "7 Ȳ4^&X6g5\⨼+6\m:Hډ}•s4x@ܚM+?$j2".l_&HpJKsȍ&>5nEI?;?oԫeM>&lè8M>isTf?Ǔm׼q~)@Nl+|\.HN pO ^L6pksGbw9gl1⩗e6zT/rJ 5̍56y<豭vU Ue k6 䣍bzAu?٬MP y1J>G\8ԟumrۖzqn}[p9/W`\QCwX ŚF6]' nV{=.#|*fuG78WqeR W%6ȫ -iM|6\`Ymj.!5q17j~ e%.W9ܺVw??~?5[upKM\ۦKPIo6ռɶx l#> 7ڳeqqCKOf,}pin?W%ْ u)0E.<&ZȡEgcTyskl D|'9m_f9#ٺu(<`g} g z KUoh qd񩇻la:d)Kv$Ge]c0\ips) Y6 >}+96^K&+rF^vtdžNƯjظe߾1yir0R޾kRȭ5U>)E%+kC74h:lSzgZq5b W^Mr8rѳ x_qϖM8xz:I0Mp9Il}nCCIte92D>9.W`\QCW:G"h,nӚe`< ,Mxlc@HZwUۼsn|^9'%5չ @fSnyv=Zfm&B+HֹRkyWρ-^w۞s)uq8y s94^wl/g--49$[%ٶ&o8ٖKÕ6\FV^Q=%r4׺TD>n>'ql0ao;=pzP>=g!xjwh5#}%g%vl; =7ر%ubsz5[sc Ot2-PznXZN=z<mM"e1vgG\XM>7zLҺ{>jC7~^@zgYmƌd ;0pʈslrImw5N&[>6>+7|+j9n Ȼ/NE7:@L $j cnKݲȫ~~WguqTK+ CI0j'6)Bۄ}u sOq+\D E>]Qam2?+,#%mMsl9AIQqX/k?hW(o7iѽIGѿJkCpG^ů}UCmRukz Uz^<9A$'uHqxy l|I5R_7Z$)s?{E2laͲ6ٶYOOzﭯb|} <*wk62dylba [ml'xYm"\Æ$%p5+mz\5K2~{؍+7 M-9Eʶ06 [b縸̱j[˱3Fz$4}Q-03hi4{?ꁟa_| ܑ>Zx{oRQ ߤ=Qɿe[lH,n ldV;g}~j=3.Uy UrlR6ŸlIR@؁S4n"j%O[0+h F{l[6 ֋_s6#`'e%p0{Im.N{\ո1>s8JsZ-Njq?Dd.RqB<2<,e\qC&7s0od՛Ә8d73[//U`\Wxc|o~n}^5:-Gz1xcYǂMP뤯 a*g2y×6p`ʫ6\-G&&96܆ mBc|ҮaăFp:L8b^>! 3:iu*о }b4YZ()diPuk\'ۖggykzӼY!M5mKpu>\>a}nY28cO+$9r8qX=m+^3<qѦCR_>β7$x Y]9:װ!W)6qxHۜjp\cv9yШ61I^A go,Wy.^PSS8b-`{?/<,k2%L39>2q/#e@;|xX/k#HA_[F-oAցC2nH2maR?4`l2 2?O)aU=$EJ:L ;/ ./i??.Wݵ/ry.kv|o?g e[^žRY@)W{I5fk+%Aʖc c,}ࣷ2`n㭖]ztp54z'1XɷO1`xA< ǃj+^箰~Cg?/~kJ]3^R46)x۸ ْ|Wsxx_r>|Қ]V֥2kjWVj uar_a\crp/-qbp۶"MnɠWKl%s%&8%܊6եi,=ͯkMR6Ɋd259lӰ11lط~+^箴~s@i|~$_[SO\ у(xj3XăetnzFzدAnyhj6t &!CIRD0y+4t](j./0Mԥ g\+p.8g2(3<ɷxA4[>A:c99Qz3N>K}ycwS2c L4V':zhV;_p>so~#J_E_G55zYNoQaXbۼm0i&'s߉xӹ"iV-ɋOl<l^~Ytoܿ_0׺< xz{CH}uqXzm0h3W,p5mqIu%e9q%}Nv,gOs"ZG<زli(ܾ汛칿_~Mz{S1MmHǾ.{ɦĂ;!i(??@by~6[?wQC$L~`օNwb/;:k(d{}еKq1A➍xyRDD2QԲROzGYo1Ɯ`<"!Z"CN)/mZ,vbg;??ˬ^M#o+7΁k6~agOL[l C%4b?Лˌ|¿mu}6moyl `>0DO |ůuVk&WM͏&/NlR4vTi&L _ .IY9O SC˺4YBm 7?Zp9. 9'0z\>mb`+^.|៸} ?PwmPwCc8o:bpk o7GS44^'{#N"cߪ:L >&+d`Dg Mw)Jmko-ܧ1bTu)Wq+'σ|4\U2GDscV2 nN7 gϜɇ+f~06Ӭc3W`e 5'n&n~6B^ n_omT'6XFOs#>cqSN?۾`|U+c{ zk:d}kE>`m;rLp6W;pllJ&=3j|mKqJ$6F5We],]lRo&+&<26 Mɡo].JQiSsBT`\S5}>#exn{6xWI^u؆~ v3.~16}"ɘ"xL[ُ|__ú<#X lܾ3GOݣ[\GkEC#Pԏ]Ėm7e Fw&P*/fUu)[^1_&6F8)g|w=#۬o*n;Zѓc xQGl ,G=?77 3]e4=<<<,9ll7.K]o2ƙ6kq?Kz*nO㮌ۈa"/is 91dSh;X"gC2m>Hnspu+Ȼ}NHu='"vm Z =z7 6^yyPMa<21Oml͆10v]_WڳPXݓN;cYIo 3K` ^r޶+j-8?lʹ9ĵo:lxJcp@e@A(4vR7ʶ I>rM 1?z(d1ea rdȷHWlyT=/_ N~Ǎr+ݟࡗcۀ5__+V{*~P!wwOÈl+g+#y2>;W٤hv>x֧R߹z\ˀ"s.< us= SPc-G=NMg589jcCo[G96us~gD ʴ@Oz!8ym}Jmm~yǗye oſ|ZW_=DdY=Nzƴfa.WƲM7\Ĩmi>e:WljUY:F*{Wr v׈8ʸ6TS$&[qm HRH>a 7&mIpɹX/볬Pn~#5G_͇I~)!/n_@rn_ݲ| ?sTݬڀ'/>Ό+d҇o @n\Rf&Eao9K!W\p\\=nr p`6-!=k;8ǃ4 n̯j"dx.V`\,<>rKqo>_8?KMlڼ~X/?+g<حٰf2p}OGC "8\Kt+}2ٌznۼ/_x8\w'֒c9rta=Bsrg7y$Pcei}H#uPp@TM/o濑?՞ ieý9P&F^˫^Cx`~Hsu[셣X2e|pw&=3\w0u-}:Wk5.ϯe` sT166[< !p.+/7N>׀"N̏Wo6^>c^x#%wy`Qc&w#ۨsN8Seic9|ފjU`\WZX8@!@٧X5nX&my#2pl&}ÉC|&9;28x(59RL6spׄ'dz\99Fg~5[6F6黜l]#޷ݾ󗀭v} ZUU ᰨ X'6O/@aOʆ!<6ᓫt1/|ydM~$<'Bľ:aD]קRT|VVZC7=9e J^[[c8h+<2dž+x5x\ίlH9r |BZ%hz:yP bxl}̑\!VO>cwЙo1ǒWT?/@ӫzxz[ޫ|8اǯXҙS<K޶ mq⇼>sl@`_5f>89ʹ9vK(emUeHRsO z||8lB;9KYퟖSX]KV w@9Ǘζ|/mN`;WrrXAR\y ;\8F)W7.sҸHTs3Wƃ+`Y -3 -8%˩P7r_ q|1=){VU`|jV a#wKOsr>"ƥ%c&}`pO>S|F[m\97t8!?qz-w1B#J{M>wp=KqMm7?HVU`|JV 3@ٲwŁ_X~.bK$՞ guU@'Mvdpr'XmrwQrz|~fC.nƍreg݃_?o?,{+^.UuH`p+<z>{[>9 2/xe8+ X f|\Wee0,=>ۗQI5[ÇuoʹsK.gJ!םs(hRǫqSܴc(|$rN SeUྭ@ @?t ap0ҔgL cL8tlr, #l kp&}EGJa*L.A~&k޸ć5ypf ?ׁW`<5_W ֯-g L m@C 2 /B&bSoza6uͪllwojG?8Fxrop.zx"6E՞ +=_sXpx|/0C|ܷW^e)vrì^8NBȊ 1p^BEm X@\ƕss-̟1+z 0'!QGX@fzPQ$Zu Vu*^rX+~Ps Ca<87qUMBv|>gsJu?LK+]JkU`U^{o=_˫:/oo ~~JowN~Pewo|VfK[['tx5_&o)AI>`ᡍ d3a$WMWv@Vk~8W%g/^z] /J/uj^:R_ xn}6a?\xМ_嗁NVnZq&]΍@j9  O| ȓ?\x)kzgaɭ=[vWV }^0mP/|J=w-rÖJѮd0w%W')Ixøa[n @/-OK.QMtR_ޯ@[/T VV EmdC'jz vm :%ڞ6_ KOR\e|/^n2Ɨ'/iSeut_LfMbU`U`Urml=~ި|]mvO|G'](^B9Io\m^<>v>Wۯ~o jWj_ZYXX C۵wH_˘k<vY]BwdpjIFNlR4/_F ?m9p~+{WJ+PkݪVeWAmw Mb({ 8ꀡGeۂl $/owP|gm~YOdot;T25UUUV}#|EE/t ж+"}zgw7~N |ff***4+n@'m_ Ў~mO)ufM^8^d |: zz0gf***U}֦Z:_@6pr+~Pз{hlo|Z!~R?KW{+M}U`U`U+>p"6߸_%L/w &K}=R?O1V[X/k K\a ԚG]i _xߨ#໴1Y^2R^T| X/c5***W@/B@n6ڰ^x xB?Y :S\ <oK_R~7~U8VU`β ܅o~zC,>}KB”WJwaȬ&[ XIENDB`il32q~vt~wo\b{vf~u@i|um@ T%|i nw +5 T]fpy 'py :DMV_h "V`ir{|OYbh!*3GPZcU#(  %.7@IC"&+0  '04*/4 #30Ĺ̌íÿگ˻ӭة̫Ҡťʘ̽ÐɲǴȲʭ̱֯{ηЯ{۴ɭyԫª{͢{ƙ}̷zup~z¡zοIJת̠|ƾʹytͶضpk{ųhsϡcĶȥϨȟ  ĸ~ yž֊Ҋ̊̊ډى އևĽЇĽ̇ĿɇŇͭ¾{NJLqiea]YbȿͲ}yumB56݋÷6567Č척x66;ϊq78iيݏirzi89lQZbksa9:k̀ц轄[LS\Z:TՆȨh:ц͆ϪȆ΀ Ċ܂ 㾿is32qyXtMdfy-e  3FU ReKRi #2;RA0^ r&QS]P ?Rb'dw /31DW#%. }vA$-4J]*, XQ& Vji' #6Hex 2EY)%07ŶŅͺʿձȨ ƪ˼ ɂ ӣIJ ɣի Ț Ӷ{ƶц uū Úg ã֢ Ȑ~v܅ׄτ ރ ׂ ҂ ɂмſ zVȾ꽲P5]픝7dӄ em9˂ 鼈q=Ղ ̂ Èl8mk{A c^n6ʿ(((pڸ>؈hhgMhnEӿw [ĺg.,hٿdhkhWמh ?3hBc A{Œs8mkbTQ۵EPPͰHSǣfX[iricochet-1.1.4/icons/icons.qrc000066400000000000000000000002101300720305500162360ustar00rootroot00000000000000 ricochet.svg ricochet_icons.ttf ricochet-1.1.4/icons/ricochet.ico000066400000000000000000001567261300720305500167400ustar00rootroot00000000000000 `V@@ (B`00 %   hnPNG  IHDR\rfgAMA a cHRMz&u0`:pQ<bKGDC pHYse\_}IDATxy$U&ȭ[JHjYmKf3^ ffXuYc:t^aOC2@en{q"ZMyĠ:3>`܈s <Ì10F?cٵ?~ܿa0x@`2~:}]p:s'A#fʩ-} < Q b*0ܩs>tftփl]w aM7n{BqI-K-L[9µm 9-Y̥.0W٧0o3ЗU;q^x4Cp[H`EwVA@ dT5vP;}㳕瞽RcgBz;؟<Qrr~$ŶlBo06 i }0^ܗ._.V7hx8N[8EݼVb:B[" CzI ĸD Hp]+>}jN6YF!Cx׶~ly_Ϝ2Ń`:> p3cRK- ,NڮI!%\'l1``'\-ho,_Z^)xKX_{wWS jG X1TwlR =/Q v}VB]g5ô&L̸՝9S\ܕY]<@Pjxث{vցLf _0ϳ 8t2K1KmgG0%:O/cYpWFSXY9)ۜöؑYc/*;m@D7[%s!J̷9;xs'H-85y;R AOD3lfT'nxsSN}e忽^?ꞷka T]nV{ô^5d9t}?ŷ@  (^*=YMb %C\)631Y|ɉ5BUs)M}8 ) )η_%PgŶDY:O/`@=8*՟Ww y/G\ r`0@Z#T-`Ѡޠ-\ͯb<1y/N[s&,*»<?Z/;;՜=\/O-k2X۟@K yj^=Vks1,~DqPڇlFLzneL,u  *zs˿)^y` hCg1{:\ 4RGU&F w, =7KP3˕H꾷zi} "`eoԗ^TlЛS1س|aA_GYb؞0,KeI-H uNzGUTwB@ʀUs1i\֮} ~[O!Bz=C`jV d5Vfz;< ղxѼB߽? ~1t-b:5ZX߱(=\w~!ڇ`튼*GMK\1-hy t깞5C#ynoTn!@K-;,R' mV@nCC8*vi+>02,Q1hUVbzrx.FPꜽY v&@CЧ#^י8$Ǘ+^PC* g?HrA5}ڌFk[!pS&60VQѢy'2ϗ.$CŻz'hN/~H/po9`G6hB=%IkF"4WU }LS%/tnamˉiL>S"`n[كGY 0.Ȱ/7D}:#˒8=ԗU+m}Y֟%z0X(Y 𖾜llي6gpbbF筱<7Ϸ svNea zCP4@pu1G]OD/}rݹ&b{MWQC=^n'@"K-4lG|9H0ˎ_/  /nɥ juwS[} RY68S+ 3I ~^{w@`Q?In<_}R (##+ K"d^>Pf^O**?V@ڣ-pv4b`Y@멷QD/P5ǯoXwncgnX[/$w ҵdn3:&yތo#8^ױx@8)BR@x#|;(v0r9zAV1ynP˫:7{efUlYU&-=u9YFK =釷LuQ ^ Q "u-H hRcL>4knQMJ/Q 15a=1?S Ax2go@h% y7Dt$ b> 4$+! \w{5{ 6fl&| ;!RWN]LAO}֟W[)%O 8u u5\7^C@/`5yE;nǃz籔 {0V_xus5t?K1sly~,R/\/5:wcT #u ]t Q?੿/]<\Mi)˻" ^BR`K) έY@ Dn@k{5AX]rSK)t@aCcB/p̵^3١I%Z"n@k_uRe~h:wߵqjĤrS- <^=wP[==73GdzAJ\03i8;nS:oL|VO]K-X? A h^965-3ap@ {y^0P4*jKɟ-(Y;x t[zd6GJ#uhA/`q wLW?Y+v@0]@E*ٿA!P_ ٿ^01[4` QWHL/JNb)G=EL ZCXD>rju?@E P$aU Z(* eR^]BhJ=g~!FܾbAV 4abZ>qED)$ knv*$ʾsVέ P7Z a8U߫dsUSaU`\15ol12Y٬.uahʣnR f棸:۵UU!=SXέlMX&(̎aNlw߫N sX7l*s|_Q@IoNfYI;„5 @^C=Y,RǛ#CyQ,B/`@M22uK" n@r}ݯEcN֔RQx Tڧz`z AQvdU uL HK-P䂠;z;'`D@x>홾iTnR x׆"QP`YEY ʀ 9zA$ 􂨪9澯 Ipn,)B/pW An5^ ¯dnR /n ֶ 6A\(+`ʀ,HA(Z/^P] |]\Ϟ3g @8&`i@Z,@p|R:(bմP(PM+69'%z&<P=^(QN:{/?K1F/mE@J-PhI)3D} e{pu.qd{iRꌩ^/-MdA\jLV&R3 +)VmQUd1vOmT/h4 ;bu*qwnv^:yYƈ0J3߹65%tBhmX Aԣܣ|FkɃ9se"Pr+L 2yP6 l DT_ aBɨ: *̃B,bςCˈ7ws+h Xa>y^;qO-۶M{Z𭔋Ə;fdm~Ck @Y'*SGL.+MU@党gkYl`e U~@/ =:4@n", Od`iyL -=EfJi3c%$n ] Rk_\w2~]hYZ卹qŠ k({CY/_z/-Mo3H@T@s 5s<+@d>wAGlUKU.N̟DԴ X:FP! 0syh}^.9I K:#yc] \/P}ZcTYSySf~ZƖbh{hw )h[y[6,1sxr)}h^i`P솮D6NR }PH 8>*5DI?os nH: ' \m`Cۿ SeҫWK_\Mkt!\hZJT*ȕʠSvڠ([ڰ9UB8H ꋏjzy?מwh=($B3U"vPg>v|mx}9[jnh,;u(*OYƢ&g005]>#g8S8{b}D!`s?_=5PHᱽVg9H(BgW޼-GZvCQε{w; [?Y|TI0!< 6*EIxx^Z!^ `h0D,.*.*.*"PYP^@4O%|,FHԌg/UY̏E^v-Ps$bjȇʎS_1&!$L&EbBV%^evPH㶜y@ ડ₩ᜮ⒡`N(u#@8#pʠW\?~t!'59>eKe)}b9i1 \ ytrNT3`S 8`pMd0!TY pK?cr9! u cyp^pPqPqPAlป wMrJo[ +iQԟnH34QaUe4-wWSul(f*oڕ`XBt2 kiV΂D fpVaŊ|EYʸ``w ]vQ༮ᒡ8 5+({Vdh%b}y\ :{1Z ߡzʷW/X/oeBPzp^(|lh]) bf0(:@YN W-, Ee $|5ga[An*80ߧVd0)ʉK/KŒ)"Uz]zA GghAm-C6+3eqj$*EC2*kUTDP !Fhk3{z`BNɆ?Ocח7!Ǵ@pj1 ="`ۆ/ c8TKUh^u^սBE>SqeXkFΡ/sq|@K- úUR2aPuZZd{/ 9>G`3qq"P1ym9'?yrl!Ӛ#˛cfI$ZlóOE@w 2ߐ_/J[Td5} 81W %C ;iCa)¤ԗx(:G,n^P?yf4 Iu^Pzb0H&FV ŝ(|h"u9o'].[M}26Zp 4)i<7J/h0h&݅L%-|v^g˰x`xoE3RydzRpgOPn7`MjbEXwCL RUMyl.ؿnj*bd65=m~qJ #Gk#=,sK=証q67^?TyDpZ  ߓz3U *&-|df lѽxӗ+ EGE=SCsQ^f J i wSw@-HS A& 8ѨA{Tx>)~14UǏsDen3l0ǎ54t^pDM]N=^`/GjeǕ2 ш~wN/ETkj9 —SoN/po廑J@dO2M/`_?*Q 8u.>ϷƠ!rT?wI#;$X^lY nMz߀T> gGVc9[! &kQz5^gc]hZc$nTO/v:ORE-8Q85@i@^BJ%sGahK/> z 6L7V7Ϗ@d9[jW m3^P `j1*@$ )Y *MKtt@[͍R#u4V! '^!G$yoLvvy×1w8p{;?B,:7L^^JHa8i ~GT-r}8ҡp:*I-}C*-I[,{.hɆx`E~ Jؽ-cٳ*ߕv5nnJ(Lt08 _FþgW SzCV j" 6Lw?R ()kT宭 :6!Z|'߸|~]vo2s"6 "'u&*l|XyoxYEݱ,e%b12@mj`4 @84oXC[-ǏE?&~X^Tt0B]DT:- BP#cXP )(gr3 tR@y~=ʤ`,^N /3ᰥ*0?B ! AJP~ c>!(bxQ5$zAڇf *fu,<ӆBC$-WM7C/.v'Cyˎ+nF7^ LZP,Fd@ч-F-UDqw,0\^Bd5M# D;{xA-0F=M>yU@ &rB*Y0?4D@SE '%#&#s<{[tOrwh?7WAd:z/Q%֌2 Z!ft ETJNg#$5MFDzAu{7gK70rd* `*Y2 B(y^P-FϼQ[]@ϪcFV?LjiY`FMqZ{꿽0t ΃PP)و 3D/h⚉Q**2)-+e։^ yH/P:L7RaFl "R ߷>_.xuYc90ok QYeâ X́ T5Yl pq"`boNjb7󜩪Z: j΋^/9ݤyTg2W3@y@o> Δc/͘ՊblKQ}L*X͂,HjߍhQ^TK\Ū6(==[,hHϾZNd7D[ζސdzs|-NZ۸g1'kk+^2+gUewȸ"@FЋd-L(- - B ׫"*O||aIeJa3^o8ۄ[|M28zzuE8~b8{y0?N??\ _hauQiڇV>\::g>8Q::kN60VT 2+Q3`- e۷(\T%P>mdySYO^2S` 0(` s27ق+P{I^ Ǔ_{R Q 4Qvdkvb|h|iRd6x\,dҼkyp6Cdso"8C%04YFdVJ^Ϯ$|+西Zc~:&_b?d W̘PZfe6㰶Q=/<-QHw#ާD/F馨/^ih,RcLCڂvon}'MVNtpjXQ4yBR)e\̓sy@B^oN V?]ۅg)VAE0V&뗾GW߉^njx8 0Q̏<ݗ.x%U3gxnϸ"^=3/TF숤_ Ƈ?i_h?y_4tl1ph;xrke⬱N7y;L1){u:'o1 )|T:>`3cBdz\N<;nf#=|To(J/ ϯ1W_t[拉x#@ ^&KO̿N+n讼=~Fses N*^݂gv⹁x~`'eJHyoR l ?6K7ԉPHL'~U @eMaԾ iDo)[+_8]gw؀пGz7BBAzA"98s}<21~vtN"E@_BdxzA$ 7?cW-$L=O>1Bmxa^-,^ @<"?t]R I1GduU#cՃב5KtiD <6:; \녍x~`z _֨/PD\=OCt{NX8#d3|Z; ?aU}j]U}ti]{&~pڃi઺esxǦd[d <1~`?w-٨'f\W}Pxi!/*x(|~"kHLF̤z,pI,eaO~߹jۗQa#l >8?/N +1B6(@ACQ?ë/?K-ԭ_h^'ǞנrZ)j1t*EٯYRqn{#Nڅ1R -^}'gcx ?8/ ݔF{'# >6t G쿺@(Ue=&c rq(w[$9Xi8wKmP\#w:O-4_$:w6Az `y}VWCk7Dj;vSz<}ܙbÛq0PCLcP@Ptt4+dq(?J"@ h9|:p-#~Ry~fVgT ;t+,R>}+)(LtHgd< 8QA8E!۸$]댥*kNJ~|Ni0 @X{Boo`# 0˨Tѧ0TfXWkN?h:4œz#(3Ǣ^2R & 뮫96{˧ 3k @ y _h Je\WqVGӥ!`aL`a-ퟳDgnӵTY"DuEѤa  nw BVi‰Y3]F,mO#XOxn09Q_=޵TYݛP3@!0*سj0`&fէI)(O/͋Z,P-! @03(_D(jla@h@K (Oc%35A:(i<z]K{ow]ff Tw6Hi, B829iYSؚE!IbF;H.,K-@mJu``UWqGUN rۣ޴f@+:NT}klړ*-M3ύV`<߳M=l#/c]KJ@Lf d"`5bvz{y>W@:ԋC}mLu^ MaKn>o繫^U "R AU?.T/pR( ȞNa_20pS {A'Mbnv|sߥqˆu~|?8gtkoėwŚh"0yE&BP/byY"l_XJcx-c6tkCj^@N Z8zd@/_/=RQ!"~AFSz-v,Z09 x*eܜ^O!"ABXdXcz߅NR u^vu`{3zo3Uv$ <0UA+1`O^zF/HAa/%O*-3a)@v,zΜa!qC̬P@bPlW~g17`uZz_l}:М^  D r |pꞰ[?ᇾIV/q0@v&~;1#b5Ucl+{ҵk?({(.9{<6uF9; Mo\o|m?a ¸z@Ok?_XLj^PMa+& zAYcLW͵b?,VjKdɓWImbW ]KHaqz{B &٢W$nczi7̄qERpiԔӷ *frnחjFC뼑 _v 2\ P D:.sv!Bxhs3$x>_YԵu ~1 f8ddObp4uo&wŧ6 `Ҳ}{Gn4J]OhDDj7l^,R7^W/|ɶ C|fʵ 8=ًjDŽJ7,dFK8o K/ XD_ ^?J/Wۡ@h>n@X[Wc3LjzS,oj0[>&WF o7L/wыDW$e!zc̝X0 }b )=% X́ T?w# |ehGNXД^dt6  'GZA\@m,$ZzZ@̞=ם3%KƕCvln2ȴb+Y@ˁT^P:Wl^ژH 'z*MΞ  t  s9 ꖂkkA iz켹dN12엛+*fmP I@9W5NC/ϗmY%X#.@F;K b`ȩ+Z۟Q>:9Ƭgbmg`7HX TVjD&2)hHUAj ݀@2Sp5x? *bqzW1ؖ 2i__Mz*Qwe/S/O^S6#ۘe^l z瀬$ OĤE'UB?_ `0=گNzӔᷬɿkԬKWW_d'V^2$ "Ρ.m"y=ק (@|OC;nW;ﴥj<ʿ`d6y^ /`Hmq`{wS0e+j 0Y&` l& ȁB~'4iҘ lrr1O^:#FkZzg-FD/pكlbQUzEZG:hzAT | ,z*BjL,:UA/6W J; cndbѿK/ÊvlgLI }@|z|y~}tvy"/#Z/P+:m=vٷH~K-LH^gDvRzFz Ο񵁊2A_ 4hv9p?y>4jɟy0uk,'DFi!zAh@[/U=@<_Pj$/i00!zIP(Xqaı B_X'N$R uQ˂WCaي@q|գz8|jHR!ؽC|ۣu{0W-zmξ3?5; <0K5>B@ >d#)$ 8fo(L N goG@!蟫`pܩ_5=`i g Եڣ`a{(*@n>Fy{&ԓºl_OZP< d+&K>:PhnruϋzV}*+#ŵ#5s_ X0VI6 i^?s?sRO^Q`d 0yXlB*qA^5X2z6ߟoQwNO/+#fFvj^ d7J ’hIv]?t~ͷמ#ы<LfhZ)|9,蛫@I73=`.Hnnï'Z<^V4 #Xz}Ϥ%zAT_<~.ʹ_嚇d)NP5lV6+Ftj@L{@tPc FVGs>^l:nyo г0T7Jvϕ؂ZYZY&@6(U Č9K;Kم&;xfI;*RK/x}8ahlaI2:6L1E8#v~ aZ}{vH-'̀T0,ĺ r= F*辥*gsWey~;m]М+ T\ہK};@x75l}b* ۧp= |0.d+%k)؄.@* }٧16Cy/* /+O_jv%|"so8)۳V cTjuk풿1K8KL@M0OKtyOo2Y)3&KWQLϪ/z2k@=?pW- VWlo ,3wp=g<<{.-| {q{4 zA|!Ƈ},QVY[]U?9kDe،X2cMbM(tԛ=+<И}jcMsư0 |f lo1Z>+y{$ip7G,#^/`|d? > QWUY m {kKW*օ^||k;ڌJ^32^b<@"'xLhaSw-~?^{`,>7LKt7w`d7iCgZo$$`1_|bt:YeևsoP egO'7AgmNkՔOGzq|oo itޘ+{/ 5rȝ:I}`1P 5o&bEފ:f3&@8+POjHt9G= /%J5WܷҞ֙y k3vbm joLtUϩ7K=iuxbn+7@`w!NoNJ+9L%bz{3W^cn`">1ڰ[t*6X%)G}kv7ٟMSg tZSxQԔQK{oԭ˨=R ^~Fw% < Xe}!N[/"^ :7̝veQ>,(QF}}'~n뱟gV_ xzA8(ɯ&gV]p9禌)w00Ԫ6 fj7Oҭ<^:  `a"` *"ɜ?~͎ 67(gG;sح]Ɩ*` CO%}wjj a@6{0sc8|hxnڼҬAbD*]C| wAZdwI> G n6;.2 *Gs7 -yRP9}?2IVG m{۾{%Y<ŒsfxZ`/ f@xn_`)tQYSz)sz?cޟ2 ;N2xfSE>H/6^F-ګʷȬܿf C?`66cN:gVpIh?كg i=)hSGE4p#})BDw;N hYSSufuXH^7i?~t5in#Q/i*#z"u;@ӻGH`qupA+ qbkF/H2u?]9(P)ߍM g7jP҆Z,CgԞlQs⠔Y:uC/Ɍ:ڐ^g N|Q !D 4(nZD@M AY Z,p-h}`]j@9qH4ay{6lҒ N(H@B(Z/Qu RzfX ⓫wcQ5^Pzpr<~C qw< m[aq+e;>VW2-&hAħoR ;-z.%Y)mf(VW-f ؋4CHFzSՁH -M9T &ȁLX$à0qMbOH|\qdy>1B$3pFwɺ95(Z[c"Cӣ&*\$ 8OЪg.;^V<(gqy~xQUt#m^:VXd ߑQZZx4Fu V zP#P4 "K?wU"*J`蟺Qn^ eu Ŀri׳PVn Ron@sI|΀.Hlr-adq9=Z/PP\+ ё?B|o>"\p9ij1X xHҍZ1#uB l`m47kC~ĸ΄'?p#s i ,WibF: @`8gj)ID@i @J[#0^whi`D!G}l*?XGטy7:}]N V˖ߨ;N|V't $wc4ڲ Y^t^7R x =20Zp \E~Uq&evzT2%o,B/@,ݭX3і-xyO:/d0C[xBG Xj(H-E4;"SuPfvZԌ(b{4`  K {k /Lo'[TGȸze+OWQ&P:ijHwS7&$+M+ Ӌ74!oV G\tzP A #:;THo0S}id&MVKE@szy~|g i dV|uS0KVb<;tX__@0{s.>,,.,װY0ũاFކ76_)0~zkLjp؋L/ >~~r@ N0ũ~" <@L1*L-b:`}N_K3̞Qn^/8tT3QHb%#NݾK &숤JN@ז}nb u#B) z˷{p ?(↿ۀyT?j@Zc_ܫso>E<pB6tc@%P,2ؖ0_&Əӷ]ǘ);XFi 1B @g=ȶ{fy.OqBKNv[ds |PQx@^=$M Dlg)ۢ? O;}ºеٓ-')T#uq0 M+SǷEƇ?۲_ަ*bDG] A IT>ұm! 1~v?wºеTۊXПD/ЊS[#<~`LS-/{=o"}aۢL|j襣N7 @;}9.tm۪^X} خK z3 S^: b'O]Km] g[z*?SܫXʈBu/yϏL ?AW º%tEXtdate:create2015-02-20T09:44:11-07:007%tEXtdate:modify2015-02-20T09:44:11-07:00яtEXtSoftwarewww.inkscape.org<IENDB`(@ @ A{ަ!¾z4t. V "(/|1y2Rk !&+/0ƒ+-~/{2x5"`  %).37<8ň&Å)ƒ+.}/{3~m$`#(-16;?DI@nj"Ɗ%ć'Ä)‚,.}1ڽx3F -hٱر #`'+059>CGLQUGɑȎ!ƌ#ʼn%Ć(Ä*-/ B }#خ֪֫W0`48=BFKPTY^bN˕ʓɐǍ!Ƌ$ň&Ć(ƒ+X "&+0׬ժԨ ԥ ѣ '=`@EJNSX\afjoV͚̗˕ʒȏ Ǎ"Ɗ$ň'  %*.38<֪ Ԩ Ӧ Ҥ ѢϠ̙ H`MRW[`einsw|^ОΜ͙̖ʔɑȏ nj#$(-26;@DIը ӥ ң ѡПϞΜϛOU`Z^chmqv{ԄՉeңРϞΛ͘˖ʓɑ"',05:>CHLQVӦ ңѡПϝΛ͙̗̖Ub`gkpuy~ӃՇ֌בٕmԧ ӥ ѢПϝΚ̘˕/49=BGKPUY^cңѡПϝΛ͙̗˕ʒ`p`sx}ӁԆ֋׏ؔٙ۝ܢt֬թԧ Ҥ ѡПΜ͚^bfkosx|u8y8w9t9r9oVzBLV`jذ/ٲٲٲ͙Å)„)†*zÃ(@̀3 ؕRֲ'֩ܨ zjnrw{z7}8{8x8v9tU~]gq{ԅٵ9ٲٲٲԩ  vz~777}8z8xTxӂ֌ٖٜ۾=ٲٲٲزp !&*6777~8|S͎~׳8С̘ɑΛرٲزp %).37666779~ͦңϝ̗ɑƋ#Ç'ң ڲm#(,16:?DĘ5566677Jң ϝ̗ɑƋ"†)~-U(059>CGLQɞ55566677џ=˗; =AFKOTY]Φh6555666JNSX\afjҭ|zxtI45556V[`dinrwӡ}{ywusqomki^8555 CHLLȎ!ƌ#ʼn%Ć(Ä*-~/}17 #(-1׬ԩ Ѣ͘Ƌ$Nj#ć'.z2`$8*/48=BFKPTYWʓɐǍ!Ƌ$ň&Ć(ƒ+-3( "',05:>׬ԩ ѢΛ˔Ŋ$‚+.ڿz2`287<@EJNSX\afb̗˕ʒȏ Ǎ"Ɗ$ň'Å)-(!&*/48=BFK׬ԩ ѢΛ˔Ǎ!ć'~/ľw5+@8DHMRW[`einsmΜ͙̖ʔɑȏ nj#ʼn%Ɔ&($).27<@EJOSX׬ԩ ѢΛ˔Ǎ!ć'.y3_M8QUZ_chmqv{xРϞΛ͘˖ʓɐȎ!ƌ (16:?DHMRW[`eدԩ ѢΛ˔Ǎ!ć'.z2`[8]bgkpuy~ӃՇ֌҄ӥ ѢПϝΚ̘˕ʒ̓ (>CGLQUZ_chmqذԩ ѢΛ˔Ǎ!ć'.z2`d8jotx}ӂԆ֋אؔٙ֏թӧ Ҥ ѡПΜ͚̗̓(KOTY]bgkpuy~ذԩ ѢΛ˔Ǎ!ć'.z2`r8w|ӀԅՊ׎ؓ٘ڜܡݦٚ׮֫Ԩ Ӧ ң ѡϞΛ̙(X\afjotx}ӂԆ֋ذԩ ѢΛ˔Ǎ!ć'.z2`р8ԄՈ֍ٖؒڛ۠ڟ՘ϐ΍Ϗhױ׭ժԨ ӥ ңР̟(dinrw|ӀԅՊ׎ؓ٘ذԩ ѢΛ˔Ǎ!ć'.z2`֍8אؕڙԓ͇|ʃ̇΋Ϗѓӕܿ=װ֬ժԧ Ҥ ҟ (qvzԄՈ֍ؒٗڛ۠ݥذԩ ѢΛ˔Ǎ!ć'.z2`ؐ'w[H8(ΙFشjhP:+ܼ,Їӌڷ׮֬թҦ (~ӂՇ֌אؕښ۞ܣґٻeТ;ңթ ѢΛ˔Ǎ!ň&ˋ%ߡ  %ɐ:}0&&0:ٲٳS؟jر׭٬(֋׏ؔٙڙyطJҦ&ԠڣПʓРסߣ #(-1˔8‚,&-7AKTٲٲٲܼ)ۦܥ8ܱ$Պ`ڹ1׫ک !&ңПʒ "'+059>͘6Ć(8HR\fmٲٲٲ۷ܥܫI߯ $).2ҤР͚!&*/48=BFK˔'Ƌ$LdnxӁԆٲٲٲٲ #(,16:?ӥ ѡΛ#).27<@EJNSX͖"ȏ^Չؓڝˊٱٲٲٲ"&+049>BGLӦ ѢΜ.6:?DHMRW[`eΚ˔n̆ŀَ˅lٵٲٲ%*/38=AFKOTYԧ ңϝ9BGLQUZ_chmqϟ͘У4ʆמӖύ˅|t޻Jٳ27;@EINSW\aeի Ҥ ϞEOTY]bgkpuy~ҤϝΚ̘ԮK͌ύ˅ut߿k_d!?CHMQV[`dinr׫ӥ ПO\aejosx}ӁԆ֋ӨѡПϜ͚͛!ҥ<˖$Ǐ KPUY^cglquz׬Ӧ Р[inrw|ӀԅՊ׎ؓ٘խӦ ң ѡϞΜ͙̖ɓX]bfkpty~ӂՇ֌׭ԧ ѡfvzԄՈ֍ٖؒڛ۠ݤرիԨ ӥ ңРϝΛ͗ejnsx|ӁԆ֊׏ؔ٘׮Ԩ ѢqӂՇ֌אؕښ۞ܣܤ՚΍ܾHد֬ժԧ ӥ ѢПϝrw{ӀԅՉ֎ؓٗڜܡݥذթң |׏ؔٙٚӗמԗАΌ̈ʄȀtڵ׮֬թӦ Ҥ ѡ~ԃՈ֍בٖڛ۟ܤڢ՗͉ۺ=֪Ҥ π̈́Ʉ͍֝֜ԘҔАΌ̈ʄȀ|x߿Xٱ׮֫Ԩ ӥ ֋אؕڙ۞ڠלӔЏ΋̇ʃpժծ6ђۨڤؠ֜ԘҔАΌ̈ʄȀ|xtoܸ3ذ֭թڝݣܦ٣ן՛ԗғЏ΋̇ʃ{w۷UϝִZ֞ؠ֜ԘҔАΌ̈ʄȀ|xtp߿l߻_ڴׯޯFݫۧ٣ן՛ԗғЏ΋̇ʃ{ws߿mҥ9͘.ھw֜ԘҔАΌ̈ʄȀ|xtpl߽h޺dsD"۶ۨ~٣ן՛ԗғЏ΋̇ʃ{wso߾k۷^ɑ+ӯbԙҔАΌ̈ʄȀ}wu`n%ڥ"֞՛ԗғЏ΋̇ʃ{wso߾k߽g޺dkժJےӕБΌ͈˃ǸӛRԗғЏ΋̇ʃ{wtVqѢ ӔЏ΋ˇ{ȁÀ ?@??(0` $ ?z"Žz37U !'--}0z4X[ %+287Ć(ƒ+.{1|4'`$*06=CIBǍ"ʼn%Ć(‚,/{2 A|ٰ׫F(`/5;AGNTZMʓȏ ƌ#ň&Å),W  &,׬ժԧ С 8`@FLRX_ekY͘˕ɒȎ!Ƌ$ć' %+17=֪ ӧ Ҥ ѡРsJ`QW]cipv|dПΛ̘˔ɑǍ"#)/5KXڱ-ٲٲОň&ň&ň&Ĉ&zƊ$?̀ ؓzMٮ!ܩiou{q8|8y8u9rbo|ٴ<ٲٲҤ |2) yx77~8{8xԆבֱ́6ٲٲڲ#*0~6777}<ѡ͙ɑɑ׭ڲ"(.4:Aħ56677gѡ͙ɑƊ&‚+ȑ339?ELRʯ555667ժϚ:̙ DJPW]cиq:5566U[agntҰ~|yvsqnkQ555ٟ(flrxԅ֋ʲroligda^\YTNuל(Ŀ{2x2$w}ԃՊאٖڜܢӞ閟b]ZWTR~aaث- &,ء.,~/z4@Ո׎ؔښρڼVר*آߧџ:qJuHrFnLlങX$*17=٧4ć'Ä*-}0ܽz4Ijۻ8ۮ &֫ѣʒ㝄aߣX4z2`#)/5EKQW]djٰӦ ϝ˔Ƌ$‚,z2`bgmsyӀԆ֌ؒQӥ ѢПΛ̘˔IPV\bhnu{ذӦ ϝ˔Ƌ$‚,z2`rx~Ԅ֊בٗ۝ܣY֫Ԩ ӥ ѡϞ͙ZagmsyӀԆ֌ذӦ ϝ˔Ƌ$‚,z2`ԃՉ׏ؕڜ؜ԔЎ͊Ў޿>׮իԧ Ҥ Сkrx~Ԅ֊אٗ۝رӦ ϝ˔Ƌ$‚,z2`֒ҋzW޴UwȀ˅fbцٸ׭ժӦ |ӃՉ׏ؕڛۡΈ׶_ժ Ӧ ϝ˔Ƌ$LJ'ۚإ<„81%!.߼/ڷuoر֬֎ؔגoضAקۤўʕآ $*0٩@ƒ++8ER߾6ٲٲtܦ:9ݯ  %+ңϝߤ")/5;Aک;ʼn%J\iv߿;ٲٲܽ1|ժ #)/6<ҤОߪ'-3:@FLR٫9ȏ iӀ֍טկ-ٲٲٱX!(.4:@GMӦР߭'8>EKQW]cڰ>˕vǃDŽ̉~ܺ2ٲٱX,39?EKQX^ը ѡ߰/IOU\bhntڵCΛΛ!ڼjҕ΋Ȁu޻Zָ2=CJPV\bio֫ ң޴9Z`gmsyԅܼGѡϝ͚Ѣ2ԬLΝ6NU[agmszӀ׭ Ҥ ޷Bkqx~ԄՊאٗNԧ ң РϜ͙̖_flrx~ԅ֋בدӥ ޺K|ӂՉ׏ؕڛܡۣV׭թӦ ҢПΜpw}ԃՉ׏ٕڜܢذԧ ޽T֍ؓٙԙٝӖ΍̇Ɂ[ذ֬Ԩ ӥ ТӁՈ֎ؔښٟ۠ԗΉݽGը ڻQ˃̋Ԛ֜ӖёΌ̇Ɂ|v۸1׮֫Ԩ ؓ؛ۢء՚ҔЏ͉ʄs֪)״Q՛ء֜ӖёΌ̇Ɂ|wq޾a۴خޫzܩڤןՙҔЏ͉ʄzu۸\Μ,عn֜ӖёΌ̇Ɂ|wq߿khݶGPZcU˖ɑƌ#ć($-6?թ Ҥ ѠϝkNhXajs|fП͚˕Ȑ+4=FPYҥРΜ̘̔dghqzԄ֍ٖwԨ ңϞ͙ENW`irѡțOh:c,ӂh֋ؓҁTٶ&׮ ذ֬ԧ Ѣ^gpzş[LS\Z:hTsީȢ4џעئПϞϞС׮իxӁՊؓQZbksa9p:kk{#5ܱ$د͛ȐȐȐѢ֋mݹ<ݪۏirzi8y9tlXkڶ=ٲҥ -g€,.!q78|iƅ۾\ҥ+ٱٳ(1:÷x66;qϞʒň&ϝ[9BKT6567ӛĉ R[dnҼ}yumB56ݙ"lu~Շ֒qiea]Yb筧aȊ.z3w+ԅ׎ؔ{ڷKڦ#ǡ'{NzJuLpӨ0&/8ʐ+ƒ+~/ؼz4E1 (ժ ͚IӶ={3h$-6?HRΙ'nj#ć'‚,}1n&/8A֪ ѡʓÄ+{2M=GPYbkѢ$˕ɐƋ$Ņ'-6?HR[֭ ѡʓĆ(y4gW`ir|ԅ֫!Ϟ͙ʔɏ GPYbktׯ ѡʓĆ({3hpzӃ֌ؕ۞شԧ Ѣϝ̘`ir|ԅ֎ׯ ѡʓĆ({3hՊؒՒэ͉͋΄ڷ֫Ӧ СzӃ֌ؕڛʂ׭ ѡʓŇ'Џ 10,ʕI?88ݿ8hر ֫Ԇaݴ+ߪџҚ%.9ć)9MYٲڸۦF>#,5ңա#,5?HMȐ#oԃxٲٲ!*3m⢶c(MGfKN;Hm 7)wspGλ;HY0UWrֲ*Onx~U+ `8t3h}FJNG{h=C;ECQY0v.9%/.3UFG:Z|dK ,飧aKZ7Ȕ +Hlo /[̉nC!bX y(+fpYQ J ,AXL701@0HFsPܜJɀ3HJB 'h, a0a]~?6߉ub 9E!Ze”E{9P-" &HeQؔО1>RzC얻B}ܰE-eP4lx1HN0,<-=(BA΂:]6wZ5j7 !\3'Bh搜" 8S= /X%ÌdnBw̕C X5W9/T@Zwf[ ~ܛ0Sײ˔"d4!c2:F#B@kտWs}eq0P(q\<4Rw(v'9Z~I8r9z"0M] %QRԟO7ohcAq'. \D:#҂0q")wK=nE!4eX,7nIu6eQ n$agK ~(Q*!1C! Cn̷9}zf@ &#c#- ;+82,4Ix Af. 3 WO>pȸL޻YnvpSCRZ"#`aGg$/uƝurJyjANRok6Oq[ڸ;i:'cz}$_Xw@ƃg +07ؼTY~H-ǖC2൉~=>#0eBW^āzwK'9F$|eˢcI+K؎16Ͻ`63) X$c-zq9۟Q ʳ*nޒ 8ļLTQO[N:v&ve4s{ZU+xU)/NMEGshVOIH_ aR=xgJV>068Ly)xR:N]1ܷč)ŶG~1Q;dzj&Ͽ;FѸ<.ޟON& X  ?o܃;лfNz]?殌w "%Ca]s3O^9'_D/X.>wn@ƂYkQvml߼,; K<ų^<4P>~'PDSi| 5|zpZaq>WH b*rԕFfҐ!b{^Qa:7d8-B3²p NdPL s J(ZDY@H\=RkK ̵onl Y#hQeK@ !ўJ@AVWZ'{ZJ #t~7~hO5y?X HHe!a\խƷbk4(X] ;|XrߓB̆PH! @\D#=œw%dC'CȪ7;'#8-2I-. e*CP !A*'0\ZH i"f½^|}սU koí^mqށ1Ka}NÈ\a 9id-KW=o|'q~X'uP.Z>.>׉!$HG @&v ؅kk8a8,s ɢF)Oxq655u`q!v!醋h_BBRJ͆&y;?]<>@#0.;JCկvFyLb0TshYY>5 8^}u~v Yp)> G/|(<&Kw3=r.hXl-: oN09a#rZ6f K }~>{B֕rϮ){ ~k gbQ;mwF(:?חzUtZ ĦqKexUY]KB9 &X?#;K&Cql?lxYb}BA[ | +|ǖ0 _9:xgf!`˩<NGٵI ԲPT͉˛ +[V8uI {%W-r~`3 -|ΔH,Z c@~dgCiu|?*hC" κs:_q1@ j# z+Xx|Bo\#pjPdPWu^a__=cr_2\K{m U-piwD0lW)aErhbpY +9pa܈g|W\W ܟ%;АYIOF_|əz!~%%tEXtdate:create2015-02-20T10:17:04-07:00A%tEXtdate:modify2015-02-20T10:17:04-07:00\tEXtSoftwarewww.inkscape.org<IENDB`ricochet-1.1.4/icons/ricochet.svg000066400000000000000000001356151300720305500167570ustar00rootroot00000000000000 image/svg+xmlricochet-1.1.4/icons/ricochet_icons.json000066400000000000000000000012551300720305500203140ustar00rootroot00000000000000{ "name": "ricochet_icons", "css_prefix_text": "icon-", "css_use_suffix": false, "hinting": true, "units_per_em": 1000, "ascent": 850, "glyphs": [ { "uid": "178053298e3e5b03551d754d4b9acd8b", "css": "doc-inv", "code": 59392, "src": "fontawesome" }, { "uid": "d10920db2e79c997c5e783279291970c", "css": "dot-3", "code": 59398, "src": "entypo" }, { "uid": "dbd39eb5a1d67beb54cfcb535e840e0f", "css": "plus-4", "code": 59408, "src": "iconic" }, { "uid": "26aba7edd46944209b4961670675a813", "css": "cog-1", "code": 59395, "src": "websymbols" } ] }ricochet-1.1.4/icons/ricochet_icons.ttf000066400000000000000000000121201300720305500201310ustar00rootroot00000000000000`OS/2>)ILVcmap#Dbcvt X fpgmYd pgaspPglyf& yheadP6hheaX$hmtxalocaY maxp  name}postR"TIprepk{zz1PfEd@RjZR\@ jYR-+7!!"&'467!;  / 4   |iQ'0.* -+'#'&''7&/5767'76?374&"26 xfey  xe fw VxTXtZdv  wddw  udcC`B-,ETX#B E #B #`B `aBB`+r+"Y-,+-,+-,+-,+-,+-,+-,+-,+-,+-, +-, +ETX#B E #B #`B `aBB`+r+"Y-,+- ,+-!,+-",+-#,+-$,+-%,+-&,+-',+-(, +-), <`-*, `` C#`C%a`)*!-+,*+**-,, G Ccb PX@`Yfc`#a8# UX G Ccb PX@`Yfc`#a8!Y--,ETX,*0"Y-., +ETX,*0"Y-/, 5`-0,Ecb PX@`Yfc+ Ccb PX@`Yfc+D>#8/*-1, < G Ccb PX@`Yfc`Ca8-2,.<-3, < G Ccb PX@`Yfc`CaCc8-4,% . G#B%IG#G#a Xb!Y#B3*-5,%%G#G#a C+e.# <8-6,%% .G#G#a #B C+ `PX @QX  &YBB# C #G#G#a#F`Cb PX@`Yfc` + a C`d#CadPXCaC`Y%b PX@`Yfca# &#Fa8#CF%CG#G#a` Cb PX@`Yfc`# +#C`+%a%b PX@`Yfc&a %`d#%`dPX!#!Y# &#Fa8Y-7, & .G#G#a#<8-8, #B F#G+#a8-9,%%G#G#aTX. <#!%%G#G#a %%G#G#a%%I%acc# Xb!Ycb PX@`Yfc`#.# <8#!Y-:, C .G#G#a ` `fb PX@`Yfc# <8-;,# .F%FRX ,5+# .F%FRX +-S,>+-T,>+-U,>+-V,@+-W,@+-X,@+-Y,@+-Z,C+-[,C+-\,C+-],C+-^,?+-_,?+-`,?+-a,?+-b,7+.++-c,7+;+-d,7+<+-e,7+=+-f,8+.++-g,8+;+-h,8+<+-i,8+=+-j,9+.++-k,9+;+-l,9+<+-m,9+=+-n,:+.++-o,:+;+-p,:+<+-q,:+=+-r, EX!#!YB+e$Px0-KRXYcpB*B*B*B *B *D$QX@XdD&QX@cTXDYYYY *Dricochet-1.1.4/packaging/000077500000000000000000000000001300720305500152345ustar00rootroot00000000000000ricochet-1.1.4/packaging/installer/000077500000000000000000000000001300720305500172315ustar00rootroot00000000000000ricochet-1.1.4/packaging/installer/SetupModern11.bmp000066400000000000000000001465361300720305500223570ustar00rootroot00000000000000BM^6(:(ɽǺĶѾѽѿ˼ͺƹŽɶ˶´Ų̹ʸþɵȵDZŲĮĭ­ۿڼ׹ع׸ַ̾ɵƳʾý̴IJӳ¹ͰʯūKLKLKLLNLNMMMMMMNKNLNLLNLNNNNNLNNNNNNNNNNNNNNNNNNNONNNNNOONYNYOOYOOOOYOOOOOOOOOOOOOOYY\t}}ttpppoXXWR\rskiJ55+5)-.)...--....////B/BB/BBBBBBKKMKKKKMMKKKMBKBKKKKKBKKKBKNLLKOKLLLLNKMMMMMMNMNNLNLLLNLNNNLNLNLNLNNNNNNNNNNNONONONONNONONNOOOOOOOOOOOOOOOOOOOOOY]vtttttpoXXXRRlsrk\C5555)5 . .---....////B/BBBBBBMBMBBKBBBK/M/M/KBBBBBBBBBKKBKKLKNKNLLLLMMMMMMMNKNKMLLLLLNNLNLNLNNNNNNNNNNNNNNNONNNNOONONOONYNYMYNOOOOOOOOOOOOOOOOOOO[ttttqtppXXWVVssklZ:965)5.)------.../////B/BBBBBBBBKKMKKMBMBMKMKMKMKKMBBKKBBKNKNKNKNLLLLLLLLLLLNLNLNNNNNLLNLNLNLNNNNNNNNNNNNNNNONONNNONONOOONOYNYOYOYOOOOOOOOOOOOOOOO]vttttpppoXXRkssrkU9565)55 .---....///B/BBBBBBKBMBKKKKKKKMKKKKKKKKKKKKKKKKKLKMLNKLLLLLLLLLLLLLLLLNLNNNLNLNNLNNNNNNNNNNNNNNNNNNNNNNONOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOYYsttttppoXXSWvsrlrU:555)5.-.--....////B/BBBBMBMBMKMMKKMMMKMMMMMMKMMMKKMKMKKNLKNKOKNLLLLLLLLLLLLNLLLLLNLNLNNLNLNNLNNNNNNNNNNNNNNNONONNONONONOOOOOOOOOOOOOOOOOOOOOMOOOOYrtttqpoodWSissskkT:5)5)5)-----...////B//BBBBBMBMKKKMMMKMKKKKKMKKKKMKMKKKLKKLKLKLLLLLLLLLLLLLLLNLNLNLNLNNLNLNNLNNNNNNNNNNNNONONNNNOONOOOOOOOOOOOOOOOOOOOOOOOOMOOOOOOOYsttqqpodXSVsvskleP565)53------..////BBBBBBBMBMBMMKMKMMMMMMMMKMMMMKMKKMKLKLKLLLLKOKOKNLLLLLLLLLLLLLLLNLNNLNLLNLNNLNLNNNNNNMNNNNNNNNONNNNOONOMOOOOOOOOOOOOOOOOMOMOMOOOOrtqqooddWSsssslkiP5)5))53 ----.../////BBBBBBMMMBKKKMKMKMBMKMKKKKMKMKKKKKKLLLLKLNKLKLLKLLLLLLNLNLNNNLLNLNNNNLLNLNNNNLNNNMONNNNNNNNNNONOMOOOOOOOOOOOOOOOOOOOOMOOMOMOMOOiqqqpddX\svvsklr\:5555 -----...-/.B/BBBBBBBBBBMMKKKKMMMMMMKMMKMKKMKKBKKKKKKKLKLLKLKLLLLLLLLKNKNKNKNNMNMKNKNNLLLLNNNLNNMMMNNNNNONONOMOOOOOOOOOOOOOOOOKOOOOOMOMOMMMMOOOkoqqdddkwvlsllilZ:)))- ) -- ..--...///BBBBBBBBBBMKKKMMBMMMKMMKMK/MBKKLKKLKLKKLKLKLLL9LLBLKNKNKLKLNKNKLNNLNKNNNNLLLNNMMCNNNNNOKOKNNNOOKOOKOKOOOOOOOOOOOOFMOMMOMMMOBOMOieeedWvvwlvlklkiT35)3) - -.---.././///BBB/BBBBBBBBKBKBKBKKBM/BMKBBKKKLJLJLKKLLLKLKLLKLKLKMKLLLKLLKNKNBNLNKNKNNNLLNNLNNNNMMMNOOONMMMOOMOOOOOKOOOKOOOOOOMMOMMMMMBOBMMMjeedowwvvlllkliiT-)) -  ----../////BBBBBBBBBBMBMBKKKKBKKKK/MBB9LKKLJLKLKLKKLKLLKLKLKKCLLBLLLKNKNKNMNKNKNMNKLNLLNNNMMMOMOKOKOOOOMOMOOKOOOOKOOOOOKOMMMOMMMOBOCMMMMOodokzwwwvvlllliiD)3 )- ----....////B/B/BBBBBBBBBKBKBKKBM/KBBBBKKJLJLJLJLKKLKLKLLKLKLLLBLLLBLLKLKLKNKNKNKNKNNMNNNLMMMNCMMOMOKOKOMMMOKOOKOOOOKOOOOOMOMMMMMMMBMBMBMMOidwwwwwmwllkjki\9)3  - ---....///B/BBBBBBBBBMBKKKBKKKBKKKBKKKKKJLJLKLKLKKKKKKLKLLBLLLBLLNKLLLLLKNKNMNKNNKMNKNMMCNNNNMMOMMOMMMOOMOOKOOOKOOKOOKOMOMMMMMMMBOBMBMBOTizxxwwwussllisiU5)) 3 - -----../////B/B/BBBBBBBBKBKKKBKKK/KBBKKJKLJLJLJLJLJLKLKKLKLLLBLLKKKLLBLLLLLLKNKNKNMMKNMNLNNNNNMOMMMMMMNCOMMMOKOKOOKOOKOOOBOMMMMMBOBBBMBBMBZzxxxmwuuslllijiD3 ) 3 . ---.../////BB/BBBBBBBMBMBKKKKBKBKBBKJJL9LJLJLJLKLKKKKLKKLKKLLKKCLBLLNBNBLKNKLMMMKMMMMMNNMMMMMCNKNMNMNMMMMMMOKOOKOOKOOBOOMMMMMMBCMBMBBBBKIxxxuxxuuuslljkli:)))) --..../////B///BBBBBBBBKKBKBKBKBBBKJKKLJLJLKLJLJLKLKKLKKKLKKLLLKLLBLLLLLLKNKNKMMMKMKMMKNCMMMKNMKNKNKNKMMMMOMKOOKOOKOOOBOMMMMMBMBMBBB7BBumxxxuxuwmuslliikU7))) .--..../////BBBBBBBBBBBBBBKBKBKBMBJKJKJKKKKKKKKKKKKJLJLKLJLKLKKLKKLKKLBNKMMKMKMKMMMMMMMNKNKMMMKMMMMMKMOMOMMKOKOKOKOOBMMMMMMMBMBMBBBBBBFmmmxxxuwwumllsiliiF))  ---....////////BBBBBBBBBBBBBBK/BBKJKKKKKKKKKKKKKKKKLKLJLKKLJLKKLKKLKKLKKLBNKMMKMKMMKMMKNKMMMKMKKKKNNKKKCOMMOKOKOKOKOOBOMMMMBMBBBBB7BBUmmmmxuxuuuuusjliiiiC))  - --...//////////BBBBBBBBBBB/BBBBKJKJKJKKKKKKKKKKKKKKKLJLLJLKLKKLKKLKKLKKMKMKMKMMKMMMKMKMKMKMKNLNNNBNKNKKKKKKKOKKOBOBOBMMMBBMBBBBBB7BhmhmmmxuuwwmmullkliiU3 &) ----....////BB/BBBBBBMBBBBBBBK/BBJKJKJLJKKKKKKKKKKKKKKKKKJLKLJLJKLKKLKKLKLKMMKMKNKMKMMKMKNKMMKKLBNBNBNBNKMMMKKKOKKOBOBMMBMMMBBBBBBBB7mhmmmmmxxuuwummljiliiF )) .--....///////B/BBBBBBMBMBBBBBBJKJKJJKKJKJLJKKKKKKKKKLJLJLJLKLLKKLBLLBLKKKKMMNBLKKKMNKNKKKCLKKNBLMKMBMKMCKKKKKKOBOBMMBMBMBBBBBBB7B/mmhmmhmxuxuuuusslljkjiC)))- .--...///.BB/BBBBBBMBBBBBKBKBBKJKJKKKKKKKJKKJLJKKKKKKKKLJLJL9LKLKLKLLLLLCKKMKMLLLCKKKKKLKLBLLBLBKKKMBMBLBMMKKKKKKKMBMBMBMBBBBB7B/7ZhmhmmmmxuuuuumuljkjiiZ3 ) ) - -----..////B/B/BBBBBBMBBMBBBBKBJJKJKJJJKJJKKJKJKKKKKKKKLJLJLJLLKLBLKKKKLBLLKKMKKLBLLKKLKLBLBLBLBKKCKBBKBNBMCKKKKKBKKBMBMBBBBBB/B/B7FhmhmhmmmxuumwuullliiliD)) )  ----...////BB/BBBBBBBMBBBBBKBBKKKJKKKKJLJJKKKKJJKJKKKKKKLJLJL9LKLKLKLKKLLBLKLKLKLKKLKLBLKKKKLBKKKBKKKKBBBBBBBBKKKKBKBBBBBBBBBB/7B.Bmhmhhmhmxmxuummmljliiii: ) ) --.--../////BB/BBBBBBBMBBBBBKBJJJJKKJJKJKKKKKJLJLKJKKKKKKKJLJLKL9LKKKLKKKLKLBKKLBLLBLKKKKKKKBBKBKBKCBCBBBBBBBBK7KBBBBBBBBBBBB7/B7B7hmhhmhmhmxuuuwuusllliliT3)) - .-....////BB//BBBBBMBMBBBKBBJKJKJKKLJKJJKKJKJKJJLJKJKKKKKKKKLJLJLKLBLKLKKLKLKLBLBLBKKBKJKKBLBCBBB9BBBBBBBBBBBBBBBBBBBB7BB/7B//..7ZhhhhmamhmxmuuummsjkjiiiC) ) --..-/../////BBBBBBBBBMBMBKKBK9KJKJJJLJKKKJKKKKJLJKJKKJKKKKKKKKLBLBLKLKKLBLBLBLBLKKKKKKKKCKBB9BBBBBBB9BB9B9BBBBBBBBBBBBB/B//B.B7.7FhmhhamhmaxuuuuuulljliliU3)) &  -0--...////B/////BBBBBBBBKBBJJJK9LJJJJJJJKJJJJKJKJLJJKKKKKKKKKKLKBLBLBLBLBKKLBLBKBKBCKKBBB9BBB99B99B9B9B9BBBB7BB/B/B/B.B7B.7/....ChhhhmahhhmmxmuuumjlijiiiD )  - .---...////BBBBBBBBBBMBMB/MJKK9LJJLJKKKKKJLJLJKJLJLKKKKKKKKKKKKKLKLKLBLKLNKKKLKKKLBLBBBBBBBBBBBB9B/B/B/BB7///B/BBB7BBBB.B.B..7.78hhhhhamhammuuuuuullllljjW:)) )) -.--....///BBBBBBBBBMBBBBBMB9K9KJJJJJJJJJJJJJJJJJJJJKJKJKJKJKKKKKKLBLKLBLBKBKKBKBBBBBBBB9BBB99`h`IE>8777B.BBBB////B//7/7.7.7...---hhhhahahmhmxmuummmljjjiiiD )  - --.....//7BBBBBBBBBBBBBBB99599J595J99999995999959595J5J9J99999999B9999999999999999//7B...7>mhhhamh`H@;40000...-...........-0-0&Ihaaaaaaahahmmmmmjjjijgib`&....---.-595555559)/)9//.99959599999995J5J99999999999999999/93/9/.7/..7...hahhhhaahhhha`H?;000.--------0-0a?&?aaaaaaaaaammmmmmajhh`i`b`:---55555559.999//699-9/9595959599595959999999999799/7//9//79..7...-Eahhahamhhhhamamhh`I4000------- 4`h`Haaaaaaaaaaaahmmmmjhjhi`i``3---5955J59.//-9/9///9///99599999599999J9999999999999997/93/.7/.---3hhahmaaaahahaahahahhh`<&000.-----&Ghaaaaaaaaaaaaaammmmmmmjhhi`i`UD....5559)9-/69//9-/699/69///959599999959599999999999979.9//.9..-7--Gahhaamamaahahhhhhhahaahh?1 000 'hahaaaaaaaaaaaahaammmmmjjjhgi`g`&/.-555599999-96/9999999996/9/99999999J9JJ59999999999997/7/-------7aahaahaaahaaaaaaaaaaaaaaahaH<'& &&8haaaaaaaaaaaaaaahammmmmjjjhgig``C..-9J9999999KK99K999999K9K9JKJJ9J9JJJJJJJJJJJ9B9B9B999999/79.9..-Gmhamaahahhaahahhhhamamamaaaahh?@&'`aaaaaahaaaaaaaahahhmmumujjlijiiig3)-....////////////JJJJJJJ9K6KKK9K9KKKK9KKKK6KJKJKKJKJLJKJKLJL9KJKBBBB9B99997/9-Chjamaamahhamaahahahahhhhhhmamahhhh`ahahahaaaahahaamamammuwmuulllliliiF ) ) ---...//B/BBBBBMBMMKKKKMJJJJJ9KK9KK6K9K9KK6KKK6K9KKKJKKJKJKJJKJJK9KKKBK9K9B99999/9/39`mamamhhhahhhhhhhhhhhhhhhhhhhhhhhahahahahahahaaaaaahahhammmwuusllklilii:)) ) -- ---.../B/BBBBBBBBKBKMBJJ9J9J99K99K9K99K9K9K9K9K9K9K9KKJKJLJJKKJK9K9K9K9B99999-93/-ChhahhahahhhahaaaaaaahahahahhhahhhahhahaahaaaaahahahahamammuumumljliliiiD ) )-- .--....////B/BBBBBBKKJ9J9JJ9K9K9K99V9K9K9K9KK9K9KKJJJJJJJJKJJ9K9K9K9B9B999/999/39ajamhhhahhaahaaaaaaaaaaaaaaaaaaaaaaaaaaahaaaaaaaaaaaaaaamahmumuulllliliiU3)) .-...///B/BBBBBBBKBBJJJJJ9J99K99K99K9K9K9KK9KKK9KKJKJKJKJJJKJKJK9K9B999997//-.-DjajahhhahahaaahaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaahhhhmmmuwmuulllilijiC) ) --.---..////B/BBBBBBBKMJ9J9JJJ9K99K9K9K9K9K9K9K9K9KJJJJJJJJJJJJJJJJBB9BB9/799/79-9hjhjaahahaaaaaa`aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaahahawuuuullljljkjU)) ) ---/..///B/BBBBBBKBBJJ9J9J9K9K9K9K9K99K9K9J9K9K9KKJKJJJJJJJJJJK9K9999999/.....Ghhhahhaaahaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaahhmammuuuulllkjiiii: ) - -.-//////BBBBBBBBBM9JJ99999999999999K9K9KJKK9KJJJJJJJ9J9JJJJ9B9999/9999.7..-7hhhahahaah`aaaaa`aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaahhhmumwmlllljljiiD)))  --.......//////BBBB995999999999999999999996K999K9J9J9JJJJ9J99999/97//.....--Gaahahaaaaa`a`aaaaaaaaaa`a`aaaaaaaaaaaaaaaaaaaaaaaaa_aaaaaaaamahmmumullgkgiigU3 -.......///////5JJ99999999999999999999999K9999JJJ99J5J99999999///.7..--Chhhahahaa`a`aaaaa`a`aa`aa`a`aaaaaaaa`aaaa`a`aaaaaaaaaaaaaaaaaaahmmummlllhljiib@&-.....////////955999999999999999999999996K99K96K9J9J5J999999-/7/...--.g`hahaaaaaaa`a`aa``aa`aa`aa`a`aa`aaaaa`a`aaaaaaaHaa_aaa_aaaaaaamahmumulljkhijgiF -......////////5J99999999//99999999999999999999999999JJ99995999..7..--Dahhahaa`a`a`a`aaaaaa`aa`aa`a`aa`aaaHa`aaaaaaaaHaaaaaa_aaaaaaaahaahjmumlllllijiii3&$-......../////95599///9996//99999999999999999999999995J59595559-.----`hahaaaaa`a`a`aaa```aa`aaaaa`a`aa`aaaaaaHaaa`aaaa_a_aaaa_a_a_aaaamammmmlllhgjgii`C-.//....//..5599//699-999///9//9999999999999999995995999595/..----Dghaaaaaaaa`a`aaHa`a``aaaHaHaaaaa`aaa_a_aa`a`aaa_aaaaaa_aaaaaaaaahahammumlllgligigU$.....//.-555599////-9-96/-96/999999999999999999995955555.-----7`haah`a`aIaa`a``aaa_aHaHaaaaaHaaaa`aaaaaaaa`a`aaaa_aaaaaa_a_a_aaaahhhmmmmmljlgiggg`7....../.5599/9//6999999999999999999999999999999595959./.7..--Ghahaaaaaaaa`aa``aaHa``aaaHaHaaaa_aaaa_aaHa`a`aaa_aaa_aa_a_aaaa_aaaaahammmlljlhliiggD$......./../9559)9-9/9-9/9999999999999999999999999959599553.----7hahaaaaa``aaa```a`aaaHa``aa_aHaaHaaHaa_a_aaaaa`aaaa_aa_aaaa_a_aaaaaahhmmmumuljhijggiG-.......///.5999999999999999999999999999K9K9999999999959959/..--`ajahahaaaaaa`aa``aa_`aaa`aH`aa`aa`aaa`a`aa`aaaaaa__a_a_aa_aa_a_aaaaaahhjmumllllliliii8)))  -/...//////B/B599/9/9///999-99/9999999999999999999999995J55553.--Chhahaaaa``a`aa`aa`aHaaHaHa`aaHaaHaa```a`a`aaa`a`a`aaaa_aa_aa_aaaaaaaahhhmmmumllkhkhiigC -....././////9.///9//69-9/99/99999999K99999999K99999599559555-.-`hahaaa`aa`aaa`a`aHa_a``a```aHaHa`aHa`a`````a`a`a`aaaHa_a_a_aa_a_a_aaaahhhjuumlllljligiU)-...///B////5996/9999999//99999999996K99999K96K999995595555553Djahaahaa`aaa`aa`a``aaHa``aa```aaHaaa```a`a`a`aaa`a``aaaaa_aa_aaaaaaaaahahmjmuumlllkhjiii8 -.....//////BB/M99999999999/69999999999999999K999999999999995555.3hhjaja`haaa`aaa`aa`aHaa```aHaaaa_aH```aa`````````a``a``a_aaaa_aa_a_aaaaaamajmuuullllkliiiC)) ---/./////BBBBBB99999///9999999999//99999999999999999999995555555Ujahh`jaaa`aaaaaa`a`a`a_a``aaHaaHaaaa`aHaa`a`a`a`a``a`aaaaa_aaaa_aaaaaaaaahmmmuumslllliliiU ) --.-././////BBBB99999/699999999999/69999999999999999999995995555-aajacahaaaaaaaaa`a`aHaa```aHa````aHaHa`aHa````````a``aHaa_aa_a_aa_aaHa_aaahahcmuulllhkligiU3) -.....//////559/9/99//////99//99999999999999999996/99555555.-Chhhacaaaaa`aa`aa```aHaa``aaHaHa``a```a`a`a`a`a`a``a`a```a_a_aaa_aaaaaaaaaahmmmmumllllgilgi7$  ----....//B//B9999/////69/699/699999//9999999999999999599555.3--Ghaj`caa`aaa`aaa``aHa````aHa``aHaHa`a``a`````````a````a`aa_aa_aa_a_a_a_aahhhhmuumlljlgliii:&) --..././//B/559)996//99999//99999996/9999999999999959555555---3`haac`aaa`aa`a`a``aaa```aaHa``a`aHaHa``aa`a`a`a``a`a````a_a_a_aaaaaaaaaaahhmhuumlllklgiigF)&  .--....//////99999999-99-9/699//9999999999//999999999595555-----Caj`haaa`aaa`aa``aHa```aHaHaHaHaHa`a```aHa``````a```a`a`aa_aa_aa_a_a_aaaahamjmmvllllhljiig) $ --.....///BB559595959/999/9//96////99999996//9//99995955555-3-- Ghaa`aaa`aaaa`aaHaa`a```aa`a`a`aHaHaHa`a`a`a`a``a`a``````aa_aa_aaaaaaaaahhhmmummlllkjiiig7 )--.//../////5959595////-9//69999/6999//999999/69//95955555.--- -3`haaaa`aa`a`aH`a_aa```aaHaHaHaHa`a`a`aHa``````a```a`a`aaHaaa_a_a_a_aaaaahhjjmulllhljjiiiC$)--..../////559595996/////9/9///9999996/9999999996//955555).--- -8gaaaaa`a`aaa`aHaaHa``aHaHaHaHaHaHaHaHa`a`a`a``a`a```````aHa_a_aaaaa_aahhhmhuumlllkhkgiiF )  --.....////B595959959/6///////699//9999999//99999/9/9555555-3-- -8aaa`a`aaaaHaaHa_aa``aHaaaaaaaaaa`aaaa``a````a`````a`a`a`aaaaaaa_a_aaaaahhmjmmumllljjjiiU&)  --.././//B/55995555959996///99/996///999996///99///955555-.- --Daaaa`a`a``a```a_aHa``aHaHaHaHaHa```````aa`a``a`a`a``a``aHaa_a_aaaaa_aaaahhmmmullljlkjiii3  -.--/.//////B55555959599)9//69-99-99/69///999/69///69955555.-3---Iaaa`a`a`aaa``a__a`````a`a`a`a`a```a```aHa``a````aHaaHaa_aa_a_a_a_a_aaaahhhhlumllllliljii8)$) -//////B/5959555555595559599-99/99996/99999996//9)955553.----3haaaa`aaaaHa```a_aaH``aHaHaHaHaHa`aHa`aHa`a``a`a`aa_aa_a_a_aa_aa_aa_aaaaahhmjmwlllljjiiiiC   ----../////BB555555959995999999/99/////9999//9//99999955555-----Dhaaaaa`a_aaa``a_a_a````a```a``a`````````````aHaHa_a_a_a_a_a__a_a_a_aaaaahhhhlmumvlllkliiiD))) -..///////BB555555555555.55555)9-96/96/99996/96/9)9-555.5..-----Iaaaaaaaaaaaaaa``aa_a````aa``a``a`aaaaaaaaaa`aaaaaa_aaaa_aaaaaaaaaaaa_aaaaahhjmmmllllijiiiT)-.-//////B - - ------------------------- - - aa`a_a__a_aH`H`H__HaHH`HaHHHaHH`H`HHHHHHHHHHHaH_HHa___________________aHaaaaaahmjjhhhgg`b`` ---------------------5------5).------5----- . -8a`a`a``____HaHaHaHaHaH``HaH`Ha`HaHaHaHaHaHaHaHa`aaH______a_____a__a_a_a_aaahhhjmlljjghig``U)-55.5555555555555555555-9595959599/9599.55555.3---- Uajaa`a`aaaaaaaHaHa_aaHaHa`a`aH``````````````````aHaa_a_a___a__a_aa_a_a_aaaaahjhwmljllijiiib8$--.-.../555).--).5-).5-.-.5.55555555559.9)959)9.5555).--- -`haaaa`a`a_a`aHa``__aHa``HaHaHaHaHaHa`````aHaHaHaaaHa_a_aaa__a_a__a_aa_aaaaahhhhmmmllhlgii`\8--/.. -- -----------...--...-5-5-55.).5...5.).. .. -- - D`h`a`aa_a_a````aHa__`H`HaHaHaHaHaHaHaHaHaHaHaHaHaHa_aHaHaHa_aHa__a_a_a__aaaaahhmjjljlgjggi`>.- ..-----------).)...)...55..555..55)5.5-).. . ---0``aaa`a`_aHa```HaHa__a`a__aHaHaH_aHaHaHaH`HaHaHaHaaHaa`aHaHaHaHaa__a__aa_aaahhhllljjjjgii`WD--.--5 -----------.-5.)5.).)...)-55)/).555..5.3--- --Chaa`a``a`aa```aHaHaH`H_____aHaHaHaHaHaHa````````aHaaHaHa_a_a_a__a_a_a__aaaaahhlmjljjjji`b`Q--- . 5 5- 5 .---- 5----.--5).5....-.5).5).5 5- --- - ``ha`a```aHa``HaHa_aH`a_a___aHaHa``````HaHaHaHaHa`aHa_a__a__a_a__a___a_a_aahhhhmlljjggiggUb--..-3---5 ....5-5..5.5555555...555555555555.5.)..-. ---8aaaa`a`a`a`a`a_a_aH`a_____aHaHaH`HaHaHaHaHaHaHaHaHaa___a_a_a__a_a_aa_a_aaahhhlmmljjlhggii`&.-----3.-55).)...).5-.......5555555555555555....3. .-- -Ihaaa`a``a`````__a_aHa_a___aHa```aH``````````````aHaa_a_a__a_a__a_a_a_a_aaahhmmmlljjkhig`b3---...-3.5 .5 5-55)5-.-55)55555555559)959555555555).-35- --7aaa`a`aaaaa``aa__aHa_____a_``aHaHa``````````````Ha__a__a_a_a_a_a__aa_aaaahhhjmllljljjibbb4$..--..-5)553.553.55555555555)555555555555555555555.55- --- - Gaaa`a`aHa`a``_a__aHa_______aHaHaHa````````````a`aa__a_a___a__a_a_a_a_aaaahhjmmmljljijggU:.-.....5--.)5)5)55)55)5)55555555555555555555555555555.35-----7aaaaaaaa`a`aaa_a_a_a_aa_aa_aHa_aHaHa``Ha``Ha``HaH__a___a_a_a_a__a_aa_aaahhhjmlmlljjjigib:&.-....---3.5555-....55555.).555555555555555555555555 55 -- - - Dha`aaa`aa``aHa___aHa______a_a__a_aHa`aHa`aHa`aHaaa_aaa_a_a_a_a___a_aa_aahhhmlljjljiiii`C.../.--. -. ..).).).).)-5.)55.)5-5-55555555.555.)5..-.---- --Iaa`aa```aaaaa_a_a___a__a____a____aHaHaHaHaHaHaHaHaHaHaHa___a_a_a_a_aaahahhmmllljjji`bb@---- 3--).). 5---------.---.-----------).5-5-.--).- . - - --``aa`a````Ha____Ha_______a________aHaHaHaHaHaHaHaa_a_aa_aa_a__a_a_a_aaahhhmjmjjjhg`i`UD--- - - - - - - -.) - - ----------.----.)---.- . . -  8aa`a`a`a`a_a__a_a__a__a_____a____HaHaHaHaHaHaH____a_________a____a_aaaahhmjljjijiig``D - - - . . 5 5--- .---.---------5..).5---5- . 5.-- . -  Caaa``HaHaa_a___________a_____a______a___a____aH_aHa__a_a_a___a_a_a_aaahhhmlhjjj`i`W`P- - - 5 3 . . - - .5 -5 5 --..-.5.)5-5.-55-55.5 5 . . . - - D``aaa``a_aa__a___a__a_____a______a___a____a___a_a__a_a__a_a__a__aaaahhamjlljjjjgi`bU.-- - 3 - . 3- .-3-3- 55 5-.5 5).5 5555.).5 5. 5.---5 . -  -E``a_a``a__a__a_______a_a___aHa____a________a___a_a_a_a__a_aa_aa_aaahhhmmljjjjiig``U.. - - -3 5 --- . .)5 .)..)-5--..5 .5)5.........-.. .. . . - 3`aaaa`a_aaaa_a_a_a_a______aHa___a____a_a__a_a_a_a__a__a_a_a_a_aaaaahahmmllljjjihWb`....------- 5 5 3----.--5..)5.--.3..5..-.5.........-.... .-- . - 7aaa``aaa__a_______________a_a____a______a_a_a__a_a_aa_a_a_aaa_aaaaamhmmmljjjjiibbU...... - - -)-- 3 )5 3 . .35-. . .-...-.........----. - . - 8aaaaa_a_aaa_________________________________________a_aa_a_aaaaaaaaammmmjjjibbb`_.-$  )     DaHaH___aH_____H_H__H_H_HHHH__H_H_H_H___H__H_H_H____________a__aaaaaahhaja```Q_QQ             AaHH___H____H_H_H_H_H_H_H_H_H_H_H___H_H__H__________________a_a_aaahamcaab___E_H  $              HaHa_________H__H_H_H_H_HHH_____H_H___H___H_____________a_____a_aaahhaaa`___QAE  )           0H`____H_H____HH_H_H_H_H_H_H_H_H___H_H__H_________a_a_a______a_aaahhmac`b_Q_QHA            4IH`_____aH____HH_H_H___H_H____H_H___HH_____________________a_aaaahhaac`___AQA                >aH_____H____HaH_H_H_H__H_H_H____H_H____________________a_____aaamhjc`b`_Q_EA $               HH`______H_H_H_H_H_H_H_______H_H_H_______________________a_aaaaahhaa``_QQ_AA            - >aH________HH_H_H_H_H_H_H_HHH_H_H_____________a_a_______a_a_aaaahjac`b___QH>               - &H`HaH_______H_H_H_HaH_H_H____H__H_________a___HaHa_a_aa_a_aaaaamaha``_Q_QE>            -   `aH___________H_H_HHHH_H_HHH__H_______Ha___HaHaHa_a_a__a_a_aaahhjac`b__QHE;   $     )       - EHaH`HaH_______H_H__HaH_______________aHaHaHaHa______a__a_aaaaamaaa``__QEE1      - -  ;aH_`HHaHaH______H_H__HHH_HH_____HaH_a____________________a`aaamaac`b_Q_HE1            -  3___H``H`HaHa_______H_HaHaHaH_HaHa_a____a_a____aHaaa_aHa_aaaaahmaja``__QQH0 $ &       --  `aHa`H`HaH`H__________HaHaHaHaHaHa___aHHaHaaaa``H`Ha_aaa`aaaamhjcabb__Q_E%  &         -  -- >`__HaH`H`HaH``HaH__________HaHaHa____aa```````aaa``aHaaaaahamamac``U__QE$         - - -  H_HaH`HaH`HaH`Ha_________a_a_aaa___aa````````I@80```aa`aaaaamajcj`b`_Q_E# &           0aHaH``HaHaHaH`HaHa___a_a_aa______a_```a`I@4 #0a`aa`aaaaahmmaca``__QQE   )          >aH``H`HaH`HaHaH__a__________a___a````I4#0`aa`aaaahahmajcabb__Q_A        H`HaH``H_aHH`HaH`Ha_______a___aaHa``A#8```aa`aaaamamaja``U_QQA            0HaHaHHa_H````HaHa___________aH``II #Cha`a`aahaacmjcc`b``Q_E> $&           EH`HaHH`EEH`HaH`H`H`aHa______a``>## & 0IhaaaGgghhjmajaa``Q_QQ4  $     H`HH>0>aH`H```aH`HaHa__aa`a8    AhIgG-0ghammjccbb`_Q_E'  ) )      038HaH`H`HaHa`aHa_a`a`0 &  IhG 0- Fhmjjcc`b_Q_QE&         0```HaHaHaHaH``a`aI#     CF0 - :jjjccb``_Q_E )        #'<<2%!#I``HaaHaHa`a```a@  ---.. -4=`jj``b_QQQE  $         1?_HaHaHaH<<#E`I`I`a`````a`a4&  00-000 --6=,@`cb``Q_EA$  )  )      0Ha_a_aHHaH___H#&8@E``aa`aa```0  0-- 0 . --=,,*@b`bQ_QQ4    )        >a____aHaHaHaH_H&038EIa``G    -  ---5=,+*Qb``_QQE'         EHa_a__aHaHaH_aHa1#0C4  0--- 0C@ID0.0-3<,,*``bQ_Q_E& )  )   Ha_a__aHaHaHa__aH0#    00 0 7FIj0 .-6=,**i`b__QQH   )      aHa_____aHHaHa__` 0-7C. -6=,+*h`bU_QQ@)          aHaa___a_a_aHa_HE Gh0 -0 --=,,*6b``_Q_Q;  ) )       &aHaHa______HaHa_2 gaA  . .3=,**@`b`Q_QE&   )       1a```Ha_____aH___& 8   IaI II80 -6=,**@b``_QQE  )       8IaHa``Ha____aHaH%0aaI0 - @ha8 8aaaaG 7---6=,+*Qb``Q_QE  )))    E``a``aHa____aHH_H&8`a@ >aaD `a`aaG -`D==+*)````Q_E>    )   )    EHaHaHaHaHa___aaHaH%8a83aaI8a`aa` -Dhj`=**(``W_QQE' )        IaaH``aHaH____aHa__H1A08IaII&Haaaa7-8hhjmg=*)b``_QQH    ) )      aHaaHaHaHaH`___aHaa__1&>Ia`aE8aaaa@ -Ihamjjh>4bbQ_QQA ) )  )   0HaHaH``HaHaH___aHa__`H1?`a`aa@H_aaI Emaamjhji``__Q_E4          3a`aHa`aaHaHH`___aHa__H&H``a`a`@1aaa`08aacmjjjaa`bU_QQQ&)          3```aHa`HaI``a__HaH__H1'__```a`8$Ha_a>0aaaajmajcbb``QQQQ$ )   )      4``aaH`_D>0A`H__aHaHa2#Haaa`aa11__a_'Iaamamjjac````Q_A@      )      8E833H_aHaHaH_%2H__a_HaHaHaHa2#?_a_aaH1'__a__aa_aaaamjjac`b__Q_QQ    ) )      H____HaHaH_HaH_2%<_a_aHH$%2HHaa__aHaaaaaajjccbb`_Q_QE>$  )       >Ha______aHaHaHaH>aHaH`HaHHa________aHH_HaH___a_a_acajc`b```Q_QQQE&  )  ?804?Ha_8;_aHaHaHa`a________HaHaH_______a`a`cibb``Q_U_QEE4$   a``H1EI```Ha&4___H`HaHHHa____H___H_H_____a__aa`cc````Q_QHEEEA$)H```4E`````E4__HaH`HaHaH______H_H_H_H_______a`c``___HHEEAEE0 Ea``A8aHaH`>&`__H``HH`HaH____H_H_H_H___H___a`a```_Q_EEHEEA?   &EH``H1HaH``&?`HHHaH`H`H____H_H_H__H_____````___HHAHE?E?0EI`HaaHaHHEHaHEHH`HH``____H_H__HH____a`a``_QHQEHEAE?A    A````&aH``80aH`H1H`HaHH_H______HH_____````____HHEEAEA&   AaH`0>HaH`?AH`H0A`H``HaH__H_H______a`a``__EHEAHAE?>     $ 8`IE_HaH8&HaHH1`HH`H`_HaH_H_____```____EHEHAEAD#     8E4a`Ia0>HaH&EH`HHH__H______a`c`__QHQHEEAE?;  ) 0&aIaH&8E@4HHaH_H_______```____HEHEAEAE    EIaH0&E`H_H_____a_a``_QHHEHEAAE?1   4a`a80E2_H________```___QHEEEAAE@$  &Ha`E1_>HH`H____a__a`___EHEHAEAAA%   Ea``&1H_4;HaH_______c`___QHEEEE?EA4 3aHa>1HaH&H_H_______a`___EHEHAEAAA?$4_H0'__H_1_H_______a`____HEHAHAAE?%30;H$'HaH_HH________``___AHA_AEA?A?1$__2!2__H_H?1_H_____a__b__Q__EHAE?E??    ?__a2____Ha1HaHa__aa`b``_Q_AQEEEAEA?0        '_aHaa__a__I18H___a````_b___Q_H_EEEAE8       H__aH____HaHA_a_a_a_c`b___Q_EQAEAEE> $     <_aHaa__HaH`H>____a_a`b____QQHEEEAE?  &H__aH____HaH14___a_ab__Q_A_A_AEAAAA1    ?a_aHa____H`>HA4_a_a``_____E_EHAEAAA4&  &`H__aH___H`H1a_A4__a``b___QHEAEAAAAAA   H_______HaHI1$_____a_a``___Q_EHEEEAAA@#  4aHa_____H`?H______a____QA_AEAHAAAA@1 #2<    H______`HH4_____c_aS_____EHHEAAAA?1#2HHHH#$4`H____H`1___________QHEHEAAAAAA?>HHHHHHH?024AA?HH)   H____H`?A____a_b______EHAEEAAAA@AHHHHHHH2&HaHaHaHa&   >H____H4____b_`___Q_AQHAEAAAAA@!HH#AHHH& #HHHHHHHHHH%&H_HaH```Ha&     ?___aHaH_______``_____AQHEEAAAE??0 'AHHHHHHHHHH&&&4_`HaHH&   1Ha___H______a```__QQHA_AEAAAAA?A1'HH?HHH``HaH_H___a_`_`___E_A_AAAAAAA>?1     1aH```aHa___`_```_Q_HE_AQAAAAA?E1AAHHHHHHHHHHHHH1 ?HH?HHHHHHHHHHHHHHHHHHHHHHHHH2 H``aHHaHaH``_HHHEAHAAAAAA>??1   %AHHHHHHH?A>1 AHHHH*&&HAAAAAAAA@' 'AHHHHH'$?AAAAA& ;# HAHHHHH030$!!!;AA=?A>A>>%          AHA; %HHHH#      AHAA ?H?HHH???>2  A#HHHHH   ???HHH;  %HH?HHHHHHHHHHHHHHHHHHHH?  &$"$"!!   1?<>1 A;AHHHHHHHHHHHHHHHHHHHHHH1  $$""!  % H;HH?HHHHHHHHHAHHHHHHHHHH  $$""" ?>1A;HHHH?HHAHHHHHHHHHHHHH_>  $$"""""!!  ?%H;AHHHHHHHHAHAHHHHHHHHHH'$"""!"!!  1?AHHH?HHAHHHHHHHHHHHHHH"!!!"!   'HHAHHHHHHHHHHHHHHHHHHH>!!!  #1'1>>AHHHHHHHHHHHHH_&!!!!!!! %##11??HHHHHH!    2HH?<%%%%144      AHHHHHH<2##%2= 5.4.0, for correct codesigning and macdeployqt behavior. set -e if [ ! -d .git ] || [ ! -f ricochet.pro ]; then echo "Must be run from source directory" exit 1 fi . .packagingrc if [ -z "$TOR_BINARY" ] || [ ! -f "$TOR_BINARY" ]; then echo "Missing TOR_BINARY: $TOR_BINARY" exit 1 fi rm -r build || true mkdir build cd build qmake CONFIG+=release .. make cp "$TOR_BINARY" ricochet.app/Contents/MacOS/ strip ricochet.app/Contents/MacOS/tor strip ricochet.app/Contents/MacOS/ricochet macdeployqt ricochet.app -qmldir=../src/ui/qml/ mv ricochet.app Ricochet.app # Code signing, if CODESIGN_ID is defined if [ ! -z "$CODESIGN_ID" ]; then codesign --verbose --sign "$CODESIGN_ID" --deep Ricochet.app fi hdiutil create Ricochet.dmg -srcfolder Ricochet.app -format UDZO -volname Ricochet echo "---------" otool -L Ricochet.app/Contents/MacOS/ricochet otool -L Ricochet.app/Contents/MacOS/tor codesign -vvvv -d Ricochet.app spctl -vvvv --assess --type execute Ricochet.app echo echo "Output: ./build/Ricochet.dmg" ricochet-1.1.4/packaging/rpm/000077500000000000000000000000001300720305500160325ustar00rootroot00000000000000ricochet-1.1.4/packaging/rpm/ricochet.spec000066400000000000000000000037051300720305500205130ustar00rootroot00000000000000Name: ricochet Version: 1.1.0 Release: 1%{?dist} Summary: Anonymous peer-to-peer instant messaging License: BSD URL: https://ricochet.im/ Source0: https://ricochet.im/releases/%{version}/ricochet-%{version}-src.tar.bz2 BuildRequires: openssl-devel BuildRequires: protobuf-compiler BuildRequires: protobuf-devel BuildRequires: qt5-qtbase-devel BuildRequires: qt5-qtbase-gui BuildRequires: qt5-qtdeclarative-devel BuildRequires: qt5-qtmultimedia-devel BuildRequires: qt5-qtquickcontrols BuildRequires: qt5-qttools-devel Requires: openssl-libs Requires: protobuf Requires: qt5-qtbase Requires: qt5-qtbase-gui Requires: qt5-qtdeclarative Requires: qt5-qtquickcontrols Requires: qt5-qtmultimedia Requires: tor %description Ricochet is an experiment with a different kind of instant messaging that doesn't trust anyone with your identity, your contact list, or your communications. * You can chat without exposing your identity (or IP address) to anyone * Nobody can discover who your contacts are or when you talk (metadata-free!) * There are no servers to compromise or operators to intimidate for your information * It's cross-platform and easy for non-technical users %prep %setup -q %build qmake-qt5 DEFINES+=RICOCHET_NO_PORTABLE CONFIG+=release make -f Makefile.Release %{?_smp_mflags} %install make -f Makefile.Release install INSTALL_ROOT=%{buildroot} install -m 0644 -D -p LICENSE %{buildroot}/%{_docdir}/%{name}/LICENSE install -m 0644 -D -p AUTHORS.md %{buildroot}/%{_docdir}/%{name}/AUTHORS.md install -m 0644 -D -p README.md %{buildroot}/%{_docdir}/%{name}/README.md %files /usr/bin/ricochet /usr/share/applications/ricochet.desktop /usr/share/icons/hicolor/48x48/apps/ricochet.png /usr/share/icons/hicolor/scalable/apps/ricochet.svg %docdir %{_docdir}/%{name} %doc %{_docdir}/%{name}/LICENSE %doc %{_docdir}/%{name}/AUTHORS.md %doc %{_docdir}/%{name}/README.md %changelog * Mon Jul 27 2015 Peter Ludikovsky 1.1.0-1 - Initial RPM Package ricochet-1.1.4/protobuf.pri000066400000000000000000000035331300720305500156700ustar00rootroot00000000000000# Qt qmake integration with Google Protocol Buffers compiler protoc # # To compile protocol buffers with qt qmake, specify PROTOS variable and # include this file # # Based on: # https://vilimpoc.org/blog/2013/06/09/using-google-protocol-buffers-with-qmake/ PROTOC = protoc unix { PKG_CONFIG = $$pkgConfigExecutable() !contains(QT_CONFIG, no-pkg-config) { CONFIG += link_pkgconfig PKGCONFIG += protobuf } else { # Some SDK builds (e.g. OS X 5.4.1) are no-pkg-config, so try to hack the linker flags in. QMAKE_LFLAGS += $$system($$PKG_CONFIG --libs protobuf) } gcc|clang { # Add -isystem for protobuf includes to suppress some loud compiler warnings in their headers PROTOBUF_CFLAGS = $$system($$PKG_CONFIG --cflags protobuf) PROTOBUF_CFLAGS ~= s/^(?!-I).*//g PROTOBUF_CFLAGS ~= s/^-I(.*)/-isystem \\1/g QMAKE_CXXFLAGS += $$PROTOBUF_CFLAGS } } win32 { isEmpty(PROTOBUFDIR):error(You must pass PROTOBUFDIR=path/to/protobuf to qmake on this platform) INCLUDEPATH += $${PROTOBUFDIR}/include LIBS += -L$${PROTOBUFDIR}/lib -lprotobuf contains(QMAKE_HOST.os,Windows):PROTOC = $${PROTOBUFDIR}/bin/protoc.exe } protobuf_decl.name = protobuf headers protobuf_decl.input = PROTOS protobuf_decl.output = ${QMAKE_FILE_IN_PATH}/${QMAKE_FILE_BASE}.pb.h protobuf_decl.commands = $$PROTOC --cpp_out=${QMAKE_FILE_IN_PATH} --proto_path=${QMAKE_FILE_IN_PATH} ${QMAKE_FILE_NAME} protobuf_decl.variable_out = HEADERS QMAKE_EXTRA_COMPILERS += protobuf_decl protobuf_impl.name = protobuf sources protobuf_impl.input = PROTOS protobuf_impl.output = ${QMAKE_FILE_IN_PATH}/${QMAKE_FILE_BASE}.pb.cc protobuf_impl.depends = ${QMAKE_FILE_IN_PATH}/${QMAKE_FILE_BASE}.pb.h protobuf_impl.commands = $$escape_expand(\n) protobuf_impl.variable_out = SOURCES QMAKE_EXTRA_COMPILERS += protobuf_impl ricochet-1.1.4/ricochet.pro000066400000000000000000000224701300720305500156370ustar00rootroot00000000000000# Ricochet - https://ricochet.im/ # Copyright (C) 2014, John Brooks # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # # * Neither the names of the copyright owners nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. lessThan(QT_MAJOR_VERSION,5)|lessThan(QT_MINOR_VERSION,1) { error("Qt 5.1 or greater is required. You can build your own, or get the SDK at https://qt-project.org/downloads") } TARGET = ricochet TEMPLATE = app QT += core gui network quick widgets multimedia CONFIG += c++11 VERSION = 1.1.4 # Use CONFIG+=no-hardened to disable compiler hardening options !CONFIG(no-hardened) { CONFIG += hardened include(hardened.pri) } # Pass DEFINES+=RICOCHET_NO_PORTABLE for a system-wide installation CONFIG(release,debug|release):DEFINES += QT_NO_DEBUG_OUTPUT QT_NO_WARNING_OUTPUT contains(DEFINES, RICOCHET_NO_PORTABLE) { unix:!macx { target.path = /usr/bin shortcut.path = /usr/share/applications shortcut.files = src/ricochet.desktop icon.path = /usr/share/icons/hicolor/48x48/apps/ icon.files = icons/ricochet.png scalable_icon.path = /usr/share/icons/hicolor/scalable/apps/ scalable_icon.files = icons/ricochet.svg INSTALLS += target shortcut icon scalable_icon exists(tor) { message(Adding bundled Tor to installations) bundletor.path = /usr/lib/ricochet/tor/ bundletor.files = tor/* INSTALLS += bundletor DEFINES += BUNDLED_TOR_PATH=\\\"/usr/lib/ricochet/tor/\\\" } } } macx { CONFIG += bundle force_debug_plist QT += macextras # Qt 5.4 introduces a bug that breaks QMAKE_INFO_PLIST when qmake has a relative path. # Work around by copying Info.plist directly. greaterThan(QT_MAJOR_VERSION,5)|greaterThan(QT_MINOR_VERSION,4) { QMAKE_INFO_PLIST = src/Info.plist } else:equals(QT_MAJOR_VERSION,5):lessThan(QT_MINOR_VERSION,4) { QMAKE_INFO_PLIST = src/Info.plist } else { CONFIG += no_plist QMAKE_POST_LINK += cp $${_PRO_FILE_PWD_}/src/Info.plist $${OUT_PWD}/$${TARGET}.app/Contents/; } exists(tor) { # Copy the entire tor/ directory, which should contain tor/tor (the binary itself) QMAKE_POST_LINK += cp -R $${_PRO_FILE_PWD_}/tor $${OUT_PWD}/$${TARGET}.app/Contents/MacOS/; } icons.files = icons/Ricochet.icns icons.path = Contents/Resources/ QMAKE_BUNDLE_DATA += icons } CONFIG += debug_and_release # Create a pdb for release builds as well, to enable debugging win32-msvc2008|win32-msvc2010 { QMAKE_CXXFLAGS_RELEASE += /Zi QMAKE_LFLAGS_RELEASE += /DEBUG /OPT:REF,ICF } INCLUDEPATH += src unix { !isEmpty(OPENSSLDIR) { INCLUDEPATH += $${OPENSSLDIR}/include LIBS += -L$${OPENSSLDIR}/lib -lcrypto } else:macx:!packagesExist(libcrypto) { # Fall back to the OS-provided 0.9.8 if no other libcrypto is present LIBS += -lcrypto } else { CONFIG += link_pkgconfig PKGCONFIG += libcrypto } } win32 { isEmpty(OPENSSLDIR):error(You must pass OPENSSLDIR=path/to/openssl to qmake on this platform) INCLUDEPATH += $${OPENSSLDIR}/include win32-g++ { LIBS += -L$${OPENSSLDIR}/lib -lcrypto } else { LIBS += -L$${OPENSSLDIR}/lib -llibeay32 } # required by openssl LIBS += -luser32 -lgdi32 -ladvapi32 } # Exclude unneeded plugins from static builds QTPLUGIN.playlistformats = - QTPLUGIN.imageformats = - QTPLUGIN.printsupport = - QTPLUGIN.mediaservice = - # Include Linux input plugins, which are missing by default, to provide complex input support. See issue #60. unix:!macx:QTPLUGIN.platforminputcontexts = composeplatforminputcontextplugin ibusplatforminputcontextplugin DEFINES += QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII SOURCES += src/main.cpp \ src/ui/MainWindow.cpp \ src/ui/ContactsModel.cpp \ src/tor/TorControl.cpp \ src/tor/TorControlSocket.cpp \ src/tor/TorControlCommand.cpp \ src/tor/ProtocolInfoCommand.cpp \ src/tor/AuthenticateCommand.cpp \ src/tor/SetConfCommand.cpp \ src/tor/AddOnionCommand.cpp \ src/utils/StringUtil.cpp \ src/core/ContactsManager.cpp \ src/core/ContactUser.cpp \ src/tor/GetConfCommand.cpp \ src/tor/HiddenService.cpp \ src/utils/CryptoKey.cpp \ src/utils/SecureRNG.cpp \ src/core/OutgoingContactRequest.cpp \ src/core/IncomingRequestManager.cpp \ src/core/ContactIDValidator.cpp \ src/core/UserIdentity.cpp \ src/core/IdentityManager.cpp \ src/core/ConversationModel.cpp \ src/tor/TorProcess.cpp \ src/tor/TorManager.cpp \ src/tor/TorSocket.cpp \ src/ui/LinkedText.cpp \ src/utils/Settings.cpp \ src/utils/PendingOperation.cpp \ src/ui/LanguagesModel.cpp HEADERS += src/ui/MainWindow.h \ src/ui/ContactsModel.h \ src/tor/TorControl.h \ src/tor/TorControlSocket.h \ src/tor/TorControlCommand.h \ src/tor/ProtocolInfoCommand.h \ src/tor/AuthenticateCommand.h \ src/tor/SetConfCommand.h \ src/tor/AddOnionCommand.h \ src/utils/StringUtil.h \ src/core/ContactsManager.h \ src/core/ContactUser.h \ src/tor/GetConfCommand.h \ src/tor/HiddenService.h \ src/utils/CryptoKey.h \ src/utils/SecureRNG.h \ src/core/OutgoingContactRequest.h \ src/core/IncomingRequestManager.h \ src/core/ContactIDValidator.h \ src/core/UserIdentity.h \ src/core/IdentityManager.h \ src/core/ConversationModel.h \ src/tor/TorProcess.h \ src/tor/TorProcess_p.h \ src/tor/TorManager.h \ src/tor/TorSocket.h \ src/ui/LinkedText.h \ src/utils/Settings.h \ src/utils/PendingOperation.h \ src/ui/LanguagesModel.h SOURCES += src/protocol/Channel.cpp \ src/protocol/ControlChannel.cpp \ src/protocol/Connection.cpp \ src/protocol/OutboundConnector.cpp \ src/protocol/AuthHiddenServiceChannel.cpp \ src/protocol/ChatChannel.cpp \ src/protocol/ContactRequestChannel.cpp HEADERS += src/protocol/Channel.h \ src/protocol/Channel_p.h \ src/protocol/ControlChannel.h \ src/protocol/Connection.h \ src/protocol/Connection_p.h \ src/protocol/OutboundConnector.h \ src/protocol/AuthHiddenServiceChannel.h \ src/protocol/ChatChannel.h \ src/protocol/ContactRequestChannel.h include(protobuf.pri) PROTOS += src/protocol/ControlChannel.proto \ src/protocol/AuthHiddenService.proto \ src/protocol/ChatChannel.proto \ src/protocol/ContactRequestChannel.proto # QML RESOURCES += src/ui/qml/qml.qrc \ icons/icons.qrc \ sounds/sounds.qrc win32:RC_ICONS = icons/ricochet.ico OTHER_FILES += src/ui/qml/* lupdate_only { SOURCES += src/ui/qml/*.qml } # Translations TRANSLATIONS += \ translation/ricochet_en.ts \ translation/ricochet_it.ts \ translation/ricochet_es.ts \ translation/ricochet_da.ts \ translation/ricochet_pl.ts \ translation/ricochet_pt_BR.ts \ translation/ricochet_de.ts \ translation/ricochet_bg.ts \ translation/ricochet_cs.ts \ translation/ricochet_fi.ts \ translation/ricochet_fr.ts \ translation/ricochet_ru.ts \ translation/ricochet_uk.ts \ translation/ricochet_tr.ts \ translation/ricochet_nl_NL.ts \ translation/ricochet_fil_PH.ts \ translation/ricochet_sv.ts \ translation/ricochet_he.ts \ translation/ricochet_sl.ts \ translation/ricochet_zh.ts \ translation/ricochet_et_EE.ts \ translation/ricochet_it_IT.ts \ translation/ricochet_nb.ts \ translation/ricochet_pt_PT.ts \ translation/ricochet_sq.ts \ translation/ricochet_zh_HK.ts isEmpty(QMAKE_LRELEASE) { contains(QMAKE_HOST.os,Windows):QMAKE_LRELEASE = $$[QT_INSTALL_BINS]/lrelease.exe else:QMAKE_LRELEASE = $$[QT_INSTALL_BINS]/lrelease } updateqm.input = TRANSLATIONS updateqm.output = ${QMAKE_FILE_PATH}/${QMAKE_FILE_BASE}.qm updateqm.commands = $$QMAKE_LRELEASE ${QMAKE_FILE_IN} -qm ${QMAKE_FILE_PATH}/${QMAKE_FILE_BASE}.qm updateqm.CONFIG += no_link target_predeps QMAKE_EXTRA_COMPILERS += updateqm RESOURCES += translation/embedded.qrc ricochet-1.1.4/ricochetscreen.png000066400000000000000000001532061300720305500170250ustar00rootroot00000000000000PNG  IHDR; sBIT|dtEXtSoftwaregnome-screenshot> IDATxuX)EDPTb݈=fϘ5{LtvfmgwwbPḤs]\y'y_>o*5B!B$B!Y݆ PT<}XAVKKPdpB!"}xby%YիU*/>Ɔ9sG||G ( .LE!B$N$^\E!BV137Dj ,-޵ ^^'88#ekÇ,_+W^5>@nGG&CbV{>VZ-}Bsy,_Nx8 0cdN9˘> !=DMP`q/T'nWE!gAP'''Zl{R,7nh*/ǹ غ};d̚lxM7bvwY-;[[lmm9tիVC;ׯ3y,{Ț5 >ubkk˸&uvLL)[4[mɓ^ !F(ߴj (@Fv%YBdzno$N=Gin~y…P/]bq^:vsq4j5\eithۖ6֝{2;>}~})Z&&-v`EtIepiJ(Aҥ9~$.^JiB0컑/>j4/STE!DpqK776o/VHR[~>2VY2VQmJ(蘋˖Rj\EZ 5IT*T ONT*dpm=~[_GȓljW&yLEJ !6S'M$CFdȈ< Ņ&]9(]iۺ;v͛$޻3g);{{{LMMpw܅ xWbw#fRmz묯CN>M^}{kx@.)X `՚\ ![ ~(#:i" ]9(y7%KulZfiy3NNUGЬlNqڶjIg 6o)ksS*Q;[[sѤa *B ѧ$@ț/:wRH萄BOFEY 1\&DZmO+\2{QV+P(?)aoq/:%j*L0QqB!D&bihZ]>z,۶g̙W^YVm9{֘5#0(BݫW|2W=!\WR(y>Ʊ;@ 'L$a J@*+z7ӼVqB!D&/ZfʌXU*3kTzt̋@8RqTt4T"y?lꙥt݄1OrC@-X^i^H{UE!nlE /AY橝F=!ћ,ݰV͛iŷLLLգ]{,Q]}8?>{vW)ݴU[LOb֍a<}J}Nl`gqQO?]Ƌa]t9`+r*<1 | m:B!3YU*yʗKqc\ϗRDqT>g8;k7{~UG}ug͝Gdd$fܜ񓦰|կ 'kn=pUQh.T$VOo9{Ѱr>,Rӭ:e6! 9WXR%JP^]v˰0r 9z_/!WΜԫÚJõ5OMT la ,T_ԯϸ^`H1zqf| M+g/%Q#&$"]- y6)v-DW ktT*4|ݻtfM+[ǜ~&CF|GR%]LjUiݢ9J ^l'#jY,,vp1ߍ0(>CBt͙P^pH0JOq.>N<=uCB]bV-vƩ&"$K{ ](QřzQH5Υkz˜ ht11Đ?_>}Q]5sUDbܽw?I;rciܔ{QJe-pʝfMp2ECBtɚ5+ypE(l}XXS-zH֬YQ(lٸ,Yk_"HݩHLRCW+Эs'JWzSgL##= ɖB#ȚOuB!>-(|4]:QÃ`l;wQEyx9bbgѯ a[CvZ[naGT&xw꤉h֌Ky|ۯ&o9!!;QzU\ĄPFuu>Ϟ?giܹsḺ0o $m/OjUUՅ$LVac*zyrY""#(p/%QTpR8HpH]]i:k 0w| bEyKvٿBpusS=%NAM&l9ol]NycBF=QW(Bd6f>ܹҥ/R^^%}"qqq( Lz-H) /-QԷNsstKҳm!DdQCAIB$B|I%aBa,I&Q!1"!2J$arciC, !B N0rhCNݘh);q>Xk~_ǷFrIj[ӧQw~ԀB! :  JLq"/Xt93ժ?QAkmms4siTB!" :wAll:x ŋZJ6CG%6ԩUR B_]SSSLMMb B!gБ_₉BYVؽ/^iݓ=zevV7""vš3B!,q'?K^g^`kkÀ9guj0*mummm:pvv6O!BAթMLl :si3ק.vvI֮YR%s2Jŋ@O||BR7...5B!`БE;;[~5_-e PF5uB`~tf#~ x|QؐB!GR-^\}ڵCKҳgOóH=y(iC-gjP,66uCptjU(lB.L߉5euaϟK*!~EtqqJQp|OҟZ&.Nipy]s㩚G3 *ov{̌i]Zګ͟ȧe| G#0$BTicc9{?ddf|ö-ݛ RknI[9$}c[6kPvl9J^Y9{"{b^OqՎpnh!DXY Kus]¼xDW7umb'R7B&i,de%el_(˟+k?c9m'y\e>Dß1sW%)[tn]>-!D1y<.ߎ-Wh[Iiّ5{Bsq&,]g߭z Cd6II26Vg9\Ήe={7Y1sW',$הR;{ ~ٵa5݆DJfj8M;TׄyrnZwP/YZ#%O!>NGOsИ<.XM3~UByچd{xHzq&rElsпE$vdjf=el mO*mTmfpARoOXO50?ؖ SSS{XQY[e = ך)B|-|7פM?s nh!҆Ex!U7_X|,3:TS%$nl: &B|n4~JXft(Bhu1114h%C?Ke2WQV.G-dC~sǾMS'-DJT❧0Ȓѡa1CDΛHU Z\vB|zflaa]oBORs°w'4bMc^@:" S(Uxh. !B=x^xR]>]Y`DZWj̑|Im7K̝i׾-hE2,4"[ ̝B+O7z80S$oDqIL5Z^9CB!Ȍ?s``3Mr7k)&oEƿ& .e28e,5ca:##x:LB:'̭uKJTD5e*kLνe|Ӭ*N#k6+ NdkCB!ˑE4"l2h,ud׆t2*i>}̊r8a!D|7djIRytmšj#O!ۿB!"],~aSVoVƽwX"9ߢKګ7٬mO֟g+ XZYP({7; T}fX˜B!>rdŲ0VfvZ[2"$o45[զY޼zˠ=P(:I+TisrȾ$yЇw|P2!-],^:2K̎:zblL42w^̌, Bu+y:Vrڸ|o>?@lt4Zt֎{77Gwo&K޾9r<{ gCB!ۻs4$ "0&MY\, |[/9wț-Τ%w`,=S^conMě-]_6mb2;pOde |K4j/ЮNɍgHφ/B9R͗OCۖҳgόC!`ɖխrZ!B;H(B!dQ!Bh%ɢB!JE!B$B!B+IB!V, !B@?+ħtCЗBrapY9(B!dQ!Bh%ɢB!JE!B$B!B+IB!V, !B$YB!ZI(B!28Y|ȱhܢ m;ucDGGpIj4JM[K[k~_ǷF;wb.eL29um۴fĉ$$$7cOXvɴl'Q\Y\)W4C&000cK-ci1)313P7 <**2lj c儆2~HX<c\<}Lr}L :jT[|I}f9 _"""ss>gl֑ Z۷trJ*űcGC+Q|ygmm?XbwԬO11dnӢ·}=d|>lll2t)͛мE &O ?}6j݆ǍU:s~A L:7DEE1ٽ{ eE_[,seUԮS'S\Y.]ؾ=1/\5߻;ȆQոKt2иaCΜ;OlvٽwYfW_1iDfX_izZ|v-~3<7gd͚/_d2 8׼~&$$p=M[fѿ_?nܼ$ ׷mڵKf׏7nbjjwLm[7 7nܠy5c…$(]GϭNϯ{ 1Crm ,c9| -͍e˖ҬiN:En;w_еOV2AԫS&Mk9u,fO]lts^g$j[RxOH5SDqvڣn\\\/|,^Lb~?{! Px TY_xqʔ))[7n$iXɓݘ'$鯿 d$o޼88}޵0׿9fixZ| \ tؑɾK.]h׾=Ŋѽ׼ ŋz>""ŋ1o&^+booOŊ?z\Z oJˡχ ޗ}S:|8eԦMV{J^zůW7sʗ'G|Gɒ%YlY~ '?fqe,h)#NƆZ5ѭsTwdozHB`~tֺϡS"C}p=ST*y1}߿RIK=|0~(V׭,X ͛5e/ hӶmT*ޕ*i) *U͛%e3vK +#->NJq]ʕZQݑlyV-R;#R]!f4mڌy?LM?`F!!!}hڬ+Wё>}zmOP BGXZZ!pdQVabY}##6}s,cƓc9bŘ7r8d"'MNqB4uiүRA#ƖYRB+^~4{6O>ƍ1q  +>>ׯNnrauxِg4G%K>{NJ`bbJttκiM[,{7 :u2-C5sk>Ny$9(XBٳg,?s4E=< KZIZ}i5=sÊ˹pAAA\r333YZBիWwN8'O32jHbbbh٪5|թ3C fΝq-n߾+##dVyȟd1=~Gٶu+Waw7/^MɓǏ9{ ˗/7h>\\\uyGϿ.bE;::ۀFرcp χskaC)J8[p}߿ի1:wϧޞbİ\vp~;kWҳW/~SO뱩T*Q/H+f+ !͂سg7gիW(PeW`#Y6u ӧO N>SLnێm}?N Ǎ#0 C)Roep[nKYW̞5|Xvfc_+W.zC]5&OʀRn ЪU+rshܤ 7nFj)R;w۳lŢ=g~C_bmmMڵ)\8oORoښӦE=bB!!7!BL4B!L>w !B_yKB!t$YB!ZI(B!2T! B^($7n؀{gPd)KHHo Rxs:pԩO[>W81ju5G&i-mޟqkۆ%[j s:h^~%y}6J,7|x?%3sS"3P-]Y~01cǒ+W .Wܹѡh^1AVҴM:b[|ظ}|sTϟ> Rz S21ɤ۷/*THsyaÆ["ѣGPPprrs\ |Pb"!88ׯi)J;Jطoo ;;O',޽{Ǐ~z֮]Ç۶m[vܙB "#uҕnnLqwaٲ̘9 S3 JS&SѳӤq#Ν=O:O:uX"=Jn…ԭC~WJ/&$9Us.!!fϦwE U:C)YŊaoy},,_BܸpB> ic͘qcN<~lж7G>pg߿OзW?f{HawOgtOn]9vmZP7*{WơkuqquާO­@:u̱LjҬ;tu{2Ν;C~W*Tl+нSdnܸS~:_͛7ܹsӧOs ڰ1ɏ<R+SSSNƺkGwڷ@r4;͛6pbΝ@jhִIk T*ҹ>}9z8kW^3gq9ff޵+ŶL̺3gϜ=}o޻Ǐsm&MhTc…v-K;^}}ok2|*WBPH(A!gж7GܼEKݻǍ5oF2eȗ?8 )o\rϟR8?Ϟ>@AÆz ݽ{7a'NzJu[?@XXkׯgC;zAM۶p2e2h@My}е^[wOZ)[6=7ќ:yuj'L׮ؽg/ޕ߯ mx?4oΝ;?d뢣92e 0i$"##5k֬ŋi֬5jw0VXA&M]6 " bBoJ|es^<c5_zůW7sʗ'G|Gɒ%YlY"::Bps+@%Rf})S NNNRlFDDx"fībEX~ٲy3~~3ɛ7/9iߡw4V[具w%׏62d2GBJ&MMmy󔏊+;I.]Ɔ*UjN={XWwo6h@UpvvCǎ \!}}5%3wݝ RJJ,IVeV`ooĉ`XXX0tP͛ȑ#5=zĤIHHHCZjhт8,X@=ؼy3" .Sk 9s4SZdܿ2O//}Em(CL-C!so-[s3v,9sEKhmOW؟ ⾽{ԩ3/Nypz;]Yf5h @fmgcՔܹSy@w?P[;;>rr#M2ScFV9t D|lw8O)]Y|{^C]k~kBBBطoF"wӤI9~ (Ue˖MA .#yeܸq$$$wo WKf &Q9x0iִ SLO55+WsԬU+6bcbXYYYP(xA< "086>?F2&M3.^-[IkX:˧ԠA>Lll,{/p1bbbطw 44xҷWSNs>}HԔ:ur~Oժ4ըY`nݼCSIm)K+~ː{;wNg}xyyŔ)S 1~'ɒ% ]W(XB.?s4E;s%K>{jTŊgŬ?͞ӧOq:AAA~E=<Һ(S' /~7LMM9rp;%)]OcBC92dԩ[-7y&s?P+++O?~ "Og֯_ǽ{Wcmo;ԇs1Ðø WDkkkTիwk0MCuj%Ytqq[&O oƭRۧ۶nwڵ f=IEH<z*ʕ/2} ƛK?KKK͹ț7/&&&>}:mݻwɟtBeOΝ)W4GfINEN2pTСCȑÞ9s^pP{W)W4keW`#̜5ٳfR˓'۠fϙ;-IE lܰAu?NL?neJOϥ4o~OOc4nO//jTFb5G٧~}Q&EWޟjа!AAA,Wχ|9m̘uIi[K80(.\8σtkgg7y)X`u'!!S`ضNi3 k!Ҽ7nxG%˗Xb:ۗ%JзoO62zhʕ+9v֬YI&%xaÆԪU.]T*Yf c֭X[[vBlҥ30/3:tJ1sVZu6i^D\<ߟr!iN̵Xb3? ...8:: C ʊ9sB|hڴio)k׮DDD;fP+vHLl#IQhZSwEե6!"VMn${s>sW>9ԩS(Hȑ#Gzᥤٳ!V%uƪXˎy&7eʔL2OTT|*>p{ T(WO>;w$[N5r$7o±@~ \HH2Ls!,_ 6qQ<8~5>}]zŊʼnma\ M7"9黡EDu]LoN-OO<͛<'fϙK5S֬^ʹdW_?Q k{3TX 7-9u&&<} Z8rĩ#{$444E]td T\gMΜ9;"#" ǵ lml-ۢEK.]ݻ{B(\pLg͓V-[0l\9r`08u,BøF蕫^6_O.Zic~ȳEBHPӢ[ndR v<6}6ʕ/o޼yϏ `|Z?rӌuf͚ /$i*TȤ)S=Oƍ˅ ˭tilllؼySʋ/-֮MVl66ܿ5jdʔSD]JE$Cptt$0 ƍz*WK./ӷw/۷fxO7LvǬYݳ[$Ί+2<΅cvMfOƮ] c߾ّ=[Dʛ7/ҕ>۷s VXau/ҕ^=?aҥܸqGpQbsqqȑ#9s:ΝQq狒E0Ǝ/iGMΙ1-FvJUXv-K-ﴲDGGS^}̚: ۷]^z/_^ (o[WXb9]_BUo8m2l͛5ݩ ԟXCбSgO[_ΪؚhGZ׻6?hfQye(Wscԩ#H7W]Ot{%KSMɄwm>';dI_z ʲ:8w'o.I\D$>sPP~~=iD$"9rw4Iz}{PddEy͙=rʕޡ$\Hw͍cǎ2p:uLΜ9*ImJEͼ?dQUvŔ)9| yrԦdQDi:r4ðZ֭iպuEDR^'""""`HDDDD$sEDDD"%""""bYt͢XGS+DD$aּ"5>Yɠ/JEDDD2$iqFɢHgMV(YJzp%"ϠMgK|IaZ8dQhQDhr40a~?#P ȳ`|OQd6-,H()T(""ec0LHfgM2QDDC䒒*a1;I$JED$)lluZ$JN NҗdJDDDD$""""bѨkEDDD$~ztXLQDDDD,sEDDD",E6XKt7Xdcљh̦W& 'ϟ9 'Ϟd2Ѳk̳~?לl ^nszkSͅ~M0a>~1ߣ9g>vU{mݹZ-me3884I|椛_5vJ.cl, Vq}Z?#Awd:u7g[r?3u2 [V/ܼvfϥX"0c >y?#zwh䣘kYd}mC/W]J.}^ȞSY[J}|~pp!wdzASFE2.)U[W/3/۷m#EJҲKvm &"lx5lb~Q}ߦUUgZbxܻs;Ub_#βwG835^ݓφe x~rRm8G@JUUgy)ciS%άK/@TdU4-YFfSO\s~nҜcg:| 8tG/WkwZW+b>]ʗ;ĩ#xӿNZ>6 |>nq %sisnܨdv:5og43u<6Bc\̋SGN-?GkqXfNBgrGWKc0)᷉#S.α{Ri eqR׭{ Kd{!&|̚, ."wl]_e2<PZ-r:柍kޭlO^3L]_̱CLY؊cwqڭkvN$HۂWP7u~%]fʒT_ @MF1?6ψaW,a\|{wZ/|[gwh%Qr$olX~ß;Cz+sOCo[/u œM6w1@h[4hގw .g㦙?qڛ6j fNc8s'gGkqSeqӘohu`Rƃ_N =ɝHJ՘7}U$S$F&%.&qөY׏|\ˇH>&E;a<}Gr-PH1z%heA Ud՗"q?P+nPͻ>vyckE2ս吇냨XݓK!\`q—c2<]\)0bKZv%4nA"l ^N%h B6;×x\ BGdMܸ۷Qy-_bތ0n@Lt)yq"X3S1T5/3 T]/x.ߟh3>}'I/N̹SLj|UDM>ZvtT$ީfrƷ]P^bM dחr˫c{]X6g&z7֦ոx$vdL6x5l5غf4>KV<6ƕPN9r`0m֟a6\4bЊVUof޼ȈlZVi167y=g.L-'U/ 9vuKn6{66&&r/?m0Oj8kJ"#"0D'^Zkh-ONʥٳEsKɮ/-+fڍC|߶Sn\BgeԪ`^=g.i_r(5wQsRڲcݪ~CRBTE9Y5BNQPЩLɣkxu>]/Q\%r+] ;Sӗ~|{ :㱱5?F'>KƆ[[R4;6Yo&JdU Iݟhq|')qt` g>&8*R5i1'5e٬{6~;&ĎZ2V0s.Kq\D21\ʦUl̻qs<=qȓϼorCQb9'%c?]q5,MmִLoۅޝky5%vF2"#׌8{;.sl.lȚ-{獍nqɴr:䡊/N., qNA?q4y5&ME^v<Ɗod z QO|yP&}i li¼_3n\ 36ptr)4k:.-X~vܹyϒ&o">ze5ωSonܜ/Gqv_ŔEͧI6ewRC}GM'%{umb.9gkJ1ײ|ԾK_(V6ލG_JĎؘhSǭWBٴr֊kg|si5.'nMK8DSNGV)i7>5Sj'sj- 1}.THq]dM7$d2r t ڽ.1:;/ ~~q3tZD#3]pKH$#ɖ#g.'U+3=9GDΎ?=g=.;C;s+˙cnpuF;4s~NޡH:C,û3gxNOo"aDGj]z"ƠwC-;ytHFcgoϢ#{{ADDDD2(fEDDD;ɠlllnDAHhSͅ~M0DD$6EDDDacdQDDDD,1(Yɔ[I@F BL#sYttSwiK;n+_gtrſcצ`zv Ln:c#1۴L B ՝{wnw#yݿF~܍v%0{Mw:1Ww f3/JnlYԪaڨtr%Lnnv""46z۟HPǿ=?VRIs7?a߳?Wn;E ߶Ϟ2c4ruF+៍k ;teӔRa=0?׹| Sn㗵8s|o`+ŕ5xs)-xu[~%]fʒT_h{<ʗ_Sr $Qp"STpŭbyέ,e OBuOr+RR5Ms-KTOJo< Ϟ"^bs=bEɓ M;vcÊ.Zbe3G${.i5ؿs3QmdnU׷hkwmu*WC/&C/7U<}z;^" ʝW?lLO88&*^ Uk,RΘ?;:b?sKMFc}[%h -`s|S/}d}![;:ɵ3kCE2~E߶]KϾǮG6 &﹪r`0m^Ȗ=u|ZK`Ӫ|0p 9ðqMZ{XfcR(iV̀~c̜0p)U㬻o&Jv֖VY\b /ɪrw:-UNEX>gBN_Tk/5t7H&qf|۹vcwakgGlqȓ`p޾ů=d!7-`T߷ذ|o^у9vȼ u[0ex6_3~ٟ*Λ?KF#{ZYDydg@ɢHfsJ(V._sut易O~E6ܹuk0ef vNvG\Lԛkp)Uf_D ufӨӸyYmf^To/DshvJhg瑡[iә;ԩSѣUnHNVPPi#""""B6d\6yw """"AifQDDDD,R("""")Y,EJEDDD"%D! IDAT""""b]z "=믘O3""""bEHɢXdQDDDD,R( tR(y4IT("")dQ_rQDDRBɢ3"P $Eg5ɠFI%"\R@%""TJE2$JED$)nh ,̞0""OfEDDD"%""""bEHɢXdQDDDD,R("""")Y,EJEDDD"%"L&QXg1*EIs3 ⓗ͟aѯS9{p:F%""лE鐇ϧw""b,dRe0m:xP&7wo ::ѻ4x/vnmqd:x_ځ~۱kS0=;P&7}ʱ{ZG@JUUg/F1N}&ͦ椃+[V/+͙>f{ɀK皓}7`43u<6Bc\̋SGmU4(<ʗK5'XH W-﷭\Fc,o^goK1У'Gǧ_1p,C|9[OPӧU<})Pș#{w/Rfm6ic/Qr$%\i`Ue/>z 6e_kݏih.=NTJ?sb%ӼDD$qJE2_έ,e c_N{ ۂ3oD>4ּW@w} mbVw=g.\+T6oS%zp~sЊЬT]@a̚4 sW%ct"y?uH/_2@8$\a4ƚ?;: :* ! u:)K`<عx(`hŨRE0F#oΐE˖h1!giXJ[GCvFsђ%ٷ=\5޳o?~ǻݻw9vXcHm29sa2z-YoQx`8W`0*(Çq5|M}"a9w.V$xzJ, @k[oo>h1!#̪PBɓ;[;.w -C˔fʘq̟Uƞ=ٸq#wxb}>/ݽ2QQQlܸN;jJݼ~p<dkkA?lݺܹC5IkD.We~3o/8ʮ]sB=h ,^j:уO>p{ T(WO>;wX3)o`ҥS j$(h,ҩFdMӵBB+:?nիUli7tę3eQQQ :O*Q͛sǎ8?~\HsEL[15p-Y]v~x:qmp0~(Zę />OӿI3JV2nJҡ};N8aci߱()]t1-JYwmVJ,+teㆍ˂ig>1L+Wxn,_^{&_͚E}Ck]FbŞ+:ge "W\4oԜ|ٯ g)gj,6Nk8q'MYRJ(^ lشѴYcHN&uFfXd)v4oԜLY$ߖG>+aai8XͫhР!ժW!6lݲMj1?`ضc' 6)o:da?ɮ+#Q("1L-jqŊ̟Ǐ~'[l +RjU .L`` UUп| *5hА*ݷ\KrɌf}N7{_-t"w pVۤI֭]Kdd$+W? ƍٴq#Z&MΑ#Sgr)4Ka^J蕫:'ƚJ: ãV 6񖥧>5kɛ'/Z`!MNߤɖ-{cKC֯'۴I.[[[5c VǗ,YP~}\‘ÇYLF]>h4daȈx,H1o8|FAPByBV~#ׯǩS:HXXOɗ?wP͛F~~,\0 :רY9r׼yPV-\]]qrrf?8q} K%eڵ aKϪKjlll?B 񸘘<%K`0Y۷Q|yYru&gjP"La?7v,.\7c&!w'yc5k2eVϚի\ksIf'NPQ$OJ*-OZ ;NK{c^|7evJE$ܹ uhܨ+ǎcmp0 k0j'./[,C g8 `0ܼyYΖ͛h]1k{Eq̞mll d_ѠAC1 4öa"o|Ӂ/ҕ^=?aҥܸqGpQs} :fjNI\\\8rgΜck|˖\x3gNgړ7o^ty{q~n߾7s`~zy+2<΅cvM8SCDDӧMc׮]o^u|ǐfg޽\v{RŅf͛[$$c$ߠaC\+$T̟~z͛׼<}R@ڵkO޽XnnDFFx{8utt$0 ƍzjy23%"a?ӳW/f@~׭̇2Mf1c;f4<ؼyM<@ԫWj 4(x4mJXXesU;!tԙS;oVS.oQQQO|Ni hGZ׻=}~mӠ^=N?ߋ#h]Rj֭]e\0d0n߾MՓ|R@L WXb9]_BUo8my =Cտ,]׈l/dcY$cLܹs兓3|( 8Jj;@>#UL>9sRc=~?777^lO̝3c2Cww};ԩSѣGzn\!L&|k'=i! ˗ͷHҔ(V3~a?%OϽ{Ȓ% E rPГ3~EI%"\r^+Gll,ue1l۱Qc2e$/[@˖t{1׵db4mJ֬Y9ذiG-ƒP{1d͚+WxRiެi Ù m;v2h`/<.ĉ8[Qn]xՄ걷ihI1%"tԉ*ݹ~/_BŊTZ… Hj8tEV-,^ @LL ˖-UpFM"Eȟ:wfcI=KjPB+_ISɼ?Y޽{|7q"舧ٳg7ؤ upvvs.\v$cgoNCH Eɰw?Q#G}6\۷5ߺu8~_~֭[~>} Z:lM{qppݽ2ǏɓDGGSZ5qkboO%"BYL!66&̙?do :-Z;z%ѴY3d@90 :sKa\ #UB\Mv{ʖ-{ϑFbbbUkfQDRE:HXXOɗ??!: ˫67K,1(^[6K̝;w8p`?q,U [$%Vqͳ""ɥdQD2 `0ܼyYΖ͛XUL#ԭ[ϼ_JlRnܸ#G8zh{޽{v*ϟޥ ͚7Nh׮={buܺu#ڶVBL&Ξ=ý{T,H1c;f4<ؼyM<^-yM65~!Cة3jew~-֯[tb֯_# `gǎ%00?jU*ӧOoΜ>meo$^Odd$cnj?QDo w"ύSңG#\v=CH3%ehبQz""DߚY,EJEDDD"=[D$ 9!:,EJEDDD"%""""bEHɢXdQDDDD,ңsDzS"""55̢H/)W("""%%K7JEDDD28kGɢH&P2׃+Yym<[K A%"ϘGD%""ϞGç %"ϐC%""Ϟ=G)YyF$*aR( &T(""ɡdQ$KJQDDJɢH&O $ -UbQDzu wK3""""bEHɢXdQDDDD,R("""")Y,EJEDDD"%""""bELd2|خ!eJJ7YM5Vz?'6WB42} .?K%Q8شre/v3c|~{d"::*%&\s-xؘ| ۾16\^gZnRs&416uxdڰ|8tAɢH&e4vG 0.;M8yxoO6峱?r14 ɔש=~8E1(~uv~#(R]ݨi۟1fzHqK X"I["z7H&rޯlZOA{p)knԔtbg݂ d՟#Ep)U"%]TDtnj2 ښp?*!'G'd09n{ih |*mG3"^D![[0Cw f3/JnlY4I=26:ڪ:\sOc6-YFfSO\s5κmZUu{!ν;G@JUUg/F\}JzаD6Zb—`%KQ cBq}9 bϖuSۗΝSoߍuq LKj.q-_ځȈD#>IJE2Ȉ;u *Re*r^WBim,JO_U c2|;GSk4պP IDATc 9ywPӧуu!0׹| Sn㗵8s|f޾O#zgdf0+Pǿ=?VRIv)Ihk/y֮K&6^2=1#؊cwq5xsm+ogv> ym=.=}2,ƜmxDnܜI6d;|$}>.~|/ܾyѿ-eg|]4dX7tŪc%M_z{Ab}ذ|!_0?wχrE){ a|cDU-sikҿ lM/$Il,HP( ^d2Qإu -ϭEӗ^õ;mǭWz3' ن_ 2~8Ro@+֮ :έWvkXzsQ/Hӎ )W& ;ВU=dF'~8EKH W*Vw}vv)n,}L(.ks-K1ײ晜{w=4`ۚ3wn&:*s BᓨPݓ zTy'&ka5{='k$k,\|4bcbZUg|u%֏^ )ZR]2߶ yb.=E;V&#&ggi޻s3'k\yհIm;Ӈ%v<'cѹ(ެ] ҨedGJ)IfQ$zxmݕK)P9u.wF7>{}ɞ#9r9$z5u>TQ]<KޥN;^a۾WC{54 Wq?ׅesfҭg*լm.KQcBqY7[вK6-q 8akr-AKlHq~LF#U| ֮N#:*wdml\'PԵ c!b\~kk"\VOߡyTe~>:n>NtT${Z\?c@Ď$u7lfL/wb8ǔ>,dB/dρsRXo˜:rrn#GN\;7X#[ V1?ÆF6\|C˛ͼȞ3CcԪ~ŌUqț֋H,ZbM\ hŎu`Ӫo_3vo^Kٻ(6 ς ()(A!KL3F+XPc-`.0 X(**UA"Nܙ;Ν73;o3v<|1"JwJ09YrgQGE۵q^$aJ.ɑYFٹU!c٦SO<}{p*d<;tfe@dB>?W͚ˆhڲME'qQ#¶1kJM^-`ڪX06/#9c9qqp,$.5uu3i :8wb޹%Z4@-#WFE /?wD"܈8W0Էm,yY _wI=K(t3rsr};f6rgQBPB{bԅЫaT;m+O,2斬ABƲfc8xā]8m-Ge"Q9թ8h>p ᧎`?y@M?utХN3!u޽{ jA$!d&yo %gMpJ8qeWUg #-Uqˋ ȿݛxޝ﨩wg7.m|PIC"n^~X8.F>:EW7Б:9 t6=sسiU6kցO,4NՋD\8~Pn]8ĸ'x?~̌ _Tf!(ݎnu}t%sH~=[KOOERb|lAc(knw<(Jiۥ7vo\44N :' Ib)8s3SFA-z6MW9s):3GbƈKF,-EmُѣY=ض^ de楳q5YM+M^\گ;a`&)_s튤xlY^H\~gpk wCOz8}k+Q#5 áW5jJ>mČafiQڡ5ڂzV?5 wt /GH=Yp7!bQdJOK߂ROJ_ "x HG:^yDPSW$DDTq"Q9mƢ|mcl_FߕAdYX,e+?WY$*b14ro^Ě9?.y!U*a4] xc֓O06/gbȁ=jjj멤3DD,c0m Ƙj+2qdBe2R-h&-݈f^7&ݡU?[) HJWJ4E" "Q"6NCNv:: );&*i24QEd]̷o!]z3DDE "-%o^EM -7ovLW042-QDDaHT݉_>GX9MЪcBej4wX4k\:xp2*Vq٣HKUe#d;b3*kk]ǡ^LX>]00tw2âiLɪ'+-6/[DD݌,8>T6,^:/KY .m|UIӲNYGPBCC[%7,LLH&&DDDD$ DDTxfdbHDDDD21Y$"""",LLH&&DDDD$C?1EDD?Y$"""",LLH&&DDDD$E"""""ddbHDDDD21Y$"""",LLH&64Q9eRY@DH-}eyfdbHDDDD21Y$"""",LLH&&DDDD$E"""""ddbHDT͛;=w1vFTJڧzv/|9_:KXw+~^7WB0S?)=Y@bbԿdIC!<"nݼY*"'"''ÆB[K VAM-# 77QQQ:}:]OOO|;bC)So^YK\tƣG18X,ƱP<{ђO8;? ơGߎ\'-ňKXx ֮ ¡[m /^"K53+qLF~DZ8t(΄ݻ5s&@[[5AAغmn/͌pA߸\ŏS >>^^~+J<^AXј`9ؾw,[3"'Zh%M^,ͻn{ ** nؿ0YCCCe;]C?N$)o`ǦMqIc(&DD<`?زwhkkZۡZ/ׯ_Ϳ` ?a"az$g^jժ;;Gff&<=[8}CC;; : I/_JF:uM֭[Kј}XpLLL``P>Hg^pn-)))شq.j];/z!MY@^{B(Sjj*֮Y5jԀKLlbwh {암߻g/ud% Tqzߊ uttĵ+W_&DDCb/r*ԪUزF,4wQmC6c"!!AP[ndeeɩB[q΃B UVȋ?z}"//.Д, =!,}ڴivvXbbՐQV$A]M]C8u$233qax}{o=oKЪھ\t񆎎6кM UB$A#%$".! _ Am)}Vf&KR oBmO%b|$UQȋEȼܹ =Õ+Wןuԑ(̢̬La}"'@K2ӧ>8Xv-, Qa۰SfZ*ލX4o022ƎltM$&&bOSa۰!  QSSGFFz(SzPWWǩ'X[Y[CMM ΅I]/t %=i21ׯpS"dըQ羽طo/AfeE)r$Ԯ][fdaccciq㺠mjԨƍHNN˗+I9555bYhӦ-444 ޻=̛WWWחNI/&D";^~m*cjj;w &{wB+.rƍ!!x޹w YkԨ/&NDDx8^z#Z/d %=i21׷&Ngŋ8v,T&6ywгg/l߶ ׮^EN%Q ҊW!n޼kkE"OH/oj0fy pI>FV Pff&,^7Jˌ Du% }HUHD F@@@YAIe}^x;[;qM*\{iii,C?XnjE޽UۦrJ\^l| KTCCC["" 5@U~v(I,7n\ǣGR:Tnn.Va*M,Ccc K++ܻwӧOC?Nhڹc}QZ5}dfE߾TZ/E""rXv nߺ5kS?aBi"] paJeHDDTw ]+l{nvd=z$x74ddbHDDDD21Y$"""",LLH&>:(\DP>U3DDDD)e>U%E""""_i\`HDDDU,%f&DPX|U,ҒҾ"QS0IdHDTL?ē,U ҒC&DDO-H-&DDqI!F""RE @H2ȄdS$ dHDDbHT)1a$""E𷡉>b1a$"el"ddbHDDDD21Y$"""",LLH&&DDDD$E"""""d ?{{ڇ},up!I'Rmqn.6.'\R1ipW7I-HʌKi ?Z ;;ûצ:'JݚJ"Pm;ޕUH,UH``oxtk86>ѮKo>5|~,YU&`ЯU x}SSޏ+ MT5Q={kOg s M>__0As~O/oXDrxYǧW$,UWz-碧9:64/;iLdMulX8 ]-@ȿ~б-W]eޮ8,DRGnN]-ުGצ&ڱn):ˢ*zbe?ɿwR^:HKw#댶l_Uqy1:qÐ,NzyzX;o2b +0~AY?}ݬm{ƥ(z2q ܍DHm`ʰ}"oˋi@ v*уyyyeӿ w.Ac!EV[II烼G|R is41Y$ۻaX(UwMOVlAibq.b?w̽f( Y5s<Į 4/Ɍ{;| kcR=ԲlѦSOF"4:UuƝɃwgl9yKgn%c q C.bw k Fzy1f Sꍎ}a?OcԕᏋvognCܜL1eůPS{/yH|ܓ}axvU8wGν<)^$Yw*d<|BOqA#d>(2gd́T~@e"Ү j1q= LZ99ݷ]ާwv VQ>pl F7o^ )1i)ػ9毆M'0kruhu }*.x `bn FhֺD2!ډи.BǾqDWz\<ο=s%S޼Ÿ[Az51hи)vo\?~ب;/h- IDATiW)Z ,m$g^*tY6x< K琝 'vJwи.p2d̡?%qUvȚ'V!cМ3Ĭ=~g &NxuG:jȿ}dgeg.Ox#2< ^$ 5 ZU:iS0sDܹNEf-d2.Ob}ܬ% ^6M}V'CL$E+ 'ʽHSp?{"(&_]\po5Lp! å/*kimM+yv r*NiU쟂ebSc+ȜQdȊY*{B̽[8AC2Iye@Aǁ"QT\7ܜDߎJbgm 555\9JPۑHJ`az5!*AJvM0mV,vAR c3 #Q֗5D"nD+<2< mK^kjVF~ʢk3ǰk Uupt6>sKԵhZF&8ǯ^~ ,M:ppp:d/x̒*OyRV.d@9Pq`HTת'I^4.Fb<CfFw﯒xts,-GC$/|uZDپ )o^piRP ٴ #)1_zJ\{X q>$Nû7sb3G|CuSX4ܿyoۊ*z ))g{!q GÞMog3ͫ*IMM -;cpiJDppp^ IC827DFFyzI?Ț'%(x {e@AǁY$`9?֕p{ҞA;"5 laվSYLg/y?b}!SfYC#S\a4w.*坤 ;,WI0oYvJH3J5]=^&~}9ٸu5m)ݯ1sW`)<R޼M'5KʌsF v01wQPP>SG0mx_l<TUIL~߀;KƗ#'*Ak.uX6e$|{ xx)nd>(.dw)~@}ȲAeX.ze4hܴár(xHxIK7u(TBet뿳:dbHDDDD21Y$"""",LLʩdLguMeRrsrn$BdY򐝝Ujssq ?y$ˊnCukjûS?XRƪ!4W>DcHTN/"`8v\F zw30w՟2+6D2ʃO}SGS  @o>|:ձʓR߆2>iErhءsZ{7,ueTdgg!xu.צ:6,ޮiW±uU x;*7w`t6G^^doaab3,gMX,. s-|mcTo/2Nظx?w#܍D{#E:ahkQu^: ?+=I6б-W]eޮ8,DRnN]-ުGצ&2/ 韼6Ŋic 61b O/(-%6a=el 0Nx-Y6= '| Gct!-%Y8b ^ުsŃ;7$܇\eQ}ZXce]_jhUWqW^:xu~b!iT:xfd*kiM X]|8uSWsKڰvo܁q= IiŹiKz=㟢Vb\8~_>`fiܜ\w+TdgebnDXiHyJp{Җ$"6.S -♘=r _OVG1kNԩk0q`uS퟼6 I M--\ěW/f;v^FjJwrsr0uxTƔJ?`833n|磠U*7Za?4NNÉ`ܕm'1Q06/i;y6Mcm#㡦^(F_wd;EAs~@HX5r{7lWP4[*4w/~6#o3QעL-aMZ+Ԯ<>pl F7o^ )1i)ػ9毆M'0k*韬6rtAm#X6U[#{ Cl]Ԫczdrsro;^&_; EuZw0.Ѳ)#-ۂaҍh}:hZUJ0 eC{t<o^E³bTBhM?KBm_7(4[R=&Dē'CL$E+C ;õ]G\п Oc1oki_qssr/~F%Me*%ykX6`"r]U"G'~38L|b99ي "Pihݩ'=8˷uμFrR0Y$ L-!p#\aoX\]UGnC\C;D]edChx <_"ҘԳ\xFpՑ~Z5U[dal^>mO2YBMM WΟTQeWAi)ɸ*4nZ40cvsvmXP99 3KY@M]*T;(61TTI;Ѣ]Gs^]J W JWT"Q[] ŢͫHM~VWkH۫wg7.m|PIC"n^~X8.ЫanP "!7!+}^8- }:p%Fr z Mm3<{Oc8F~fMpJ8qeWR+W=Y˄ҭdqɯvtKQv݉_>GX9MЪcw+ȼFgW-{i$=Әh,;dfd}CX8a8ڇIxx&bC&|KCܐW"PQ%cywεKhC\輲jн8#aϦU yFd3wwӹ"N$튤xlY^H,teѷu#?|d[E0^vmA=+[[@cg7 l?BvvLz#1q?z46bֺR?'m"F^Vc}1=%ݏE)N`yqWT_Pχӆ4R`xGW rģ;X4tt#g.E>rY]C0aZtq &~S<z U9[NɆg淐y5jr&vX8a8jFMCbPU<݌,8>T6LBFZ*44+FYze4hܴÑRsK߲/<0`dé0Z)JWh3pE"*Uu:2seUGBW,!}JF0Y$"*eqc3+ܻU3ï`TѩV֡QshhپV-PaHDTn_ e}5j_ 7â &';G)H&DDmhۥwYԲTIC'uTnh"""""ddbHDDDD21Y$*,upġB+ \l\<O/#ppvDzZռ>\<ߺ}ZYE!tkjûm6Uc8q`B۔ޒi)[0ݿT0j2 z5đ=[UVqSX<\V=>:H{IԭYY n;.r Tڤ4)}xfB)f ok]7.$tlhmAUGxY꠷% ?%[UÀ6g _v3p)n$BxYHwlhSֺ2'.ؾ>imqIIoӰpw4Fgژ?nRKhQ)Մ1p7!1.??u|\kcOsQ{Ǻ /w4Y ƞm`ʰ2[q266ձa4vO=ܾp Qfl 0Nx-Y'+ޢTU,Iyqq7܇\eQ}ZX *E cp7qу _F,El]@~% fǙCbֺE3p6.g`T׼P8ɯhk6=yc·0oPIc q C.bw k Sy^YY<ZzwƖ7txv&{;| kcR={`>)+~ZG"V\VlAip Qƭ}'CRMmzZ?Kyc^\ :k\<"7)E +e-\_Cbƕ;>pl F7o^ )1i)ػ9毆M'0k%s605FMݽ?tûG<{)x `h\ jc8#Cv׆ ȴ16#o3QעL-aMZ+\~]:4C:p邆Έ}pyyXwvPhT?_ .PSWW8iEY5A:&0gIK6 7'ˌWUg d̕ G>`x ;9;D'CL$E+ɛhE-#M-D+;; /GvV&}"h,Q .o?+Ob}ܬ%\{+gO+g*^ =3G1xT4n֢Duob ë HM~m q_8q`V=Z%˅[iO֡.6l8Q/DžY!}2%Ʋ23S3D1+"Q"qeya#iWՁH$‘8$31<+ϙcZM;/O 񀉳Ƿ[!x*J\QEl؍мw6oˆ}B :nP*p)z\ݟr郼1WY&Dػ"S kD"܈8Wh}dx6egm 555\9JPyPSWGfF16:"N-\#'WL_Bu?F3kMHxm,`ڪX06/˫u;I>y,lCO&DE&Œ5ϙcصadq+JVMQPtU1.v$,moSE 8B o̅ġLL-Zq!<{ѩP, ߼7m|ܿy [F?K&: aGMm3<{Oc]=t̡}H~wo"ޭB23*Nwb٣8q`]%9 t6=sسid] ٴ #)1_zJwUgnP "!7!+}^8-y;3V7>nBWȢh X9|i$=Әh,;dfd}rBBY>s!qz.HTn>As~+X]u`nHy 6Mv9Ԭc,٦gX;GL/\8z[_mv&-iF\*trxS kv2گGumZ2FMC5k9/<`bn Q!;AÎ/f#UL[aֺЫa J"8'lX8 n;ݧ>SG0mx_l<T4n_qcSEbs85 wtEj:4ê}'7f9.ݟ%Cqc.$UnFFuD`*_|^HDgx24nZ}x\ֲNYGPBCC[%7/C'Ss2QEdeh" =5fV6w f_LqAB1Y$ p ^Q!Zq:,2やbHD^.ѶKやwH&&DDDD$E"""""d 3䑠yyy*qw#/$aMDDEO@zZ Go *i ]vY?p@DDC ѭW|vTEϡapnODD%3DTdžX8]-m)zr zWΝAdYɶۂᫎ.AoWK?e'l\<WϟF"=yc/cȷC-=Qyds: `7v(|{a ml]@~!p6.g`T:4B=k[qDCGgD>Z.Qehr̵/,mMZC${w8g+SS֫?[#)15 jUϰSIDAT{dg#Buk֡\x1Y$ j!bCL'26/`z:,l u&"dJ̺#~0&D&fxx&DKFtȞmu󚠛adگ;a`&)2:ܺfshު&"Y$*}] m\{ѩ2542-TVX;W#͔r<0sKedeCbss}|9r1:DDT24)LM]]ng-:""dJcVXr:""RE"*UFeE"""""ddbHDDDD21Y$"""",LX:e}xfdbHDDDD21Y$"""",LLH&&DDDD$E"""""ddbHDDDD21Y$""""DA=T!}j .3DDDD$E"""""ddbHDDDD21Y$"""",LLH&&DDDD$E"h ΑO^5sc;Uw0$={t/0Uba` <~R _2כcT֖qʂaM>|Dm 5 $M`/qeI? ++Kpyyy ͝ݻu aHDEFFFYTWܻwC)444[Iu r-^C{mKX{+ha zY 񷡉>BuPϬ^Y4===\~(D"%IcITV xjL4 >y}ܽ{gϞcK}xfʥ5AAjzuMa3OX,.T&$$>ިW͛5ChQeꂼ<ɲ/_Nm>z Ἱs й.EDDZ0igϞAݷ+\0NNo.tlm0f(Ȍ σ,aA8{,z psuAXXRcV2xY]\pAU+fuTh`pvrBzfpGƍ$ۗ/,[5u뇘2/j&ư'ھ MbFF%''?o&a]r[6-@;&86P}B)yPRmL裒&/+3P9OOO.Zp]C*s 4k W|;brsso/K=[nJؿ06)So^YK\tƣG1FF%:([m`8ř0ܽ{uիWضcB3g0zH._cS9RR^ `Phkia ђ(L>.\'1BXΘ> qU8e UE }wA EjЩcG4YYY8KxLXؽ:*S\АyzhlPꟐEKK ͛#b˽;.<~]_7w>u60nn-%_D]3BeSC!c,KY]^24}T֭]ukJ]S3ɿ5M֭['Y>p@0d0l߶ 011,h {$t{En7~n;vϜ'Lcǰaz̜5[pH ͛K^'$$}p011s,\ ʬmvl H={z]@jj*tttQ^0p}:|2nn--[~Ebb"VM7ݻ$pv^rDh_Ҕ+;F022,g8:4={/VPzz:222`ie sR7W+ih A:u$g@e)z\ZXX@Ryǔ.d?ƶ Y$ʘc%g i۶P'O`¥9,RDemVTtz}"//.@,õE IyHn-UvÇ.͝aTF رHHH\wQw?d ` A'Eɍ(Q Qˡv4U9BEO ("C@  f@r?B\f,Mxf2{<׺#>M}٧z75EDDx'GyYUVVk2T}h}tYSN=))/׻wa%;[;c.{3NyՆ7RG }~ohȐ'*J ͯuWSTӊlW}eF~i#*s}:[>"!A,4tYYumnWxxF]kڵxSÖ?n ڜ;t'ԅ}K׮2 Ֆ0 hŊ4SDu2 Cu0| qС 4jSv]|cz-u}֬"/@6M7~Ўk؛}շJ{;g s>X׸<,Y/htoJJJo񪖚U:qƔ}϶"6kp%ѣZxڸѧ/KOG~q=z.Y-7k iԨzSmV^Զ[|/} z =fNO?DEEEڵsv:|X_'3ӟֳ3fSN^-[PPܜM~egg+1)IRs9G;wԞ=?ջ&1>l[2[v{@XCfќٳWj/_;GLq=q*//)KZR^q7v.$Zɧ:U-iT뚫ԩSԥKýގgg#SO>K.S ^/ISxJHLwլ;ga74x@\%Ӝ*..Vb| ߭U~PI=&iE2өի?ѣt%Zh^_FLCJKK5glmgV꫺(RJr׊zK/ѣ~-Ym|Ẳ 4Hos4<%mFT~v^ 4%'' 9/.+3gСCzy󘦩W_'Omފյ֭_~.w9h%kL1z*fddԛs<:ꨨw;W^8߶m[w&Մwl$u+Ak:crEpwmte銋Sǎ[m}:vݻw駟;thNǘb<,@3k{܋ٴiNDj¸:4F=Њ9Ϟqd!((H;vcH7Fw#)ƳgY%",`KEmRee 8Wֹ}k|b@ ,h;wvݪ.KO״Ryy:+kz9RZMԩlm6Auv&?#cnz-8 ̟bRW]zLt> T>iĉr: s8 9+D6Mi*$$DdO@CXJ\.ed@A~GIU~}|(O?={utq0k.CӦiYnHEFF*#c$iر8}JnnJKKu},ycH͞;WUrDqZ 9\}", IZf$)c'$(&6VUaq횵~PmoOCWK6LYY> a@HLLԆ,jՊOPll hJL)hp qW($$D/_}r8:JO_~A׼מ?AOhv^qd@lOHЌg!CWPP Pl\OMՀ֥KFDtSf:ݻk˥I'UZqk} ,(IIr:KwB%x8}ϤIZh&~M6W ЮQ]pHKKSrr#?Yzcbb",`KEX",aDX%",`Yj~~E/"@jN/E֔w:7!t]CXh>@9=485524/0(+$%" {zxutmlokmjiefbda`^^Za_]XVPPLQLKKIFHBICG>B8<2:1=881+**%**% # y{xyzttssrnmkifghiebbhff_d^^YW[WWSRVXVVOSLQMMHNILFAJFLEIBH=B;>:B7@8><6;3<642.5/-0.0+'',)%'((&&$%%#"##!!            $$! ##%!$#()*)&$"((().,+)',,00/30.622.//-6:=:;>;A=C=EG?E?LMLPIQINSSUQUUW[S[SaZdZ`Y]Z_abcafgniohoilrsptoyouruyzzy|   #'""$".'2*1,31358;==<>ACIDKDJINJKJNUQ]W]ZZZ\\`bghjngkkksmwxz~y}z~   !"*%,,-21398<==BCIGOISQWVZY`_b`a_^aabgagglqotntqtxt~x|}  %%4*3,357=9<=AFIHGEIISNSNSRSTSZ[`\d_kglnqz~      ++8BV_y_N]x'$z ^f*1w1kP*cv_rX{! o1!Pvp!lwP^v*V=J{YPM)Q'3L}bFG  6c /1B+D !,#W$t&%%>%A%$#"!,W ^ | 1wGil"81%lTF^op]#ՠ;ՁՄ1ִ -؆'yv ecahd= Aj 1 IXi o kw h\!!2s#%'CG)*,I-[-8f...I.? -Q ,6 +Z*h(&$0"mi0 &V % =  nt&xNqv\4!v&/_Үp% ͒1̻̖͑J0K>ЗӽՖcؔ ,=b0Y+F~4 +05 3$.+ rK h# &h(iz*G,-.V/|i0l0H00&0o/>z.cJ-c+JG*s(w&-R$" "^8O 4'"Q Aj xs`>">ݬۺua][q a@c)ъz]ԭչ{EthPX aE/KZ+'*=Efr Z2LX ""W $G&((* ,AW-c.2-//00/]O/.j{-$,*(r&!$Y~!}^Yi3 # n(Lk )o hk T3U9}Fk}j|ԫd]Ρpx̜j~84Y \\l9IHK$F   S2v\Q O! $ &P(*%,Jc-]O.A. 6/8/B.p. -K,t+*|(X&$"aO ;A_ a  C * 7 bC^X=y@sۓ>;!9ӓ ԡPl",fc9ۼJ5 |l|3/KVX3d hy}>~ic   "]$*&t'(d))UY*n*K*)TJ)h(Q'%w$P" n"|9B+| = a   ) _ 6 {3.+~ Z5ؼAךiWaeeڌnYHr[}xM ,$Y &1Kr}D  'FezF _ E  u Qh !"L##=$`$vW$$#""!3@|&gn    # m +!7%SV7@Cߨ@߹cߗyhm+s.r ro*z 5fkR.DPOl>[5  g8 @ e  C` 7A!z!}!D!, ) H>DF NhCam@  `  u}m+SHu?==P\4VC0,K?}DG8!d* ;jqde\pJFNsH5r   E j o3  2 j#w8YBn#L;<(P } "   I E  %'H+M9\MbI]X`gV{yDDQ'bc;y- ( t 5mx9LS ,> s" "!" ##"O$ %& '(*D+^ +.,-3~.5//0C11܂2(33F`4p4FЎ56ϲ6K27lѰ7/8ԧ8 9~d9ܪ99(:6T:m:+o:Xr:Q`: P:/::C9v9d998%:8T73765L514d32120/@. - , +?*)l(B'G%$dj#!"# %k'#((S(M(sd' &@$"x@  8 A*pZKhX$cd3X|p`ܯ:~5'0޲oL H[׮szJYTZʐp;ƩHnE$@~g2 ' ߺ` z/V:,l$<p[\R8 AT!ի!t!!̪ V/+6 _ p1H3.VK̬8򺭁?0ŮTܯ0m|䠱c7Ҳp ޳J]ݯdjиJ߆?̻噼j 6]{nFŹW];Oɻ4ʳR  Xў:ԇ!(#Rٙ%z'@()އ)H߉))<(&)C%Z9#   ?n? e |1}`y C4u6et`*NHyTۦ~: i W 1NK!\36<9j1/+^6 !"o#I$o%&'() * +/,R-[-B./0!1#2%36'4i(5P)6)7)8)9(:';"&<$=!>|?tb@@A*B$B CDLEFFZpG!H)H~I/JJK/LLQM#MjNNWO[OPsmPPQDQnvQQQQQrQ=QPPVPSOOO>OIN`߆N-NMlMMzL.LNK`:KJ}&JYIPHI ZH-GFvJFE D6$D\'_C*B],A.Ae/M@0?A0>/=.=-%<+:;)L:&]9#`8 j7d6#c5Q4\A3.2 1 0.-,O+M*b)?('%$V#X"^'!HM_ 5J}?5D h Q V+_amZ>ԋ!Yܘ,>H?[. f>5R !P$"'e)~)+[[,>--,޵+Z*ܛ(ی&%$ن!ة׭֚ՅyӚ ҃ K_И=#͋#d˺ʎl[*UoǪƻ0Ɖcf&ÌdM(\ᗿځ ՉѠ2jƻli`ѶGdٹHܙ$_R&Q񇸪`?<')oy R3.1W !"#+$pr%%%VV%$ $k)#ֻ"F!3DgF{/$|0¶Åo>3HƜ Ǽ eȭ 1j#'wI͎$=rЫ~Rl]VRքTׂT_m3тѕLѨ߈\{Bc݅w k:r9} b$ Ar!j9T 6 z  I G5+ v  DiE[ 6:{XWCE = '! ]" #$%9&(6)^J*V+`,f-m.h/b0S1;2b'334ڱ5؊6R7 88զ9$h:;;~<2==ޅ>#?k?|^@@A{ B BC8CD<DD [E E(E:FFgFF FG&G9G"CG8HG/HGEG?G.GGGF{FwFjiFc,FNE:EkEEDmDDCFCB\B@AgA@ p@W ?e?C>~L>="=<;:;:99_87k65 5@24N3d2s1؅0ؒ/X٤.Uڮ-۴,ݳ+߹*r)G(`'&%$x#^"E! (q X F"34t& f ? <aI{XQ5+(8iGCOZ636?N2M) L 5 N\U5Sܩ(س,ԥY;g /`kD (] DfXùN~·[9DLƿiP߾tS [̼2Om 8 oo[YV+_ros&ʻ=}Nμoaý!~޾C led©pÜ[3<~!%-ǟ_?wjr42*L̓m8,ϕIYҗ!qI)ؤ iۗR) =yOy i :#]tB<YP/}^   8fX} s  8 %QtqgX!29?BC{=?3#( B !g"s"#~${%l&\q'QF(?#)<)I*l+Y, --Q.&`/D0{01{7262 3+44m566 '7% 7C88R9o9b:i:Z;;F<2< ==K=@>>>b/?p??? @ Q@ @@@@Aa+A3AOEAAAEAw_>->,=Q='PâkwoÇ Aġr;Ť\x\MZD," vȎ / jɳxBB$̺+QV\'8@ЉAѩѯґaX Ԩ 1 Wֳ5׶.jٷCSܷݳrY@:-=gT!K,G9KHAW9k%cn?$d7b2h>$=G^y V , JX E/% B ^ t  $ b! "l # $3 }% f& I' ((R ))* +I[,'--).l/&0D01O223MK44576k6*77"88 9{99O:: ;`;;Cf6>a>(>>|>K>&>>>>g>>>>& >5 > > w>x O> %>' =J =N =' B= = <% a< <" ; I; :L h:9{9v9y8W7b7N686i54L4 32OE210 0G/0.-I,,K=+c*3)('<& &)%9$^S#`" r!j~ w'>i0q`G5 b ) wq]8-tl >9N_oc? 1hX>AC([}kcVXSSUߑTWhUܢfۿnټ؛g#Ԋ85ҟZnF+ykiu!7Ic[sz0H)_ r  w  +4/  m $,-B0*&/d! p!q";G#!$I$%&}E'M (9(8)P3*o*+@,;,- 0.. c/0031;1I2b2V3_3S474D55)6F6q6Z778m889c99: R:*:C:h;R;;;;#<;6   / kP ~cpcOG,+r C mC;Y|Q2()D#`E\x4qݫG%^ڞ:Rג#sC՜0{ Ӓ0x k6ϝp"ϘYRΐ͵\͸̛hJ)zl˨8&˨ʎn_L%* ʂFɿ-ɷEɗP:;c_ʃʝ3f- u [Ͷ Βdf6,ϸaШЕFэѓAңHF`Ԗb#[]\PgqOUܹp"hާe! %&t,E sLy&P*Vn2YK>)}] >) |T- W n#"# ' v. !% $ %F 4q9S. ~fqAC !!O"e#"$$*%4]&?'8'5g( ))U**M+ ,,u(-*-/..^2/,/001s11A223Hj3}34a444o4I555+6R86pg6666787^7x7s7W7@777777i797{7Z7>77r6V62v6<655Y554^433+32L21`1.0C_0V/eQ/x.1.-,[,{+d+Ri*8)')O('&)&l%$# 0#.r"c! & 3_3Ez<;Koz 1TtoF  B " 2 SF Sbvo-}Q iF1BXX)mdL$=R@vFarڧCc׳NoB<ӧT~/JЫT ϟ'8K__myr0hP<8#TɼɗlEi;ȸǏ\1qǬg|_Oc'^nwǜǂU/ ȭ9wiF RɤɈi3UD9[69.Qg͈ͨ;z;еb@ч-JwԊ+~B.uצ`3Px^>qngWIn1I%c4p[Cs)Q, uX< %     0Ny;u^Lv# !"f#)$$%b&s'i'Uw(G")3)#i* ++@,,f--.///00 k11=D2V2z3q33 4v4453]54595+%6%X6 6667?7|d7U7/777777i7[7K7C7A7G7M|7W^7e57}766q6*655+Z5F 5Y4m`433232U21[1z0^X0=/=/..k-d,1",v+**p`)L(+'.'c&%$$ =#$k"6!M bC^z0Ul)?Sfv.>S_p   ! 0 BRbr-=Tdou'-EuKc[Ik( ^%$BX\"{lD!-Li  ;4w[ރݲ@'b1ۡ'8^ؙd:<Q$ho{ywv*mcDTEr0#Y_νκͩͣÎ͖͍|wyK| |rhgd\OG|4u%vu}ˬ˓{bD((G|!_Ϳͻ@:gB2L1jҌ,Ӭ8Y9`ք'ץl ], ;AlG-KNPLMF?,G Y"}RaI)X0lK]14j@q[A.z W. ]:-Df _ $K K/ n "Pf}<3iR:j|k4;i !!{"##|#hj$I %*%>&&`''jq(A(r))[**m@+8+,q,,---d-I4.0../a///0J0z0001%1I1 f1$1?1X1{1111 181c11111Q1||1]1<11,0L0m0\00//W/ /.j..-Q-,,,+9+*K*x)^O)H(+F('0'& &r%$xD$Z#<# c"!!c zY^M58! D?v##/S=J\nJt  H p7Pj~9^ !B.e9?OSQ)VKSoVLD:,6`3_jQ1#TDfJ1VG߹E߯ީݪYݧܫ$ܩ۴ڶkSKZ+@rYkՉ/գd T%9OZ_pxsф(ъАЋ[Ќ ЊlcSY%M>0"|eWG86β-Χ4Δ2Ί?wNh_YvPE94)"$U !B&!,--|DJ9\d xuӆӗ\ԴXh&?XGsمڗ2۫ۺ) F&3u>:JPaiMmnppzmJeWL6g>zm[B?+ iE|$\F' fAxttdUc1VUT Y ^ ib u7 xS %+FdmBk4.Snl E  !1N"A"Q#`1$s$xj%&~&*'}'=(v(qA)f)Z8*L*<(+)+ ,n,,5---A..l.U)/Eu/*//50o0000.1Y1z11~1z1s1m1f 2d2]2d2a2b2`2f1k1q1o1t1uy1}R1&1000Q00//B/..G.-- -,M,+i+*u*)n)(Y('.'&%a%$$k#""g!  N cQ(W8d  9 ^ 4Jb{ w4oMgfaYWIFCL5IVLtNYZ[ X;_]kvxNPf _*ApYh6܂ېۥڶvv" 1@-JXdafpHp{tazzxwQh!iZXQS=4.ЧsВko^RZ6TTXbЬhЊrr}P7Ѫ?јkr^Y5PtKIG=HJU,`m};ՓժlH.8Kl/َٵ7L/nPsݒ8ްwu2F]ss/zR BznT^!LE7wM V.tcH5Y* e7{fYEs=B6,&tH     f 1$07EbUcq{z9w+F3[:NUNfko: u zd!!"#|#$s$j!%Y%G&7&,&Y''#(((>)))|1*j}*Y*H +3P+/+++ -, \,,,,-$-C-]-m-- - ---4-A-U-b-z--w-`-J-*- -,,),?v,OK,e,u++k+%+**P*))P))(;('m''&&%*%$1$#.#""!p oi ]OC?8 ,m")7,~gH d<|  J  %P6HSa[tTWL KIETl]I4i;meVm6,t/[,߰ ߧޫݣwݯܸlܾtD1E{_p׆nמ"׷=6S@uՉԡԹaE  $2ADITQUKMJFB5 ,%#EmռJ՜ՊoW6Ew( O׶G؜؆n]eK&A/#l TF?DY~  3 m M&2<+EMS.]fr;Ms0^P .3OA@?GbMXY`ji4mspuKy~g1y C  ~ |H ~ sreBe \TyO(HE5O-#V L>z>d !!""##$w$$R%%$&&&H'''E(((#)l)))#*W****+;+\+z++++ +++!+& ,3 ,> ,L ,Q ,\,h+q+++++n+S+/+ +**y*?**))C)((](('[''r&b<&Y%Hf%-$$ $##""! i k\KK3#(ESRDb""71=rU]r.~m ! ^  >(}0CR'e[y <w*Y C5p5sHueWjO: /SIRn,{cld>[N#H=76+1 '/++711\=BK5QUj/r׈ז>פ׳z: v'U<#QfuԍԓԚ{ԧmԮdԷ`Է^Խ]ԶeԵoԭy԰ԬԤԞԑԋt(oNYqXF> +?"} F)6ٽRڻ-۽ܿz|8!b'9G0PZxftzkl|2N 7o%{uhb^!J.-h$ i(k+|kq(gYPm?*<88]/ / / 8G 4 8 ?t K*RZaPqyky&r]+ 8LB[fzq3Vpw x!!m""\##:$$ %m%%+&&&/'z''p (_Q(K(7( )>)k))))*{#*c=*JV*0e*s*}******w*i*Y*D*/**))))m)A) )((+y(?<(W(m''5'&&Z&&%0\%N$h$>$#z# #":"!P! e i_ M6Mvs]/P4'02 nT x e d4 Zx W T TQVTN&ae^kk:w~L]7z T$29@~HZbiqv ~f cq%MxP2voaWK@;+'+GqW=ܼ۵۪Iۭ۟ڥpڞ,ڝٟٚٙKٟ٤ثصtP9 (*78>FIOO5OKKnH:71)%EoFھڴDۢۘۍ$܉r|ukvkc3b\`f_d@bc-crw7Ty"0A@L}bme[]ex(IxWr bVuA-2=VjvjpVF.~' ~ v % k R(d< l &9.FbKuh~ +Fdx{hD # $ * 2U!6!?"A"<"@)#>x#>#2$-m$)$$=%w%%%&C&n&&&&k&Q'B5'(S'h'y''''''b'Q'Gp'8`'(R'A'-''&&&&l& E&*&+%:%St%c9%t%~$$G$$##2# "'"12":!C!N.!R ]p e sk-trUvlhnhkhekdXeUYH^U1ID Dt=9C-, !j&'83 - y  j ZB*xj  X C$* >.7044B.abj/gtn4oilMimgmeU1WI=g66C?@1V=$OmD#>]!k6lDnr^݀يkTxۙ#/؁,v ܮҀj~:UѲFї CҬH[K!tԍ&)Y+d0֎5ב:l?\D"UIXMRޗUkX1=ZP [`L[ZYt!"RM%Sq(+.14p8g ;a>pA~CF eI L, N P GS U> W Yu U[ ]^_'aAb,cJdd;x97I41tR/,)!'.U$^!u*$ -j > I b  9.su=V 3)+ #ی:i*CO/jߢ1eoؑ̿`eC?8<>dUчƊū./Ҥ.#|ՇU@ç;RQ߇7ÃO&XngǷ'F ̆JG&!ԛ'-1[2S$7um;&?YB DLGHGJP}KLQMNOMP jQRWR:_R.R Q. 4QdPPgOANJLPK'IGE"C8$MAJ&>9(;*P8+4j-0.&,S0i'1X"23X4K5 5>6qn6 y6Q65.u5W޸4|3Я2c1/*A.նg,ǰv*U(%&#ĝj!ĚM+ zu i A ⢂\P]7vn'ܺҐmUD\+0z謹C_KO Huš4["&R*.431ߒ3'6709.:l: ;::(9867͏53Α1{/X- +ᗦ(ܙ&n#: Y 6MN> X8~ӽJVy2~k(Usܕt";q?ޕޱ 1F6!$$w(,}w/o2*6Df99Md:~c6b3a'/`9+f_J'^]#\l1[YWUwS" QO]MJcqHE`5C~@=:7;52.+(Դ%f"OuSMN%mr V dEvؕr{=rܕ˖g4盡 7⴫{4 ڀgm۰[ }Ӛ, Қ%є'/Е7в>EJH3PTp#Y8\_0`b^Pdץe%lfڷf`܅f$޿edd b#f`k]5[/*XT>;QVMCIQ!E@Q<73S. ) $Dq0 Jw#%^(*,7.1<2 Ǿ40p6ǻ*89ղP;f<">a?w@쨧ABsC-D*Dʯ:EEEEELʼnE:E1D9DވCB9Ap@?> O=;:u 9(d7T/5o63=1C/sH-Md+P(Ti&[V#W!X1XDWK^V9(T]QN -JxEDlA<7D2J,'!!f^h ,B&yӂ#1ՍɫSt3X"$߲4Q]ݓ'R١MǒW*H _O;Iޚ]U:O>`vܜtqJӜƝ) ߞ7jŢ!,"!$Qz$/$##"M=!N <UTSqƄj|Mϰ ք !܆!i""m7##qI$$Lv%'&& '&h(/3)#) * +u, -'/ 030j12 2"2j$B3)&e3's3e)P3* 3;,2-2.O2/10J110[2/3.3.04(-4=,4_+05*a5){5(5'5&5%h5$;5#4"4!54 32/2F1;0/-z,+)7(&:%#=" "3Nw ; Fq   P R   w! Y0]ca&Gs_biswH%^ :y}K& vJ8$D0<]U .?  B~l^CL;(VR "  $Ov%[ &G!&( "g)"*F#+#,,$-$.$/$0$S1$2S$2#3o#\3"3"3A!3D O312 c21r0/.-/&,H*T)ZD'D s% #!~IhLz(yt4 { &M*fkgXPB, /~raHr9%3?Kf݈9tSޱ1ߥ7=Hᷱᅯ|{ル_䯩IBC2Ǥi5?s֞UqӝZ͝-AMSy OL!yuNHZY೪RظK;)`:@243).X654G]z wZK21-!BE%P(ae,q/(3[69lLn%(N`de`dD$Jq|0xG@ n#u7 =L#)'3p~!b9YYo]z &1$;3-1&  I9^އ-ܹuۍolX OKRd{ٌٹ,jcڲm 1ܗCb@iI1=;CsVuX;M9(7 4;2/e/-*,'D%"L=Hge . #Nz!K/svOf3 `!0Ilc|Ӧ:HʐVĻZʿO{nlHiBMosmbٳks˲IYز+te㳉fa* 3[uB aub,ȹʊfA([ 3Ho@Mp ?BG{" *}8O  _+ FH D D "h   T!o"`$%.'2(#S)I*++)+i,"---2.TY.k.h.Xd.V.^1..)-t- #- ,S d, + t+ *Q*)?)WC(j'&%%,$B#]"k! F4{5]/^C7u?Y`#Eg  [ ) ! q ' oC/s'xan>7)  R  s t m, k gmZ\Scbaz/ V"U~2 ;:K`GL9( <UuS,d(g-{<4s5 ZE  Y / M ]o j! m l `E U B +l $ U b &8CeLOT ebD4+ܱۑ u\F0+Hk4e/_O:ρϴ'9*cw)p)nр>#ӕrJ 1խ  _ ײ U ڭ  PM ތ > O . W-5hF%IB1$p h ] R Ml F; C = 6 ( r 5    n A   x J ! " $ L& ' ) (+ , . / 0 2 P3? j4a 5 6 7 e8# <9W 9 : 1; ;F '<{ < < <0=e=<<z< < ;);.:-97)9'Z8&u765p4Z3/2Y0)/ V. ,Q + * z(K & C%q # !  7} fx[5d # 3 OZw4;! CW:y/`G;X?)E h1~3_3ߊs݅<ܡ03Shڣ[;م)Xs  ف h * Sv  GC w -ܙ ܸ : k  s , [ q0 X 8  W  fI <  U o 1D X+ q\c:lUc' H t@4'Tt4kPh)>; MZht]3O$}qlcm_9alye?QK h_U%NOCIOrYf{]  u 7  - VPV~>1xj'3@ts -Jf^"q^< x { l S#!nSyьOC%ϰΌRaL-#wafu /B`O΄ά(B~s1Xӭf$؅I!ܯM k\s n`8RtN+vfeBS/QQ  Gd&Cfo(p=y[1 !w"?##$L%%c&&J'''E( (+(G(k( )) )(.(U((7('''?&c&~%$@$#" "7!c rO/[9DP ^  H sP dp V^B95h2=)I`xkL6P2%1;N:m=85"O{W~O,Ck`12egT?8&2k2=JLYtMV; [ E  m,Q&]mn5qbX 3I R.7BE^C:b 1 &D! !"g""~ #YF#&###!$p:$AV$f$t$|$wj$I`$ :$"$##fw#>/#"""!9! = ruo~:TS:xEvM e  u (3pdQ>k3,Hamgw(snfTSC'' Y|*|~{AXN ߹ߔ!qV}?5&% %~A~Stݒݽ%cTޞ,;|/wyQ4#[];6 w  _ Q \ ?  3F t = p *  ]  L g ? [ _g $ X T Z { 9 Rdd{p,<> @   (  Y  `R2$]{yfHgHu 3 _   5 C R Z/ aB \b Yn O C ; &     t sf XL :? &         p a Y J D = D ? D I W ` r ~       gD Pi .    A `m &   i I m m  K c % 4 q > J k W ^ X ] ^ DU E ,? . % n  :   f n "O +  ' ~ r gG   I { '\ q- V  _ v e V S XC L S5pB$l 87_  J";VkBQ" k    ,  f  ( gJ .a y  O  ~ 0q ^ M>  \  M W 2   jG lD(Ow_)R'x%Qp]9nQ|?;40/D9@sTf g-6VFH8xCDt x   0 X y0APQNI9%2FfxH>ue$ _S uJ>6t,2Cxf=/X,2W\< "Bf6Wu  + >I*[1t:@D>7 &2$X SxO;)x@O!%{>HhzNLA?xJP3\qofPE-zk^K>-ye\RtI:Z(, f, h@ X 4 p A  ~{mpkh]hfNln2sh"tc(A#\[r"Ko<Wji= 74c}xQ.x]p<8e V # g& /v YN.}}V1C4|$|6`6Ks{G"\XR"P\7.Y ,<8LT^]VTFA-I NW&h$Lx*D|b/ a'K*{#O2<Jtl&)eIAs0=$vA`_2]hMAk!Rv )  >  ? ; )sIt">md7ev >c E]J"X 'Rs1Hff6d'3]HE&Om8D/;Le(   y n h f kt(6zFQa o '9'KG\z]v &3WDyo .,UV{B3jN`v#Fn4me }tw^,M?0m1 wX<({s|'>T~: w,Jv9x5afS ByWrQ>pGwI,-Gesk`P?/+j&I/$+9kWJ$P3xjuc T Y] L X a0 p zg   7/VLX-[lTA<-p'sqfMh~F8 xd d A !   ! d0 +4 : 7 ^3 '  r   U  ~} ` 9   X n k >R  4    e Rj@-G(  W)=Lnas#rp( ?UrA0EbBV , J ^ w Y ? + & 8 Y  :  [  t b J : , - 1 6 F X g   % \* 7 H #[ lr  t 7  {: _ ay R G ;, E 3f y &  m T 9 + l X 9 | k c e S@  L 7 k (8     yi N d*  \PE> ~ z9 t ~(     S   w# ? T ft   % S  $ 4 Q #c @~ X n            ~ ` L 2n Y 8    i = r P $  e7 vV0Z*   j 9    T v" r j s_ v   B   L   Y =XAr}> [ / a@ O `X e Lm p ?f o %a a O {E 1 _  B%lI{khe9`hm8{.[-cYl?{<sgX7G1% } ii^^#\,b7iGpValy#Bl+`X7.NkP9cXbh;{ Bb?#ii~onYH@m1$k nX 5  c   $ + +6>JhNJ5LDD7<9-"6m )mEH[%iv~^,}rldAQ8!],U%tM3d#g"@C % W }   / 5DEH E= 3 iQ9l$R&#(rRE$p I]DBg y]If82%iH1 ,8Qj 2P1n:IUbn|  0NkwX(2V~ s[D$jyLHAa; X~yXY=&4oS< r P-8*T_uj; m 56Me[y f  k _ ! * 2< 9 ? @ FJ G H H >2>e57+($Kx $<J_n|um`VWS{QkQ[UB\-ciqxR(r*  &D 5 E Y* l wZ u  l\K6}yilaP`MDXA3`1*o(""!%*J2AI'Qcoqg2 *BV`yJ])'@Ue{vS45W}qiM`XSBeC9Ah<;,BEMgRdIt=32&;HmLZmM|-B{BRP]_(^ZSF 6dyE V   1Q ? H R0 Js J B D# 4d *   7 _ y w V @  *|2`6G:.BJJJGE96*% w \ :6 Z ~ ` & H w ~ ? E ps /  >N uVBiL;9t^S" _(::>OO*ZYz`'ddi2hohEupzdsvysK{|=~A_&\&#=E]lg7 _7  "4JW}nc|C+ $2DKSdp0Mi Ek'2?M*]ekwx^&q [V!?JKbl^kz/ 5   .   , w  gO0wdwqe>\L? 7o0* "Q ! ! "* +s ' 7 >@ R \ i {E }   ( W  ; _  8 Y$ nA t     7 Z } u b J '  1 8 R W _ " m {  l  - } v V p e ] d L  5 # j  W  y 6T 7 o   >  rY 4  8  ]  u =S < n ?r K'l]L^ {' *> P g `|  c +  & V* +@ L ^ h t i| H 0  w j Z U = 2    + V v q ] )B d2   a@2~l;jb[JVOOdIJFpF@'<}>?#;40%/m+#g  J/xN~u_SM2UEpQ:9#p'_u>drRHC 27 )m     > h       " 5 B M W ^ a b a \ [ N J 9 *       k N #  y f Lq 3=  m2rM/ G tQR, YxZT<Q x?hUB|:6" u0z39G$ 6;LURZnp^!^!e%n.m}c@E6]+wMv*R0 qBS+U5?`k]I7/*d 3s  _%d#5:!NdMTO8Yu^jlAnjgfTadY%WfJ<3>"dFs"Yo?" X@eMe/BOvfK@<$% \  8 e   - 9 K T ^ ` b a b Z Q F 9 +  m F ! }o v8 j _RI_>'0$k #^U teWAK=0w2g+d%c^T &?)/2hAAIC=ECBgB880L.% Nm; gqIh"T M:/ 2O(n=EYf|5e -"\3I`mN} Cw M~(e 7rT Ms_?O>3!CV%f: TX:NkD4jCg + c ' P } 3 G ` p /v S s z r k h "a 6W :E P: P$ Y R Z T Y Mz EX :/ 5 #V `(|haSbE65 &!sDy(?. @K`Rk~K  D,Ai:q+0XQ  +!H[Y w2 ]7G[iZ}$o@mS-sne \QN%P$W1^8hCrI~RZkx.Sr $;QdB|r p &#LkU.)m<_H|. ~h_u>cPYkofm\hn/uZ l&>gW5 Y  } JQ9"]SlEvE#vAYo|}j7&p@Q .5w8+]a >  ] \&{<]G<R,$XRq%b-9I Yk*%#@^HfAf~oE&eFeW Q 8 I E[ hc _ \ W S "Q lF 9 ( , X 'F~xr{mmsyy)w4sGySkje?m   Q  x % $ @ g U   v : d 0   x L u   $  8 X x >  v    D    S  } S d1   I y K w" X a v ec D P*  H F xn c G k: $(  g Y;pjyb UOHDhDJH1J-T6SLgf`uiwtzvsnos o0twjrn]rnj4fz]VJaA9 .^"4b0+U'6CECI I Q V b xGU 6 I f (p - * "    ! " # $& %- u&? )'M '\ x(f )d )g *f s*p *n *h *_ +P *D *0 >* ) ) /( ;' &\ $# #" ZS(*M8;+f . =kd<gOqVQ.$ {SH7)?yf?YMA*A-ԺӻӲӥӓӈӄu%lV QB;:,m LALiDU:iym]WGD2-&#0 Ef{!(+7::8>{;G?:Aw? A = 3q #  S  ?  v |lkVqM 83L#  [gSdde~t\[L!7j*$EmzyzpSj `ZI1C5=* xh&7.5&|i Y E -H vqE<tn:a74v>Os/w Rp[HA.fqBT0 2X2$5Ts!9UlQ!B[yglPT4 "nB^q~:R!xlfC]_tsP'1nLIr\yV A4c,$!( m & uY @%2K8O}mX*Chfzkfw_)zGb q x( 7 q 2oQTkdJcu9f!)y17|6=:4{,g0#uYڂ ؜ZՐ\+mGПpku%В*Vx '&X&Ԉ )p5#ׄٙܔؙgգԿ݉ Jhf>Xd \n>^F 5 gJ;50cJu ( )YN705,  E!"r$c &c'()(*, -R.Yp/0z1h2+ 3(z4D5x5v$6 U6TK6{ 65443P10[d/<-nw,*,)&}'d%%#/"; \R~6mlR;/U(   c NP8R]/A]0 D'R4\ m 3J6w# A Z|NWt_Wa#!A05lI8Au\ 3<e2l7"8sc. 0 m E .9M>;J  _|i3r: nN>s>G :R \}|wfT[KID D)`m!6LF1 hX u  T  R Y* }  <rJtnE>bUH!8.LAklD|@s 2 $=Xb~?FO{*P{)v]X_q *B@j@9hhjj0(p>pM-jI<: ; %  4' < HQJ> |  O :X~: ( 7Yy  ,L@HO"^l  1!q`!s_!?!* y v#DGPO/?F1 p-<P( D"#HD$p#p!+:DY j\ ` V^ G L ZM  D o  Y" %cx\ N <Q I!'sLj@$euup1IYZZ^%\TC -8 _blDYblZ~N7Y݄Z$ܰB0~) T &ܘ Fߋ(W7A>X?n+'yL2Un<w!) C  g   Z q 5/({my4Og[iI,Gk@4C1#` 4}h N   gqF + s A 7B 1w6` tO 1Em XV[O;|]Ba+)J#*%!3=Ndk|wqfC$vH4-!%B6\ ;U<( x)2Z(4 b X mq v"w&+g]q R5!U{!!!x", 7" G"dR"F"\)"H!!#\!I " k 28t !(X U 8k0  j  _ b  u |o `| S c w8  % EQVu  , dd 5%9mM7x4 ZebTI,!W<\G G]_8,(05t{7C|Rasp nHZ` H S:c  #RFJxqbok%f   & )B&%,!F#D(!Nw4' b]  1 G # V    & 5 /   . 4O  )6 w  `yRa&!o\B'"5@YG2e%]o,wij 8q-\kJ'7vVyIU=&( e;'x^K$tE|wBkFYet2Z k$vhAv@Na-F:NkXGez :A S6  fK+@%}<no*8u W   D  3 c#1IfvGxrX;5  S? x  dk  e* Cy GsH" nt'm g] f vG |GnvH  dr"S1 9}CCRqPo>l6 2 z~ k  l!~/M._BdH"y uV|M:v10r(5>4_$=I[_n{&3 gr  r j R s# s   ? . f   { b R?  h E] o j ?   Rz1]jO3i  val:X v S > > b9 L[s2;#aLE6abML*Pgd\bA^b;2a`D~K~W"Db iv1kgg,E`9E#  :`0N)sf0{ # x :  8 4  t U1 ?} ? 7^ 8 KPDj}4/w&Y|+r0GX:]SV.75 j W H  #V@Xq AJ6z&OH9b;+@<w,V  r j . y F -a >h-L:6gd9@N]lY:LwF!w.413#!|#W/ @GvH"FYIT;j n4BY[vnoS}[@X/rJ,?JTew>-Rr6\y3N,mJ/[~\E"eLf\u*RTl1Mlo2's$|?b ,(Uox?.q8yzQ09R-"5XSwhoVQ#~ 7s8t`:BTh-nf,#i  ,]  *   > ?^'+ m-<g9 P4&Q 2sa.{( g s rpX[Dx^Fs?qK2'1S:&b4z>v/pjl/%<E3?#r_sr>_x?\~!wZ @\F}E2YF"0L:X D  *~ y O [  F `  VUj   C] ~'=:DgCN& g  G j  &X X>Rl yI>pR13)7pjF"%M#4TD&+ZU2{4H)'REpzvYZZmP=GUU  a& % [qR<c; @ # ) # T.Um7,.QdgAW7M uBu$ S`ZJ ; 9X / +  z j s r y   >q   2u6P-h,GC6VIV3]`R;J h<]A!?j6Ze}#fxI$rasBew]3[w\$>t1 s8uPNQi3*fZ Mh}.9{*e66Fn*W3CA$7z2 GAyEUyw;C4]!0.q'  y*y p_~DvcXjD): q~U  { ?   <IXnQlWl!C>U ~B   aR   "hB-T`!cpmnyv~2@/&p*N \  g hC6/8,HOVrC)]PQ)ErRnMh$np4O#~~')srw(P lzkOIUU$ZCGW@(JGU*uvnyzznrKgvZc/GY' tfFa,d.Svs``#C.p*lz ZD.8 4 01.f.0:He~f%a7T7%q=V"1p<YDf%BqJcNuo.gSmGaSL *PayE }"*^,ab&>_~E=P8DLd5P T" u/cb$n;!YA,~?pXD #Nv LJw$?U_[9V:}!?T$1qQQa.MD%fbD3O NsoQnn|XRDz'+d'Y:3]+) *0~6Q934)$ "l?Y*iSJiAr^VLRqR{WT]qGY#6pU>u,@ (Fdzp..IYs% r5 vV0 KVwT`0$ +1xf`\eQNAHIASHQ}`.s|C{@f!Ea \:7W~#8~ gPp@~^}{*kxQYsyg;>Og~K0[p9EX"|By K{5QU=GCVuS =\8i(B<~$kPrPrXU{ Z'yB *zu'qtT[",4CLskRf77Bfxk\ cE  Y{cZPeOIM5NC[D[@mQwnS:V "7CXu` 9m2 Ez-/{k;%0fk%wjq.naS].C~ T<;V\ 0nG bi~CF;xkSS>)'xADHu.%ZP){"3_ t'i, oU7! [5QySV3:  L#Eia, F,w}vx#v".^El>mR;^/x_ Fa*xwQr?n@ocy{w A5a;#WDCJ Fz?JaIX+}n[f-i+T;Oh s|\jKW&{9t0^x| R ' $!t,f~ya1=GYa~o8v~vskDg,ZK&7<#X  6ndA d6j3VUzVeJ&;?JxbB}n=  |P)^ xW(Z w_OI,FcoJ+  o3GjH [v1#q _YDA%x)7aH1E 4ZisZT$Fb~5y|fqX0h$lB)_aTy>,:v4-q[ NQ hWl8+! (whRgazloe' &Nl DcrD%;Wf{^- ulS+=X& &_z^A#!/1, g#F'#+6ASrJz$=N#o )F>lb! 59/66.DB>>:Z,yjo[6 #qW%mTd{:jP.!u\A m,g76yxV.H2J\yRqA[#U?@3r*A)+86BR_pI~T-Z%a1p@-yw#[}107P%-{ S@ s"@h8`vYd+E!s>T4-j&Cp=r%H4qo H'SC \E5Tn~ztyV,*XM Bk5$mt TT<; d&[wZN'/Rx(2zIRM"`pR%fEnX55JbxpN*_0wUz0|{,^mC,.g,C[c}2g.BV} Iv4WlKz&WN^/a43JVnll{Gv1q#cW!A$'2 8Qjk=N @`?i 7kS)d>1]oaG5oW0 (R6)HYwwl``#dN_zgk}P\tr]O8& '9UfzlWF)y|[`:<pT* c19U~mXA1y%]D -#?\ )=N+cOipsk_'MD&rqG0ZpP85PpuI) o/V_6gv"Nricochet-1.1.4/sounds/online.wav000066400000000000000000006515441300720305500166450ustar00rootroot00000000000000RIFF\SWAVEfmt Ddata8S+lFG/EyuFj~G#sE59m9)6Fp`1% \=jl}v-|a9:xq&?Rahk jUfB?4l(6=p|IIYw8C r^S_0h4Rw|OijK7!5F ^*1oiC0C4ZwR  }GDn^2}u@V&!Y`v%U0o"U!R$M=,aU-LN#NA \PL 7)`KcV5l?+mIxiyo7'&v?&{'|3u}bhRi0-5|4 +n7"; ZkFa9#(o618'zAhaU3K7]pQiJ8622w[6s2o$i+VE^;H9A 6/T0)W.RlIu&KF(0b|yC4@f|F`2)JXdfw9 7/}g;u`72n$ oUW}3w|]1W3@$ eIj,{Z^K(|WkoA{nR" RC@|<zZ LI }^4ka B:Tuh65>%dM I&Q%[t9d~mv?1eT2z= L qLHx8Em7!~wS}PxBQ}3 @uDK39]4 lT3}U$5`wUnv!6fe9gsya2rrsX<'d."Ji6z  0)!1/}24eVb*no >;3=!0@un\?2!<`"~L=Z;d>,7}6@kfX2?oC>p6^TcGj6W//`-W+uf(\F^SrN*^7C%<14faEy8s<P.U\sR 8lN  wzB,A`:tbmA r ][N(P?n ,[6k]wq[?of%s!1[xBjj!;rfVTOQW!-W8GX|)( +9WfFR%:gYoO'294ajK6; I0mz+YjPTQ6P;w<$O!2IR^\bRfy'HTZ3 a"\Ri:Dg|T1p1zPI "Y^(+pgnW0>dFc- G{q4G<(j_BAcnA6_MSw.xuH}DjCXKI{PTF r';Z0H<{h.#nCm7KybYT( +}Xf ]431Ge ?M!qfNn,uJ5>1 &dN.N(S] VvAK5=hQ0K~#ky:bRF7D<j;TS4JbWnCv.FKm L}s1._/Q;yQ2E VLfcd[6|LF+EI8-&149 iR' 9X@B +Df3 wHTW1h-[,hZ.gsi , B p,+=)ghl-_ph>.o+OQ18c@[my/[|m6(M!K?(l=h~kcQV6 ei(eNBAe;^xYi._Ir*Yj0yF,:(29g'q;~H]8=AO#S-/1 *$)+w6= 77Yiq *L%v"O1AdeTlK{ Ax Nc(n$r g"{yklO.,0cEBrm6[emSxyYjk|2Ss:N7SV J?=1h |\$v]B}YjyI R7jh>85r|H 8 ?6D;'O.KGd N%F6sPpMHBbMCtx#>*VpY5n}M& gG3 CD@z3n>T_ ky*]E3Em")S5H zvmw~7Fj1i$B0">0sbyKaU*& DN2>B\ Xp,dNUR/0#{|MDJMI97|=B@0^~>9_:f4r&ix *Qk')3q "-O[O5k\>Nutce4^@L%fz4 ACAP3H $a{7)3Xlf]}%+hx\8V'kZ-gp#Pfr*5YQf&/<))^:C?&x=Y|y1sZJ0m,gH&2l|Z*9rkb8j3}u%18.ok)q'g"+ajre9E]SRP9+ [Fhovb}ntV:37H?2'), J!Od.j:KEp j: Xy?gf8gB/%$I3M|^D/K>^3,G|X c~=Y*Jw\R`Wc@rVZ;,|\lW-pGozk`]i 2LYhp9{oSg02lu M;j<+0:3!u\cg %(O[ZQh,yqf3H]6^=uCk:CI~"*S~l8}a4Q;uD,/K)ix S?}05u3LX(dq) J`sz$+Z8Q8Y=\}QpyFRvudO39th/bHR&?ewS~2&!QwO<'i $~cO ]:kj*wBO)din9+fj.^^dBgg [)^W,yTU4`)N aH6;s?-|+o5S;!]& y(NJ^nsj2z+j!bo8X8>%ve6C2u~YDZ>   )'9R ,4:Y(8PO7"KrH(A]2{//RMZx c[1a(4qp~z}~BjPo~(7.`tcI5Z;1V e/$3BWOsJ!#m Ij}b 5F@>1 Tuui`tb5#L<8}Hz$:`2|Q 50u6/]:2[U|sTdv<ES{yNMg~a)ui5mO?gCj_B Z= "D(n:&'sl Je=D*i?4 _Q l6u54cVF\UqsU1Q?u8u`.b4liN,{N>#]#bd* @.#L^1Rvof}#Ya"4vO KdJ^0m@U0d/T73} }. g<fBkxu'_aYI 7skp 1 h<cXh  mc G8B\ g :'" T  =r O <^O m lH L = a H   o 9 6  2  t   = 2 o b 9  b N  ;   q  % .  - $4  u   D    A + 6 {] r 2[ i & V  y  S|  j X  ) ] R '  ) 6 . W% UdV$= M A<p E G b L+ Fq&zH s] vH.0 E +Uift (C"%[]QU '8 _Rk8C  e nNe pu<T Fw_YtB l cr {  dsn U $) H c  [ . 1A l `  6] H=  n< M "< A< % #1<  F )ytc M$Q #  3aWD8gk 1adk,Gf,LmLK{a^NJy$^qi ,t  Puu/{QS)f {!#e|nm* DpL~Q),]m$}eQ5aX04g;V\  J*GlS$.8K! AjPkvB~Wfe [YQeu\"inOHDE}*c/uVn;oHQ`>Vtt*dVs^5k9XC+ 6_phU=:$eTT`d  ) _sa0a9mOyrVH0Ly s8TCO1. 1nqAeMym[[R"!fZD,; bn'PID6K$ -=0!Q5dtEb3Q v,zDY^X6:w{DyHmk u/ )"@ Rk3~i<]U7UiD;zUx3CW W?+\RkkwX6B8L Y9{5s 2]Kzq\e<K4rS6eCO4AVUuf]`hs@ig A|t**p:>M^;~}GAs  E" /7  tKp*90nOb sy y/z#JAC?nRS#HEK6ZoJ + v^>?z StHfFsO">?t d> @z CQ!$3v79Y=$+pr"wwxrD*IY2Y]L `9gIH4q_cO A5WAtg4[O@4 ~a Q #~ b `m  O  b:l5 _ I Pv%r L-    ed !  K\  M  s {+ $> +  I  / =.cz    2 e AV r Ya ^ # Lj 2 4 , - Q  }  >  |T 4Wfd;To  EiX+}9"1d0BoB].,$6(V"Y#o9U&ir x/P :|Kx)#T ftRJT<e&QOkGiB\ ur_X@!rTbPa)XF'C?s=08:1pY RZ`@O5zJR5tBgS}#?H|Yh .A\<@Y:.`LR&[S_L{)* g196:/#HkNP6KT>3DZ.?6 4V 9|2d \7B[!8QcH y(cq"jfd(lZf>WtKFaB#uYel0R[s,Q0[ +D' :L4W+{(/ZpMX9 \.i#i"s7ZwS>uNw"gW1B )"NsH+o H)a!fmf"M*Rl*nybXx;d|j c/:yX(Y't)l~guDuS);?*xyzp_2.,t%`-#\>IX_v2n,XCCsNh ;JTo8Bywq&%W>Qkr= A $ 3p* xS_.N fV;wx7*+b;--wt%C& %#2U1ezF-c9z8b=6 WXC__yqy vxrk uptm{s)`XY NLt[D/'9av'5zo3u=Yy)Rd&~%? ++]*#-bjpf'7JTd>]ht)3DzK)r>.Bb?4BWi"z?e6i00 <TO $=HPD2|(e|iN;}27(BijQ2UI*0$-tYBlv?~l_EB w[9 z9\nj"b1?]=boi6]j0vsn*p;wIeM|AB r^yTB^$ ~b$)9wl F*&l7Ot!,(uU]9r354xz+I |^29"gJ$ErjmiNSJ~KZIX1>UR^k^"9S-/]=LZ]4_SSb !xi _`p~ YH7xbRME>2{D8&}rb5b:utq\Bk~4gD`  VPa ItNL kCv|Ayhfd*[7:M,q$ 0l_X,v StT?3905u!1Tm)+r FFM9%z; twkN O9#3) Re #7:r9o K$wiut 5m{ y8_d)&)PMs( >Al e>/#^4v']}PcGA `owQ!:APF;vS7E5i'A:t+, _/o=O{w&  a$v<05in29+4M w*JTa)C|%J"}<FLtARR\V8, &Q-Dv9,J rUZiEe}"PgV  bX:' x!>Zp ykZ6r~,|]?Vm X}"Gle-(Tyl+86}XPq8nol&]n&}c. ;1Hp@fTw?q#-c@9[ SsL9HX02l~e!K%lL()m6YGG];;3X9OJ39vb16>pNikZc@a"EgkGd'&PoU>dUnLB,)i_" YN*;)l>M}Orc@ %o5adh<Du4v=AKook)Q1mK\5PWIGA)f9D!,~Ri6;+ `IEALzI.>%0 tCY:z=+V|1ve8HUOcr.!HWUWTH1& }!Tp7Rm^ L4Y!?7  )Q_ M =!5wgOY0s35@&<ti~7l tm H}*k~V\gzFl9)HTVWpo>tfky#Q_<.<`ia@yP:Lll&t4_+_TfA,]Bx<z7~,EXto7F* 0ZC8$/#L&1%t h]a0F` eNgZ$T: NqPJ\AGKFlMN*&^N mEJA`3S 9 Q J 2 )+%F =O x U 6cm : 3 ^f{AewbwHYFbHcb wr2 _D h  Fe i(+{F!` v8 R^e q}S P  J! ] 3  a O3 U tt u 40  L X^qV nSVM'q~$t #   jUkhd<3H}J n<0%!^dfq8u^ )6&2N*9q&[1pLJ%fLVFs|#N(%Y? ?rjC(;u WjJ kIvyoLZ@S']2@o:G? qcJ|+kq~KFM;#(TrORKHd$*\e#0:V|hJH).vqdKmmg<s1?"|3x{VE"><;PqZ`CK7sJHP,[@`X 0cV_8pJF)!_Bk7:9-]ygy;1 E`RLBZZgZ81 f5z-;e$_L:&][u#kbN$XJ(/6J tkY)C ls, V4JnD/c  tgk 8.U  Cwk*##HqO!Zt]NMu^7N^9hEyGV]vI5`:>k%TZx{3o2rzQ%F ^C7mD*sZR~15B1F@q+*\[qSk:>stVBOQv7QU:_*eVmY`yI4IL^/7u|ZQEj .!`#4\2Joz^!Jdl[  \ 38 ^WGL#%BtD&ycfPgt_U~3J\;@.eFp&}rCHc7 /5c6mt'`d[ 81DGNz,Z;[@B{3T$Z?mfi?t1u}m 6 *4[8`IFcG[GN EfQK(U`\?60:[uK^.'*_ euWj).3`MQ W{q[fe8H"3Wqu]~^s  MIH1&y5ps4h]i&W &ML~_$1eNEuCY<& `f=~6%;%~]x]/^7&86Y`9~q2K_#Q3+rC/?aRy^["H) 7JBRL$p M) qxB:c)8n% h&/~ FZ2rCJl*rNv]3wx6S 8hG^aLz%avx,? |h!6M1I tTDZsCK0{Yp^j=*AV\p l+TK=28_1j[TZEE b5F3kY~`e,_|$B}zgNn<1Bm8.s(W $ HT8o}r= nw7Yvy~(@'S?2gGe7S4C$LD^PCngX\+Oj ck[?S%`M4e& )" LXe u@" kH>w1s_#W^?4]6dbFCCmu eJC1fz5">|D\zQ8-V^`(C1a|sw'?7&$;n48t}o X A[S0p >%IA=5 PDyc/~qS^OK Dk5R#^_G,eMjn,{ pg"[G9Ri"`P@BJ  >+1| T`_nAV4/CdKCf >("0b6kp7D_:-@cg(S.q~ ^_?n-nUTOY|NxS{R2rSJc_tO( c1Une9.8CubOwB|4{cZpRO@J55[\/hv|;sFE:q!Ef`WC6T{_a2+ely 4M9zNk9vvjIO3m/`l:=)X3pv$[X \qn!KXa4/a4{Tja|[ V,L@oL0, !:u*%]{ u:f5qZ]X"kRMUf "\9@$a,[5X_% g@0ZNuEiU6p3O8 &x86My3o`KrxH+pdJM\ 1A{:A|N> >M#72Os]qEkZ5KJ%Oulm AMbHIIiQ`LRqjU rpVT|ifz :o]yI/y`N|PL{N6Aws9FWtXC[V^dv-fcCJ2xvsB{+~IT;g>'m/?T]9zI4q"awy9,GC6p_e4-_4@L.J FKTi7u=mtFG?t``%g 'dG0u]UM>3}#}z; 5&5J&+V1c OVPJ6u2T?nH@;<> &3 $*V [@pQpy4 9 EWFg1wU4fv`~ J;`f *f"AZn.&,Sc8_~ q0fDw5v57!O]Msb:=kJ`>ga>Db &@ OlZ gqWS\V@n\ ,R4J;!sFW` "/,fH\[a\-eWI@R{bcG8x_@b)Ght@d>1{g3bD/.D#xCs^:$49d+0r~LhpgM4M+lCf8cN9u=NCdhpT|%ft 0iWm"?d<0G;1Y2=gb`q! ujZ2SjxBCf#gsBkCG ] 8x35=G@'XzouC"LI!iVR6+s/aO`W&CLb57i^Uz6 V><GDKtBY4Oco^tG%aB(MReaG{!7v*P%lOGhIlIl;bq$rxasbF&6( <=H$|i"3QutwS~mwSw2Y4 -V 6c -V0n74vA9g4]JB 3 wq(H3@dbJL#b)9H!eQ}by5-!D#Y:']Y7 O7DA5*L7(*ISY|-|@%Oe;1 >lh@Y"f"b}(\f"&(d!BqE(` h[*\IPdIVR2@]jBiI':0vXHXaI0Q_ Z huxaP5 >Xn_q7[c#UG4U?/dhS?pp2d-B?Iv|xk8LOc C\Hl=kYk,FL'EF^aBF`v(MrC @XT;PeAo/:x1=0bsH'*;]IH>c .qcp7|c h/ 02 v  8k}EVQV}c79{EOcM@;kmJY_Kee<!_>DOfzX3MG! A;920pz#b5|to c eHlwA BfQ)G d   e&a6V " ~ O  rD# # K % 4Ze3RpFB{YM~ v=>'"H)IV=5_jTH yAQJi ^u{Fzzt$o{JjmR2^*<0_$+r `g+j ]Z}ZmC] i< _ 3"_5lR*FM={<H L8 Xy#,eh2a`#~ @E3Thh3b\ %F ~ 33 E f 8snj` (!  f  K(%{q- 1c    x}@  X$S { 9 z  ? 6E 2S ? t  & + D !   I fA &LC Pna x 4 :;" < 4 Q  i5 fE  =. Y>PM 7  > $e* S ~~x 7p$- Oc>  dKb & ^ <4XI%& C='ng'@zF-%iP  4]:g>2KFl  0fT <T\[x-$/ (ijY$ K5f|$N (,k | t(6uy:O/a2j*+#L64$=! jhJ'q[3HD}|&+j3KI!`B, Gz thSu:( JB{$)*]M2#~Cnxe`'z\K`tQ^E;\bXPQ<{ [Z !.~ U3 6@I)GT'6AW*Cx 3l4!%/"- Z:3t*  ]7PAMv fFt_5eN2E^M:u~ H$!U9R\5Bn[Xh]cA'3h ;kl2 b?)`Q=/)n U8<)bA (`G`~s5b(! cF8K7Kw"3(O` \OC/i=4.},Sh$@y:,tyR K- 8&}@|B x*RR 8F| BR&GJRo\!+>v5y wd .&~'(Kko#F)G}D+e>'QUWai$9hjC?G-up4.YGK+DPZK;&t:C 2l@}W%qCE )8x [?3OPYs42=l!PNc)hy>wiB|HR{U,>? R>.- oDuCFuQNVYcaPb r/Y'0]Q+2 y_3P|?H_FQAj5|*1D,g9 Qe/0Ks`yh7k1xv3OAD99?! D"9of ~} b Cn?f5tq# E.~5[AmO>b.*_Y5qaLlU(ggh|2_@[o5@"\ tuLz~)ln.cT dnb J:.B&$[k9B:<}c( d!j1TN6fhNF~:omc@hmu_V6#-.L:zMh!FMQTbo1DPG"DP~T5 F?%hlPl;^[kKlCY{zt<$T #du.,euXtaD-yL?0A*aB7;%G jyj .z<O(8,g<_s=,YC=aUwU}.S:x>$e3r#M21&t^1/_/O*ZJQ 08`8@v1Zu&Zq ).wX\^ hK\_0o9D><+fA"9-=2^bvr.My0TYPNwR H4O_w.F S(4-(C#@Gii{]%?s2&` \WItUIb0Q\(fU[cLu38 )tvFfI&Y+7L-{blV=;^cWiK7ueJ:e{|=[J~<]XNby1++53v.3zZ*Ei[`P6 2=sf8Hc*b'5ess:֚ k % ]]X/;j3O@| g@ uue 2 1 GP:^!hJ,l8M {FwZm] 1 y1E 6K|o3ig !FB2\ ( Tz#M 7 =i   a uu4Z A A+} DZ ;- ]qP`  K P OIY&ik36 gcd! p" @   = H JKk  ^ ]8Gu(\T E +1HQ*[7d|6okn`U1r7s}%EWY  U(y]XR$~ecQ CEiP@ &;9L+T$[qel+DF J?[.xcK3jS;EavqT6w \WDzm"u0M $3v[Wd~oIY?L  A)IxE8hEL);WTo+y* .Q<2F?LF@\n 0LXJOX>rWsgN' |lh;Eb f#p .juNs(n ( R.hS v aWV H p  [](58O`EElMZ~d E  * : 24  <  i u D<  %J : ww, d_ 4 J Lw xg)7'd  [ {NP X   / u qm #  #> w G _A  M  A(+ KT QeF  - )> 4r' T  g p @ #  9 C  R# 80J l  x  v    # S E `8 C N G 6 A KK w m v   U  & L  @  o8  ( o <P n t<6 4 + h X 9 z { Ph$ 7 pc;G 9cH3 ( a_0*@(| z8Z#63H  ]Ky~ 0Q,88:7\Z 9Znl.=[=o P |Em$.  g?x)  Sf[W)g: khZ xq-3 |  fv e  U_lL D 2/ W]F  , B  ` D --   S 2 e jk8   =  s {| T~D,ZYw";_ ~ y  H  W ~X,F`F &4  q[ Rh M  b  Z > ]P   1  %^     q ( < \ 6  s ( L * p z}6DZ g  ^ g   B WF  (    e .  ' 0  o  O? h Lq  ( MO w& I K  x  9% z e  D !  { +  p|  p  {   y7 +  K\ 9 hg Y      2+ L  o n1 -g uF y  "   H99  k Yh  s  z  ; N}~ $P ~  M f c0 u| dX : wyGL }= zz:@ tkY6bP WhA3&,XkG Z4#oI=!{\z?hN9- ; H U ?p!U 7   RXRv ,c #e Sf O g ?Zv'U %  0 X @  (     XO ) 3 Z <iia2 Fh{(qrZ_#F$?-IY = 9} : K ` o . K  X 3  C=  7  W q} 3` @G  wS |n 6#7g x   ? a { p H -   (  35K 4! /    ~   'X]  D     " f Co  O ,F 6  l s   e YY"    P } z + q e b L R  ^G  G    7   2 A  ,UG B Za +  ec `$au  A ) s * v X E  ] 3   T|  2=  h   Z j z ~   v S- < W C e   fB j[  ~ 2 {)  jy\ ( i  ]Zu/:   ,  - u}  <j ( H,? K ? v3   z J  | h  k ~ * 7 C {+ p  T! ] , I0  F u 2  ,   v TI X  ~  * & p K z hx ]H .D S x  b F \  ` ] d/ d 6  = a , R  G q y a  R >e  \\ b oF _ ` R, _ L p Q s "D c )u ~ G I{ k  @ I  ,    c   9 ?  r K ) R  }) 4   /f l U, a 1 S  ~I @ w ) t~    }  $ g ) FE ' N K wt {KpQ+:+ I G,)   e 4 6e.= , ~ v7\%M nBNgB8KK L d2>7vC  - bfrx,  6b_Y SBH   y:lwdK}: Yh{vM]`; by@|ce}EdJ-NZwmE.&MF-oj-)xu'8dPQz&Gzs#n  a^`oCKy ! ] R e C) 4Z TbzvV)!hP Y 0mG+) !a O a[T3cH2 1]L% $D pSL:>wMgW/1kH5Bq>3@}RZRV?KnMvlgA7k6I"UQvJb;xd61 1zOm\Fvv,Bo[J/Pk&OX^Wb<@r 6uO jDraQ7z3E\e8, PTdV)cftQ#cfJW5?|aB|eTFO,v" &mWfnu#S`E#YS ftC4k,H:R _fd/-vc,Yn^}*f&~.!^ riYJ~UB=F_@ &JcEk S}; yL1d%R(7&`rn{k J(n<QeUDq?-<!nr/x|v#baAj4K[I p91a&S3sQEX;42o`uTIPlplJ:qN`qF}c6e [N_I-ZN S tB (OU{lE<@bPXW 3U9  -t)FLHq2 O!X ;bHA]> +Jfr4W`&p@X;[8 8/&/8a *^TVEHS\Qm5b:viQD \6$6thX f?RX!GD973&/6[LMSIb Hm ?*/ mW-.Nsu'j^<[E  {fj] 4/4'$ L0aramUJ6iJ"o?AdS U : W<C [ Dh{%5U 9 _)6 M = T#*?J VDGcyJ  ,)U[Fe(4)L>H)Ud9t"Suhb ->s1 <#w 5=O}4>8b#2K;xj2}`{fK=;McQ4\r&hC5Y?D#__[C%T9a[]! . 9]rgl=[9f{ apTa @0Ltv$z#?Uf@R5)YvmrZ 7y5xN S_lX3~$ C p-7A*>V5O[ V  ,`- .T;~En g] )k Y5  } GOP 1 n X _ 6e 1 C  &  R, ## ` | y Xz uI ep {{':F DM VN>Ew u72e9mM, N6.H2K/j-"NTeZy3, Rlx7[PkqZ$s?[hh}"_K^\{u'DB|I*_fjq "VWVeKfP:G;GJJ2K [#EN_r-<1Q5J1T^IaaczO+gtA:RXU0vK? B k "GW     V S ` _ bcvg$yVQ_H~UR2o 2 l ++0&{/pqPbH#_rAw)6#Qc<6`gZTlKNo;iGfbLCYM!^P>&/9@PF[TJhYEN?Ja<=;3EoI H5IMZ5H@>8qF@Diobh=Z6pWA7uxJDaN m?),L|_w U#zwd/&$ ; UX > rS : $ 5", x R ; p  q   h p ' o0+ L  -x (^ j N | \ O d"B MaO v aK+OX (jgEBm~`T5kEn PNyDtIJ{P8 j g2 g )5U4u&#vP +:;Up1mL?;8#`iQv5";X~=&X^ Erb&S!J}l90 _#F,3#obIVi!\IPcUv&p&WQl Y] c  Y w^ H w 1h  ~E 0l`Zr&OC/^urYs57L9bld4b7WyG]$5s} `ziiI>xVbr9|RBi<4ZWql ' ! h B . p Dkums4p;b]j'\7bon=%A+#Iq\\eTzDp* )8Nka @|;ditseX{{VF4N76mn}M)V Ev-r)D-C s p [YlNTLe}g=aI k\U M\5^CrU:/ii y(7u : $vFU% \ e2{ U] e ZJn L{p \DXL Q0MqsZ1  R ;= `h  _# ? V w ? C@yL 1 hm R9g7F<oo{+ ]= A4j "FxFX`;B.0`@8O$HA#Z o I0 6    ?) 7<  n'6 k d1 C 86 *@\i *{ 1 {  1  h" 7 <w f ] 3~M `sZ ^-yg1- L dy X W%c<G u46D]Uyy! r gMCcw$)/c:{d0 /BB G $ b\ db:vhb PmX*#o L C [TFA[XzzA S[_SaJ03OR ~ l !>A~t1:CG 1 u42D ,`xU[ )^ j pZ kbf u'i;=N19HR>uRb%55x80 N2H [&9'$Vl#o-L^3m*^4f;J%xFC1ByXB*4*D^G=06V$.{ &p^x||oPI#Zt HJ%!J&:(gtRurq#0LgeKBGBJFVB9{:[uVV$^E=r9RX 8w!#P),Qj %b}\dj_ eXcAz/u4-(@Pn/k Z~{-TNYCP]g~`%~=99Z2m5 \g q6+8r5 qNH~cD}U? ,D*k)xpGi~}Q />?o 6+ XnE%Cs=|_)^Lg1 yXZs( ?j\ zST#uH[;mxB+%"qcs{6$`l:&\D[c-m9WPcjt^4!oGh.3t!G\6[["7 pzH2r5oaBpo6'w_5Rw]'2j (/ Y{f2vFOgh%Z9>U Lyr$;[ }.wn \U%bA{a}90X  DxW JA<~?Gy,xaMpU,1Iw qEq%]'t1$ tki2A1Q9N w slNQ{r+R 9R}{1";M Soe99Kip+T`h_n9^ 8cr < 4b3p6oD&}R >UPo**52!) e:OO8Z4 |'_P3[:Mt4Af+;K[vds-"s < 8"((Rr1<*g;_Y__y&L6 a7u ad )]_T{=_ [p2^bu?s{U=r4iWBW0PI:Gzn7(_nKV4[\2-?w1h-Web X"TYH?~Nk&;VGr6B46skZ8OZ^ D=2(.SMFSL}!$"Ah"br<c//L^:o0t      z n ) -lj z z E  s  Gb ,: |uu ;$ Y ]  T<cR <  w i S 7C $ ) - 0 je V ^ eq : N Y O- f q #PU ^+Dwd#X7/Y`j:n9akP4k&zdF 8{w\g3 LM!m4@W 2PzX0c?V1{o_ KO@Jk7j.~t9~h0TQ'H>6`$5@DK;14,$v =kimay#n(,S :eOCc(VPn\[I ON,VK_3}mXYHj;(z"jmLsj 2'bhrdzk`%?y;aW&tp"qwuZ&q*Vei1=2O.C|e +yNAK|z{iwAwY5Jbu$d%C4t\9 6XCB9M$7 RK>;x3N:@+K^:;m[8twja!xs^l%OMx@"UlohRBv*hg-gOWv" mp`!v' +0azu&"vh@nIVOa8>\q.gDR5-#Dya[.6B>qXF FLa5KLn"f 55r-.tm~{wwP_R 5*A>H.. Ua09Ca' UK-iuZ]#6TKA4c b<&lckl0VD~8>Y|8cvC#>Zc&ALGoF0T%HD #O5w^^jv(T>[-y#t7<$'hG5R $<)17%d) i/[tu"^K 8tm^J^=A -=6dw,DsKnHM5P*xu.0;GRcR kM4* >L{+tuGa0n& <VV%KWYPw62j= 6G!%O:Q ysM <q=M ;1<UgU6DYfe=R-/ocyc:b.R<6mK]hy"Kp sER@]= ?7 ?  ]2 e $R [MJ UI@ ? J y j1vrCq4 :x)2De4+2 {PK   6H {#A >Vg{]D p:OOr?%`:Z1Ih gV=nc 2_ kD .P# >Y,L- ly )ol2iF a T  G-} L |[ ` g GC  IUV" ydk =% Q X' /hu? x nt P gU ; m  \y.B& ik O t|8:% E h/ $;O|JYCIv QgStx'$+k<gMNjgvH0"Th.1&~P*tO a~I@ CAZHJ( Q> `_E-b,Zp-.n"zsp X & ]'i} pc37S N 8) J : DQy$0 j" r  udf&> Q no\06!K     F\a"/,/ / , y F CEQM   [k2)!7H[kpT3pQ7\su9 4Rb")<dZqd*.Z^8Fdq|7JZ2Q9) *qie]j  2\VzbPS{}ey/ fh|n1@N0_] c[>,qn`Y04 ?}J?[ 8O1"i%\a .#uTdi@e V  4S lq   e =jt= d G x  M CT     t  u sP 4  *+ O <"F(Q     N ?   U e  m  I 9 x  $C  Z :#Jw7Z{-P%^* Y0\JqU : OfDAnlsrSe q &h|x*(nI"Gr{&  /_;  @@5H{Svw=, I$g TXFw1 Z Gf# L X &    F UO | |nD >j  ] H   fU 9 j V C .  MI ?  Wv W a Vr%3 ;  Qhkz V0eDL@Uk*p'PQX0Bw41~_LCO`0!  _gjW h >;MIx6 w$?\vS}  l ! T7  !T'O g  ^Z ;  D  n   8 ( NF f- 7a a  DtGyoq a[J #QeW1K3dQXy7C 3  9 k 0H J*U   0  e ;  q0N s  Nkg ' q$ G 5  Pz6 -rAd / l To JM  M  Wj00dn de:3\ <+SpUD*xu!3p|i$\LG7WfJQ  HJDh^b+k  ; 5 w "7R  b H\* X q a dG  |  2 6o V s iy Y(: *'3 > A~    o LT  } * D -7 _  j /  v 3 kS0u   Z.  sx[=P  [ o B* ) > V T D .F &ai7 b}[S xE%C.^+ 7O M)/UTp~jGM>l[ ,  XCpOZ/ X]] ^-%kx[C48 Wbzxg XFj'a0-G[*JDPkQ,} 7 ],l_DsV1^{YUt`w5@r? Vqb<>U#4i?"]qr4=#XAOh/e7z u|C) pk9Hhhl j"T}L@?qDlBC]lL u!V 69c&v 'rDNtZpDq  kZ"  Kmq9f(Dg! nceKm k=F(l=TmUb dIJHmI^_S6T%Kje$.{G [?j0S T,fFJ"X2)E! yWL=p]/H|9r,pVL;OeIp_ .83y j<iB` (.ub gC ACN1n=y?5:3O5z?;ZM,2o]U ryZy. KL}f%JtY40# 95ST.[D-P;q$Rieu z3rWynS), EL~.! "n3P= W_c3b:LM \Jm g>fv839<BdrQ>+OL R2m Z]"A'[fwZ0`>|4'$j)Z/om |{L ~ 9 R~+O?&U2uRj{)W<V"_[ +jn 7Qv.vc"KBeS}H>s@,k}zR7 &@|(3xzL2XB.\gCJLlB0mA7is5|o1bHKVe:sxx8GKkArz[o;" cE=.= Dp3cE*wwJI$E+CM,z6sXxAH0O.^W3 iX}[G(EARFzoby'\0U zuWL / srRbg7+*u#Fv$~j2^ST{ ^";S1gX+]M>E"Ru(Qh.`!7G:mx5! tzID&$^9VW_mqcr]}diN YTHC3V~;>K*>@*N&3[E_])Mjb/\6&R6JuB|Wd kFJr? E@a;<IwWK{FaN$P*v2z@1Ej,k<:|\CCsEdt #Tl !q-4vR{=d]qN1BDJ^\a+Wm*nS4JJ+C )m@E na; N` uLl>wY}WkQ4$@"  <|<t;2.6U,R'D B=ae':|5g}[rJ5YS?B35d_;q9/nO6Ch~X*Y*|q35:wJgf,Sss a 3fj[1X'=3Mwny6d*-E0I`&fB[:FO~/4&m# WkJG@{`hi>n+]`>Io8 _ #pO{SDsttvU{}8bre^^p0vi"2! "OflU9pr%f*Fxm!=9;O~5o[f-n\ DB#K^/_k_9z86[@FhhA7g'"q-2$ty0EAKo`q 5cuS{4z1>}J>mX@ Z\R/t{F P!Cpk tRD{  Z[PaSx6 iQ)ogsK~^6`SPj-Gr' IHi*5t&bLjG&e%b >LGod yr/8*9.R: lSNh@)O!>a8msaB0upqq L ~Wo0f`gh:A 1o(C<C|;Ig[t)*g*<? n1v-Jv]9 0\:H!@?@xyVNZNQxALfkihwwdc8VmJll}w0kMRA\E.|Bwl&HYZ <JP6S?8 zzU)XP ~#{A#c2 Nepv(OZE]ntba#9Yvm2QX$Q&C{STL!`1?v#~.D"8{|'RiL<I;y Y&Oo8E}O+dw !@)DRwhB_h|FV=~wm,Vb & X!m H@+n*zR%5M7M]#-ui@T\)x@"cDa&X^(znuk"\hXLktMBnL5i')KGgkB/}(9f}xqX5|},YJZ 0bR[)` L$;p4K@h7,da%=4p}(E9c0&v I'W-UTC8ci=v[yCO@Usx@xbXaRjc6-|HoP*T?ZmdS*=7N4o2$>9TO"K(4xL^6j45ZMM,!Wj .RD&G -C`RR %14E!^yY V=y8eT.p^eii6WsX(%Ku@Ve  J6fzs(}6TAov\,6s-"G`;I<vV@mr ^mV`V> Y.Jw:E_w.%0x} ]/hf44oaGFv-Oq8;VPg<$y5'}j}v0\- g;+Ii  JGot!U]Z L% :i04a"8xL}Eg4sH\woj|-vQKJsm,/"ct(t.]Xr?s)EZV<I~"-H[P+P G :hf8Yh\!q'mJW\xT(!f I r[!5'P#lQjWvVh>/g{i:(,tZ5.g9F} VQoW!(6u]2FAEm*hW!bW+A)G !K>R4~m hjt42! ~sI 9tZ-}5DPZ:N%fKh*neK1}z@xIRKaOP86* 2CKA p>f- e y~ <'yu? x  Ng{~?1m PYH@mX^ s ubYa(K  |  eDt)  e>)h  XE3  DA"8bt}, 2 k$Un zC5_6[+|Av ^9nku z4z H&d,Pd{GK u!cz24z'b ;XGNu+A#y[m?PC6W]^1&%EZHdL{-7 yh_Gn>8"lz/',' djRT};E`Sw9+@)9 n~EMyLa_q7~; +1rbF"ndPu_i{r FeqqDixP1"SX`p ( !;$%1X@H4fn }Q^5Z7< }Lw(m-ZqnY{BFD&+1s3AQlO,: RCU)_@d4{NHZh\/=U :+}hM 2SG2whm4]]Z=dGi"%>'MK|:|?oEup$$E N!`nYA2B@rX5PNtW><5'^;9|ws Nh ,*f ) 8 "p 4 7A+2og_fp;U2h`1Va&-J*7KF>&<KzNu.iFFetN7~1ac2Oy].{+Ji48"QII\Y=*$ : /}:z f[C/FXVEfRUif7m~-]odK()<;Gh2^ijb m:&/rI)l>iDOyn/S{Y#HN96R P&yM3:P? HL;1P9\o6)Ci @ K|vuo5T3 A#SM+n]|@MW(? {~ G{% eCV,k| h hj  S k+h_Wb1xFdEkA-b%[3HT=6@44s(X m;Z&4GwT uQZ6BsK Bu4F)]!PvCI,d}R Lx#Z>[zY3 |\|k)FC AA8xM:  AM86R:^>w,$@* @OW IQ>kt MjCxeKDD)WCiG^.J z" U d;dN\  .]X%b*&=GGCNcECQp o5.[kI !OV"Ax5 J20&. H cl B1zyyatl/W-AUW\HF~CA}#MaxA$"q|DWD;n\>96 Dv@YH5[ 5aH-hN#>^^~8{d:6ds?<8n6 ~eas'`{$&SVFa%8$iw:}8DrI/1 mNmQGr?IRDTZ{s<gwz^LU~!^bcO&s9PVh Tj VPG6I@V}fAh{ 5GM 9 }-0""_kV,F 0 G.WX*LT!cM?uJlsIwe/tAvE&+^Q4?S+k,+YV#P/!#f{- a - {Ll=?kfj8^3xVBE%wcRf8 J!>):izy`PWYdN7X${:Nbg:T`w:r+ Q!PLk6hWrSQcK Tx+ nn7h|L}[kjhM @R*)_'p9 ik7 5=K}`CQ %j1o)j +m8DNb.cZtsD*/1SM{Tw*oAR{%Ws3t;1 bHk]&#NvvDg%Bn0>p'8o){~@>`&-[! "+:}'y'Gn5u|)|y/>qOg!Rw Tkts Y3 , .l(7D udA2/K.+!XhUeAh:yV0qW27rPhHCla W\TI$A Qy/]t4HCUr!8G2M 4l&*\ZP+V#!+,H~_i&I>s< qET<Cy_--kenTO9y!*svK=n}dF"Onx o'0 R?s*S8( jF~r~+!{5X*S [/sEuW"0O d3I1j729A4`@p]S$2X_wOyT- D2[\ ,WNR $Ag>BAC;z_F<50(4Ja=\6? cX|:4gVc} [ &tD 'oF>$)h0Hyi:W}"[ ++<*hnz>ee j 2cx!r8 N0m8]R A0    h< ER LQAW cA{ %{Mj oCB^Ti%7 Y;NQ bvA^ $t !49lHlQ xeuO< .$5:B :xcRA# %* uBy3 `q \gU@E c4; *6L/  oE~? B3?k5 )  =nD_l l8:C=%x= F>YPaKa D?S{2 n16J!jSnK'V Hc3k=]Y)< Bx< R fFw!=l) j;-{zQ,FH5p t/ za   HkM Y {LfGfgGu_ q 3 (./!5x2 , R# g5 =xc[tF,3 ^Sw e|85j [{  D *, (i0 f Y  *  (. e ~AR we a  @" E p / 5[A  D[  d C_WEq 7tf?5B/bQekC4Vd#^1)'ayQ]g^9es(7fPtl 5]5 "gB krz"݀z5%pk xr!.{>O 8=CGVbZb.QGF# `{@<l&* u+Q(#( JRW7/YD% k=  PXAb xNG <U CgA(NP V ,s=b1/tPFq1: ?40U:sJBA$A xp|yQbk T06(HP QWXv6dz--0 B* Gx-]_*{(-b+eNIJq&'9d+sKT2J   /r ?> ^0  4 w} E3 h[\t G tG l_Ru 5 '}4X!z tu _u $  ?eb W C%e? =H N9% ya/ 8_bH>3 APIEg qM| + ZS Q tW9 ,=N$Y^~m!>Iy;<Nt=w'!/sf% L71Mj)Mya?#=qod8=a.8Fh/~tfo^,KZKt M[H'xv6R9G}^/Tj"nT ]FQt5z D7iiqy |mI7(&3{u+zqYObU9 mmtsKvH!oxQb Z{7&B{.s`#IL{V<$(64 Hgv cwX030/yT6|)S[el&shn@LB*O |$b..yR xWQ35Tg({%, 5W"xS6&[$IomIkxY-9nh"g},&/%yEPMRj,jNi:f r}@2Mq@^nPy6SA7,1lc V~ hR6JoP9a\U'<h?|,C1P<Tm[=4[(tKJ~i} z,)#S#0 u{`,p&"(Ge>`s=Ls[ x I*BA%_`Y.,MI54+~d!cb9B8:s `Y3}(I DWH<'(rl^]hd^ EBY#e2Ai!D G}Jx-1QC.&o|g {A]yF)XzMJ61WgK|E@^cf%d4SjF-e=1Q^&n{\CI-v~Emx;#7LUq^4? jwAR"5lUi=py|{ZGT0wGwI&I[U5o^)LI^ <3@ R V-$) z^a5},f -{`wNV6j  22YI}1d*?UdBa<`SLD,51SQi\<%U/m\'7{QAO3|RJ'5<UI2t:F.CW;1bI)l8Z]{l@V 1z  GD3 l_N(;V ~cp|BOgh`yJ40k> "h/ lT3gid^_d'#+%Uh+"!e>E=."b^973=K MlkIFD@'2G UHe3jCE[-t fO3')C0?  !%NMJ0=Bfh*iS+#-2#nA}{h ?wd[LCQ[U>_"fKE l=^|C&VyQvL*E?6DE,+ 17 &o ;BDN~tJE0F~ ?hU+~zw8 ?hZn (:YtV XPD: Cc,t?v4@Xfb% Rtos5z?QJ 7[HLW(&pA]+E(]nUv+{6tLTT!z:Z2-B*T+a*e8"h=*r/` f*Vk(n#*I1_EV(f4TC^uRA ^Ov?L ~uvI}P q KL;nN_{,QW(9 s nmC3d J  X (dp%c ~vG/+q)18  }\EIvci ]4^Ct 6 Hwb3A4u[Gvtg:]w/]jq*jA? {_5CjFoC mL:OoFy:bsMQI9^38>]<+/q^g:pz1.V^\%Dp4 .tUU/7yU! .wr +U@A301;.3LI~}{9G)k8 _=q|~ JZxK<Dq}?P> "$0*W4k5,'!Ik +?E>  Sm}XxBuv}CwC'5\|01v$Y;qze}t 61)<|+K:.2;jBj .?>yT4h.}U!j8u2/p=o{bn_ iF|g`9jX`ix+e1"_*:tNH?YP$U ~NTYlu3-N= L)'adXHC E,dq_#2JKNw>|*7.*sJDs.[ /=S `XwodWEQjpazPz}AGYw") '>=txgB.`dDY4kn/XLH/`GN,~g0_F?C'[0z%-(r~VsbRZF -D" 7]>z+t7B`WjHJ|$pf 0XO1UNu=Xt \~7VR$KSf2~&r}7k%NUy$2J!"|L{9wig5z>auuU4@l>/8 ]^ {~C8m( D;H&'kLJmsWTd I(7Cj =N-%Hl)u!C{{8odnF)4,mjJxJ"BlC*6:P$!oG^oRC2r`?-[rw8&v|rtu2(pouZX59l_%[JW^{q"rfNJU\#pW7PK0KN,HtRuv;2w9:..T~=~axs 0'hg6v2I69/O}MArh}}ikEv6~_J"/IdkT@mD+Pn\Ml(=+>'dnn-ZXlpaXEpfY2XiNx"]u*Y6 pY}}Gk7*j!"\byH)rlxR^u!2wTyu2 pbRG~ATVJ"(+t>b`_waL3)eMg%|U&:}gl~q]#q<1u5,X8\Nf*Q~ jyf\P3E$h5s)3 "Wg7V JgHI+HRbBBy6v #h9t8P8 L. uJ+YpYeUozv3 nWBGt10F$i%JAafME&i(y3|-GyG:i #j9~PSQ V:v`CV Wwa~$h'ovN~Q at0aR 9UU:Wq'F;!Sln} UA + t''J+UAC/r7-pDB2O, ]ngGP} & l9s^8OwH/xg, =`G2G51 -7x'?cRq aQ'!7 &%5jsT_7[#U gt4UvCmGTiXSc:@r35I,FX t!& {S;W_s+}|FVM.::$9aK#+G^P(-%2eM(g1mVHU-87Kj=+U0I$%?44jYB-|LH@*C5oJy)B`7?B`b~nWd^=\Yx'  )~A;n\u;b-Q{v^FXbtQkV0 nGUJ&G<l7 fgtc[pCX,7ZBQh:~s+9R~ggx1N0  p*() x7 O  V/iJr: > $;5kg\  tNx`ifPs0z<VnNQSBvcMv+P-=-PYv%,)yFb{9Jv%|O$@"{ iTZ%.qp(vT4?5#8I l3J.-O  "{%rFaX +NsLz^ucYI^FLU A.S@EQ}.jKW-VmLGp1h}K=s!>C!M-qPW-3AfB1(5-(^O0c*k8&B|rXG]0rXfSm> t#]>yj'Yu4 &0='fhWhCMK>FuCsZR14/YZA{16WgW9auvQig^_7grDqT)e0 rCnjl&Q1\-SM89</w"1aK)m"@P/>'4d2&iuB6Dhz&FnT(xAEGDGud>cr3c8Iq3:8rW-kZs[ o@[ldis77}w;6T&l![#Xc7KW6$fLm-1Pgu^3UEQ3Pd#O`hz>U&T!$_ur&` rM6I?CG`+t ,*eJ)8 "A  (a8-+_h?>.`^15T]kjMGZ$}p/\ eDCbi*P;6,Zp,@!>y:ru5|jr{929SL9d46qWk3gb{! ?)6 nS+MW'>u{[%Lg !H"Ct[U"|#.TNq0cd4p>/!AbQ6EwQiMoH.))d%l6e}q}r:pV%ly83I.}"'0[yO"`km$K"zEn6uY_b /O~zCL h};z_# RHh",,FQD?9#eT,V*.5}^Z\I0`Y AY0bX'NliX\=fv[l[+vh~ K  ,DnpB"&0:Hviw?Xm)H3x3T@{\[U^^O&VK|hu E@}U5}]l{ E RZi;c%i h@7c1veA@2FXluS4 <?gV+h> S4V>zH3~{7UE.`na v= Pz1Y;}VFf`2]a ^+0[[o{M~Yc>RW6.,;c6D6B @.>"~8m^!AA(z^=: b5vYDPnF?,H^-qNC_\x:o}Z%KV5 N LQHJE7B R m0 O7qW`<=}:\** F`.A`[{7G,IrU]kZ z  .yC nN # \ '~^z:$&{aw , 8T^BBo_P L:~ >y );/zbp&iJ^ &!7u[^DU6 &7*lS?dHe#$5QinY '[f { 3/J2A[idki..d-;Q @   a!F CNs;|\mR>|/W|]= V #eD7v%+({[tU*+dxp r1z+~b { w+u@@ \@AZs"kOd<M [BBILi}4mICAHCTW>UuwwepTgW,$'3mymm@0mXYI;Q+zrU]cMLg_B2k[qY.c16q ]r6e~0H w<.F= '{(  v4PV!p.,'g5s8VTnYBc h s314BKx/&Iq9:3e5gdDKg>-;!i6~n)6yW9NPK?dhH`s|;~C|p7. J~0fl|:De q%AN9H>!Zj 1 YlE$u= >OlH x!u "8&G>Wf7!`4UQc^vx!{J9R %?`?E=9LzE"Mq)2Jhm$<fqc% W\ 'T@-mL E`op#E ~ qft#1j"C!LmQH)-thmX]L=cPcP[$56I~>02%C%lr]OR&sJvE;MY %:?\B/,v .[Q MGtjxY)T lqvs#$+cEyRc%cHld7b)^<)p~4vh/erw)( LCE3O `B{ 6Y,THje'0`"Mp!oCB3{C-j-/Q,h,dl K6`702z l '48 :t $T8G96Io1'#D RInBm!3_\rwh`g3AS[Qg+pmAORYe^d7J+Gq3.!J?qT}*B^r{Cc{?K+h nS+0oJp7]k,5S*ZY& 8M$q aMTO ^!<+8]f>d e6 5k53)>&yND4 % O%- {Tu'P &;T^=G(g73E:U{>*Q| IG ~^4dpmu'w,xk1+d]8iv, 0QF rC9v8hftC\d aSr{Ne5lVe -ZO"C6@p*8\:Z1(q*!* (:FrwOc(E9V]%T:4QxcW}<O> U *<sD5>[I*cO(Jv6y& < aYa:= r''8Ig>` +pebDiD( S D r  sUhVAYq: UsDEsJ&O&+q=`BM(wX('c84 Oo}A#(`v o4" -:vcj,]eGCw9+~~sWbVz" V#mXR?ZIa'mD $a\YDC.h'D#p1mh<%\0:6N-7B >'OW(R hn1`>3%\q0.siW0Tm %] 7v4JBIc59V&U [0y0-mIb [zMhp +3S:Q`\%2 d>qyIRw&/=]bPn5?P]4%UnV,EJjMKF Tt?,DQI&D;oPnD,_9*pY+fgmioLABH0v/' 35!Z5 /9WNYz\=x*:C ]Ejm! '}mCqjSLsIqc3#mN!nxY"=Tipld4\ 34G r mg%*n%5_dcV8 _a4MnLc;s9(k]ll6#6T >mSR+R#5Y+u2%B:#GWHK)n $(&oGiz<pj1.N!R^ _RwtdYXr "_[e.}4k* #i<^Cx5_EE]/s=XG.[,5[P D|?N D"~Q|pNE (.{]Z q|N P0]";z:wnZQ{1N_!^ es)- =O|dHV3,V\&GSy3fedZ=T=o1-DNi"8Fi ]rg1i8T^asrj$Gu 2(UO+gSkg'gkSk3C^y>Y+cX&H^2m3R/1.bY (`_k{]@==2& `ykbYt& MTei-h> bO6-n!7WtclqW u_bDb$C~ 04{_#l5)7_wi=3v %#m T9&/0CNq4yNs 'YtI=ge.KS)A}8Ye &44;F Q0;n5ozP qoCQ(b#$9#M ET `8wMzBregVg *UIM%\M-rUg T~ |]PdG(7c84P4DpF/Sar;^gfEjE W[Bd!3G 2B%bPgPW4cF+9S%JU $\7`Kl &t%*@ *|:V-d[`P>-.2(<72{n]o4wl6fZAJ)M-~<?53( JK)U AZb ' omy#u B =bo8Z))>|%2gTtj2p)dv^cX ssio<X,b4r//Nu[Nz[YMddrWsJ#:y[=mr pt{.^h 1B,@lm;E/7,[fma3uBk?>%qk6F6"#d9u`d(ub."viB$MS~)Q@Tck*}P:L9rlVt`r+v@|Tj 57GB]wNjB Zymbo'mA41)8$#)Ntt0dyw'{0=!BVO2x()SUb=4}` a IS`AqjciSS*.e|. g"NxV6h%QPULclt zo r guGLC@$?P?,(Z<~ JYS9FPB]7A9TS0O W ]n"ip^- jPiga4a6%&+DIA\i<nK-q;iEV.2ZN@ e$` J?16'Gp:/QxnMrpD_t\^c q5u?b6+sb-'a:K^:oPYFXQ)exfk!UfyS<n7_,a@=l*#S"  Q3eJu/0b_u U1Gm$!4i;D0TPzy('CCO ZRyC6]X'f. FVjVudnmYqi73#xalF&aS"7Wqu% zTaYcdm{K I$L?lr@ /E%lL*DEkXfKOm:BLF#H'kT f t=xW\,t"D`lYe2WecJwUf0K9T_ARN.TUu ^eEjmxV&&Krz]Y|I+kbyiyHUUo5!#_F1uJ3S(p]K)c0I X h*Br9R ` uVEA'VJ 3L2W,Y)``5d3N|v{rx.zg/(=i)JsRtR#^&1X0*y>s?dt*'7+;73&E}O`T) Oq(N7] *>gF\]0> /j$"->>5jO>mXNfEh45E?=JgPXx&76q\& GFW#sWyx kK/  pq(ze?hpXCpL_.OAYl9 :&VM&-hQX{4_7tr!!U0Ehpre/nIm5X=$VeQAS_,=u 6z9si3UlMt#nDKBN]1BGJ|X8xjVa?|-1g.;M\dmes"6sr'~ M/Z1Y;y2}qOFr@ ^n.zmCa9aXGJryxA01n <+(k5VEP" t9Je {w{rI ]'_p9jinwiG3s]]66W,_E~)a_j(DM Juv;~zM!B}Bs[sg0+Hkag9C<,]L_+\vwL z &l9sg}:qU%YvbR\c>RhiG{Y[k!-s;y~e{)=t,qG8bZ'!yis+> G4&3d|)$dHT3$>$|uw`7/DGgs}eH$j{p?L O]JWP2DI-i}aq~iD~}cFpl#F7z{TgXm`yEQ _=wYgC}.^ , {-W*3B9qg7o:*lu&h+GT'Mxpc'hW!KW-p}EH8_?9 5"-2*<6nANh['(,5T,3](PaK\@8 7S'N^2]8v8/=@3x`.-6* 5W!Yc]]GL5#(F)K[PU*g>&3%LwcVb<k6`@zt`8q}X 5 =Mb{EJ{=Le2';Ly*`F7j@AT@6GY9U |. nC. 9U:S vUDpR~EEm's_` 6*''3@p w~=c[?E +7;l+/n U.TWS`cc} BPqpxK6>z_2ihCW~SyiRvPXdW hbmGS7tT-T}"s=j>$i+(c:] 4?r-['sX\FX\f k}{Iu5sbKA`2Q% *s_~gjac r* RgSyN.~- <J_+WO? Jn -#p_9`z9$R'"%4Gldr^gei X) h}*De22k:gf!bwQxwW1NVsE #KW;`H8@Sk0d<Z$)B;omj$ItO%) " !q%2{{o1YMj;QnR1#!KLGDTF1J7M/Z]{yYE 7Z D0$j Q9d'&!M-2?I$ &[ ~iQM=N)!-:-10oVF ]LyS:E  g OKp@s3spaYc*l=PggX*yZwJ\Nj):HPJJf+e :=Of@j%/:&nN-asr?^Oa,{TOg{&K *A70pRw2UT8Y-Hx{G 4m  1 Ft*n&P-i8{WkXlPVpSy>*aVQp!SIL{R'{>$D~Q )4D*  -jhuh h +0_W-T K??EORL-.(4T=C{)9{0+hlJd=N Dj$[o@>XEzFy;zTKT Y+^$&oud)O0_<ZP+Bt(z)nKX  I w0 3FJ-|"7a PoJ!y@{Z*B"C*Xoquzg d|~,-bv#'{##b:Pe1JjFJM*&{dl]ofkR sXgL)T%yla@&#%4=;oP?_SL#'U$YHd/=(h/ 31VK=mNzj r7yLTLZM4'J~#4yo OWj%aV{lPaVX~;(Jx>q ,QpP7gfM[\nD[cVr[}X??~'kxqgrl,~,-:w5MXFlpzEY _m9h+ }~dT?bX/T~xGn;1OU"]DU6'OM<4<I:tZ|E+2:|<p/L.6P=Xq|'SLb;i!i{\ c8{7'/wK>$#Ax"FP#SZ9U9$!G`<2u 1))=B-8,n%*EFe-T#=t! raw'8glZw{B~ZAXe\-J}5TJjHNn I|Tkt1Qedu(U4vU Q]E M0&mX&UN-"DV #@M/nuL x+FjBR~._|woq|T%PvJ?&=X b eVRl,OU`L`lo\g(2hSmIs ydC& NKSal2DWPb(<dS7+ b0c<s 43'}_Yz$+Z4a?+@ ~RZ(MQxo)?\kfC0n>uG) X&!_y6pz}K/K`XOUx*Lm 5jM)?T>FZ1t'/(FD"1e[6oD"S?#ytwdS)J3p:q;z<r=%> `.ZEY wJ8LmQ/P"-4e!N4zj[PJ!0ahB'e<T *DRyINSXV;SivZD<iG'( F;, W$f ?GT(MaK>R'o}- i9N 4 6cX[?"=rL\ Nwh47$X7AJK(TF 8^Z\@A7EOf+DslUl@Qj:Me2 9=HuJd0pJ"yHLv! =r s"[nw9K)oef_ : /7)%T+Y5d|PWl6[{uK+ d{Jy@# klum(BJr#Y IrC\QmT+}sPG;] I#}4`OH,W}i#8QN8T5X5=?i,EzD%E<=V[-a|%C~@~S /uxc~IA)nidur0 z~Ljo 77vN/N(Vc@_ If/="E2Xg$Q8b#]rp?@$J73//; JCHSs0 w&OZA".Zp@\k=y1_qK `[Szf2f"/F(cKt+("lhEA?Djc66rNX %Q`(7O(P2'68&|F9>&h)="'m`8819hl'6ScWQv+vE{O9Ps0%-bi+\nlGea;g -'*fYq)+ ^\(#i%7XWNf(KJD~L/7#qd~8fLR/9`}$B J"qeVPZ ]746UF,+=/xFD-nL=vKUodBw}'{ )>R%ukStbUwP)#]fP|5Y_]2PG6ay|u?Z| ~-f:;DTaUk 2bYH^17 ]U0'%-D%7l<Q D+:>WH+LZ._S}]N8oJgFA/ ]N,}J9S5cWl`1%HP3J.wv3l)**GY\u UdTDPk_s7z\[22<Gp(x|*]N\,p<cm-9Eq^xJsk !!>utjh8f'w@*XeJ\k=BMM-^U\`Lw#T"11"N6zRv?&s8FX| FuShc(rd"UJA&LA9` U:pgX jj'=cVQYB zDxYxslPQLYNgA! of%_;b0i I G[Z`iFJ1<BD.kP7#"Z.Zx+D_z3mcT:w+qg`yncDYD]Fnr9s_,]ri vOW8d\k]^2, tIS&4 $KvQ dWR2]=`@>< =$,.0h!jbKDiY }{%fPtbD*D_HssNk:OM#-+wcK RmiXsqU<W_{,Z =K`cuk-`c ^vk>T/nk&v^nJyFQNW E!N,%bZH6U_&AU hS0D?uZ- uDq6q6"^Hl;-kmn>nL?s(O7FQp\OyS  GLJh"p.3JgWdjI rg("}zJ6[Fb*rWYrmHDG j U0KX\ormTW l7G)<DiX>#bJ&#Y\MNJ> .|Hm1^E/SKr9?-il ,0!oxR"`B% l|8$ FgDp&T@*\l\U@a?~'|q,H<L^Z2#Y6'$[?Z6QS&TY/P<k;k=n ,`PKUFZYvd .=B\B:"96t"k<J.z&vnYDt!\+o;@E(&%RH:{}02CV;Fy^ 4p =:[j,hL #|";|Hjc#g)~YuFgf#=Gzl~4pv (mE?a!j nf x+9X8Y"T7T}3~%23 AtLbO K'(cfVVUa5(dO_Qq)nNI$af\OyhYfZ0sO$vX"=?^F#mZc-&[xdw_^(!s iP${ug`$hi Q9-u-5qI|E(#PEHE5! U6- {j ']x@t A(IfE) /B;I=h#myt]QCig$H1moGDQ*E6SJ2#vy7(p?jw7;epp'EF5>pyb4[QY$(e.sbAhT+C$7DGwAhJe6h B5Dt &x]8R ~e:+h( wC2.\b`)nyQJ2ev"Tp 9lZ;pAVG,)oear_P0~3MW*Rnsp):[{^?Hq>9v. >FQ #T  {>WsiGMx$"S]r5i]zbcI aveJs_jCy1wHr=:Io  C~x8X  `sb#X&|} 6FZO/GY5A2+mlsqMv%JT!/E$d F).Aak`ld\]m-)DV!,U]^\LF~ l2JWbjb/'(~%O +E@{ZVf -=/LeRO5d;Z<(UmV.xb*`\-3uskmEw}xN{3En.~Py`u.:_|{wO-(GK!\E{b."e)Q1BF OtE{(ui)]uv#N ^A/&(#*P} 16%f}zYv  1oFBV= ("iG N qs)i+Sg }L~,H-,kV3P{_)Dcdd_9{^ElUUsCdKCy0 -:7$kP^DA3:3DA'#m@nU< N;0&R]wf#ZLUn@~/ 3J(#W-3.1PNB+wO.S*Dl3q7|r,Kc1SZh[x\MWn;em~<x #^V\5ES=QcF"|dNkw.UKIk*>k3^}z%m=hh#F{2 (DDQm| a{'GcqA :W 4wC$k>]lMtU"kDE_MfY'Pc{hc.8uw2x,(RvVrZft.$upu6iNKQ1?A!nJ*xNsX ol=`fNn?t?z*?<Hhjs0_{2T`dTS7t@ :0"B0/4t$B_<0H58K^2HVO=@BGaMn_o-3Dw,wHx/If"*>~2/;?$(`gfE$v^3f`k"S9x?@{kJR4ZZY1KS;,9N) (:{=\JI^T$b(*QD>^%)& T d5?C?fI)XoJXMD=2qy8/Dnw;~6h$f@ PtLNAwyY[*ka*"<g3 ~Rwd^L7bi#l c'j { )3|RO,&|Pve_^D)T/h~FY)ZWs/@t(31..g_r_VpeE65/)9e\ l4$4F;"8609ao9 a J N%sxbl(LBTtG kJ {B'Q5ZD|Xtt\Dtg}u8@FNOIawFnf]Kt6}Z,WqclqD5pk&EqEr<3;TA(oo+Q1x^D et[<h"3`lz*,@OC:-QaE?M;^  _x@q;2qY|5lE4zO,S3UmW"t6CWK | M%I ?"G^<xm zV+xLBS-?BjU/'cI~a\/O.2XG17$"`eX.@=_(L"@K^.1:s>7e@ 7<wl..-& [{2};;>v,HtQR;U`ANmX z{&#H &fzf{2bf;A^M!VNqvmE )l(ckL~+rL5w_ *]6bED 0& `*4Edb3ay&f 8'2~]eo{.i4a,cHn.{Q[n1O3jV2E$Z5CGA{ =6S>PSkjB wua9.GBpS[ |pe7ZxgO.B4MwZLhdQ) r" R{ [E8(/W3xM7s+M kj 6 %<[Vw|zD\'W,PKY yHA vX9gd!{sOkrmK#zF;-kf6[@ aO26@VzubV 0V$&B$]^4>#B{%1nG?hVd'y Z14& A\HB[SpB|(| HXJwiIDoNw .k5IbXbfIHKIVoi@9(U>-J(/8DK+4gpY1-,O^( w=76+UdqxD0}~+oT? F4gFIL\R{_{b3[_{+n#[}t\J>.V)3MDqyJ QWbIVPF`9OP*CODe- PN5aM<]&5~4H SJfx?lYe3dUWqt}TwD>J9 k lGAd@?hqH,A^vpKfPZR %nKE<(:8EzGmLQr x"w<}E -K,cI}}N5||"%&$ fg)d YZ$s{U8J )KPX,BG*j~J/]F G`.O`O[c`/"e3G1(VH?6m,"koh\):@$EsuyBU q)hC):/.q]#_l 86{FdG4RFqPXo+$YO, m#w  uhK^nSJo.(;#6P%l:W`(&/,00T:A@:uzLaY8:f X/V(~2Mn$Wt@/}f.&)V0k30S2yZ"6J"vbOY14ro]@U+~ q RQ@ LI77UI),Uj-%!<: J7>(v?GCoHbVAXmx|, X/3,F$ 'Ye}m)Z)mc"-*_@l/Q,u ejMUJ]-_]6L|#ep=AJ?2wUO , +P9dMOq P\xv2THk^ z#]]#Owkp|q,-B$/u^?!(L,y'G%dVg" 1-c {.+>giE^5'Bh-}/x^mE:9#AnFDGBhy^ 0[2C}g#/fFX;!OytbL)qZcEvzi9NzLb!T^p~ <NY\D~O lLmZD7cKO d }"?X}@(H9_EpWDk% YELGH"s^]|\~LI\$& Z_izP uDe7 F$24 4yVy* =+ *nFd?:HM{)hrp#~1 V!^}Ss,- dI5gIBr F$t%=5W;Sh\VPA.`zS"L!~RUN +l$o6h17nnsf 3Y 3|a+E]o_%/#U&~|i`_~G3r] b1:,q/`,gg,c*!}1]x KM. zY %6`Orfh!:hC.pxK1v!jA.TDRQ1-7>});8A['hg[enzcXh@ :@xz@jx%v`^1;x4&"kSH"R=h%60b0Pw6{%%H{hv75t<J@1A_Xa.pTTx  \b~d="J A#|0?e B% Ht$w0+:iY 9W QMedzM+!%4:=X3c@}AV6/xg!Sh?f,Sx^iVm\@,%.E>Hv.'{kZr8kUA`p'v g-_n/jwC1"6,b oxa R+bCF`DE^T<&|F.|!aSdUu< m(,:.KFPNMOh 2OaO/@}qhNXa_1@hK6Pe(oxq "Nj Nn6{q*T t{> 0Zp5W ^&`e C iyR?$s=,r~n h7e@ "z9TvZ'&KBh&oDV $r`mwvFQF/%?3~\jn3Xc*f+/nbL=Z=SC1F3?nk5i}sxL@dZX>7~fiO*vnrhg\5lIOF8^q&vR$PsXmJO2D\6$U) \!h=0oZ@#Wu6uAQaCCM{[ ;6@d)hE&P+E)*'X*&@&d48.X{$PQ"v7|!8 WFs)t1$x.'S_Y$njp0%}W(=7z@\=!}=/IjV#4\M.|KW9h ! 7w8:$Dc<h0Zl&KMW#* k%a%xod0 jCSu,VieW r[Ml  "%+.&I64 zN%ajDh_J G"4>z)(7,Ned~XZU!Fy2JKE8{&}4`o 'nfbQ vn.VRD$*P Ax m$|eLoFGyw -k$!WV<Z=wXjeqke/0zZxvr"rHno D\ > SSb9tl? ,3;)pLZys7e>dPh 9q<ZJ8UKr06[5f`k4Bh`1hV{;0|}M#\L+:)+\];f8/eZs8! '^R L}k@HyGbVN>~zK+<:}bI=uo8"k`GV3Zjd//IW?i+0[}}PyQD6s5y~DoD\zrzx13 `Z^$Vc'w'pk}\"0{vWw+-Z>b ae)#B$)F0"nR!5@c 1 Jm5G[)|j Y&; {^I!1d4K"V'Jq?(]<@Y&}BteK~ugw?@ei|7I 1wR24r%j+fC%RD'nKdF`3[z*}$K!,&t5kl+[Qh3#W&W7sOey<-,OHqBG#7lGs1m[h01 5wZ^a"0'&'Cre+.22 $?qRP^T$[o a( 7MPYE1O)nMl]vds%yx3|s;boJy5-mD:ouggAA6yQLy^h>l <> Adp}^)>s!OuMstOPoknJ0>4fEV&zTbj1} YrIp]Hr%f){I+^3<f:}Gt7R8 O77H0@Vu^/7jO:Ol_fI`+pLGC'rjhjkkKTsFW^<Hvlgi\N csh_g47!}?,D$*r> e ?dCP:[(mtr6*0zK,lCx293O$v24ya[&N;Q*8*iJ%G[;~SEi8ynOLNf,rR0n>.)h:jIw+jzF/5kkjg/nwx*oR4bR{{foyF5JzGujJlU|z_P QEQYReT[pfMp:\d^hx?b6:E+gzrle. b_Bq%$.'=_sw<)xT/^1_epCU35@-*L*z|QoB .Cq&QzJ50cB5 Q3LPd\# /Hq {^nfQ"l_)./X*BB2 Y.7?(nwxO L#[/Edhcg~|zp =} cVxd*m%m.?de78?7jJS:"bFf JVo/a*830S7_S*yI_KT vSu^7"wf};A8aGnOf8-F^D 9/ K3`-+(!Jm=[qOhC#^O\Bl{SUNd.MIZ 4 $)Yi sN\o^Kv1)T6kP\uy o'bFvwbx9}v5HIYB`|: Y1w qkv^KF-2sP}*THxN$4@BV{a(wV}e1e sI&+.4~AI[J3$0RUb+ok# dUa~e +3da8&]` @^ {^}= "5yG'#+#7YWJGu+!!oe~<v5o_qz5Qf4iCw67'qIO}{*(w2)F:(~[{2DnpHn6sfYeR)7  8 ,?'{`A8!0tqc&CeL7 k[qwPr ]%$$mvb9"S-L ETqavrWP @k2.:PUi%P7^Zs.c89w"l;kbf,4~Z#Jmq!E'=' DJ.N08L\iP-JKtfXJvXxIEs@^B LRI8c0^PWvIPTp=6;eF0L+-E|q(-ErBV7ni#cKh M_GY|e6-,'5>r~/K|F]5 O]@2%^bP 'f UmsEf0o]`;W@^! # 9&; gx/?ZE-/Gx)$ WzAjJl`aQw<W O=0:+mrc,H%` k1})4$x^|X<$n.w7Si yP3I|` OUXAIf Hopd7fl > 3* C5t|fQE.?4$ekR/<02qM=uto%aL%,[fiAn{RT-n%(%]U'jO`33bcif{}v|Q|c9p=mAlc%P"hM#[@_|#c]a `E^tX<@^^? R_zS@Re#emxwptQF-CtCHgn!= v>hLD Ni+>,'6pP<e_*/E;B]8L]L )>J&/58~c2LL{](}r7_)=]&Ut0UVIZ!s9lh->WNV+7,hK304S^ "hA3L|iC|PB+*7UCh2H3Yu)>6KInwNu%CA\>`%VL4'Zw @ .P-vENF/;  r23i\wd9Rw3Vr~"kl@`z'-'%\|H^MV2ZbFC3%]GaS umW* TGm xb~xd%<Ry>e(^OFm`%UUu)k GQ7?+`TG/uX(| ^ a`\U2u,z,A 2ltGuXuXtxouNSfLE"^(2mDyu avzx'hjT:?m8U`_+b#!Y(5CJy@n}<AEMvxpn AE4>[@F8``ZmL: R)F:7/hp=!~_ Lr&=> hPC2\^\mTJzU`}6Y- D'bf3Lj{&K |~DRMN;e6nK~bOcCPx2 1(  v!R MNAZ' #cy b~*-j<|T[BwUV'Oz&K5wLe=mD-%n*$VU|9WLlkba4g}NR~J";-tE > +^iM,2/g g#]$q{E F!MmHQNaX@Tlbnf5VFn/;kuuJl]>iS$Kx: pq=XTO\#v,Ena'~q=7\\:3v=s <6[ 7l+4E7/T q 'iJ]H#eKD"H^C&(E?)v9 E+t+Ts+Xa%t55-$oqxxV[JsJ4xe +b/T t y?*+Jh3  h$LZrUi>[k{pqo >>!{Wvv{.{2,~;/C|7Ygn.J0}{NWf aSM\e1s@+Zl-VA[}DV}*4-o4+^T3]0`2cPqrZ7Rmn|`$%_&@/e% #q+ZOY<5 n"u9F QZ*! |l|(kSv~JKK1c'"6..R_a\\@-\3xe /y~y!=A I}Zv \_Qt@K5&4Po\u0> (|UVvQ5j;~` UMvm-,DQWTi919d# }39#,r|zRm 'NhAyPx2Qr&NWvC &j6NI>{O:EG4zj[V%si) Q(9Bh5&Km0{]xukw6>RJ[c|y: ]\" ]X %h[a3nl{)\OaL.0U`NZ2VDB2"t\mcC4twmr!CQ* x 75bS|au w76\XPxC5Q/<M,+T <MtvwDULle8w,9-`e^Qzz0.nx&u~XO)o<1>6:X. Z`Zg%*dGI8HI? No;>hT+ n#Nd^iV.c B CmLq2p3I9L)R\ Mf&a./` qtc`%-:iaN/4vr74m#U<bI Odb <[dE7"MeQNs= 2 5]gNW)Z-3-H:^3#'|iqQ4A!y;~KTy(c87 H1w?\evn/ p>(B/C!brFD7p#V:ev#=rEwYbcYRWoZBo>o3,2a 1w&I.`&@tAsP.N@V5R{9( |V7ffB8>N&& D?([JK7QCWa3N zn"sY/5sQxh~.%y$G%U cXAN'~g30,ZOPZy IQN #;Md=o$O&{GN@A[8Ql2|L.{wwMxvB{5!.nwk1,MProv|S#gzc +1iM1E|5xO?DTa '~aZos0QWwNDBX) 4Bd;b,W- q@2C  *}K)1*4k|Rrct_f]4_OsSmr*/P EO|6I1KzkJYe|OzmAUM_[?!H=dN&5K&0#30`bqqbpQ}'RdIML7~ F` OJ/0.xQr!6Rtk- AJ4.~$;}v v 7sfVoY~RHzEHen"KgR%?8#\XZ+~SOvc6k1*"4lN]jZKEbXO.+_k<0C:!n}=YrHb [S:[A`l{8rQ,OUFl?k>*: }*=A`o*)q0phF5o]yY4B#NMz]t%_'G}UpVp(t&Y,(CGe{)9uR(IFsLgw" {\~rjX([aab/5u`TX mJ7@?[[}*Gw k5JtUOkaJ@c\GaqC  ~V/%v _KKk *O}$e%x[2yKC|`aUB*P6I;k..}k!mz}J %:}B*M N5\6b"n BR#EJ "?+IvwcXMYW(,pB1[x! dB$qXg/WD:$O|i'|0$6<8Wb.B 0Iw/9*l , cGC@.B:K^CXLYT_%\(iI_I[6,V}o ^=24>+{ @,oE|9 XBCOOAK;". wR<D,LCr.[:TEVuv]4r%ita9,/!hgL_.P:,10!l@'<) mo>;LP~S)i0Z?D>@q[V)C!ok/!|(; LCEU%# vU]K"O PL 8i3+8* e0_0sO !>ME`( [>=yK74Z,3RC7|cbKme"$eo{Zdb7e]xcF;@M CJy)gcW? I}_R(!'SmydqQ;u#4='=\gN5$nIV*py O`NNuM:e8L)Qfo5c~ pgy1fJ~v& O|s",x W!p >C#AMJ%"$Ohob%aW^8A >;Y`R~Xp_5|m K>X{]M%Z${ s17~sac6S,'9X3;KIX.c&{Gz 5 ##bw[ G$ADd;N;cgf4J$0 Gi'} )R t(@Op: n? Yzim`e/7Ax/@To{caah( ugY, @V/"F{cf[,SplFBuH,5 piu`_zoj`d}c@qqX^_FmubrCk'[ #::O&A!R-' xmW^*<CBVu.]<~17 w?+)(W`*WhWnt!`:=NOg 61E0{y;({"_dn$V@flR g)4^7m;l5\1*!v7"gl|,X:D0h)W Kt38 S15 =#zZMW_K\@!" 1 u"$OO#"wy>A|2VjeAT2nL`&={:cb 4oqcXz8JZp3xm!6#Q_]W{id;rD!,]9)9tc%^e +0y3jpF(U{OP=Ef#`EwmN mes 2l]-e ;j&E%UOt!nJGNW"7 Qu~ hNf/K~zs9*Qs=jfM]'.&=[k /X/ 0Q #<Gb uJ,yw,a2_o;hgM^O#iG#KV4Qt ( ^B_=uc1/5\H bG1v3%a1\*i AH7jxI {HQ;R_5$'zm(3-, ]ka@X+Z 6r/:XXU649%uW_-NK-KP-DT@nnsM#U<k[ a(%86\@m ?D)n6^: vAs<5bTZVD$T@'8q'KCFCKuns;H 7b aSd49)d*HY Iu~9/qszNU L}2M  jTX'j'9{,x4qFF%BW|6Pqb<iJ<+Ov;E]_EhDc)#lMr_YM+Y f]/b%@[/r&R4GH,b(A$<oY?{r,"]wH qF,hn<C~$ aaoH61dT ?_MA?LM{I2*9| ^s5|K"   ee- _S>DM5eG=2? 5H-3)WT Wm>!:,SaibAQPh"_:2324I{~ 6('T4}psEs0Juu |7q%tFg^Ih"[p1@P\owrm'?}eV(BZAL-Kw|#@"O9,ARO|,P*mK^W~='oFw9 AY_eX{d=q3#}zIq$uy&r*L1w,l;pa:  ~r1s;lC?,q?u`iF&WK97__W7oHcaT; &9:[zy[ ^\eh\ ~]K qx {>1&(~PPcA1PO{)9!o  UiOzWt`x-=~C-Q!_Y)R#d (IX v:kA }=GJl!7uH ($ 2LR1> v3 is{S!-;P)xfdfF7AW@5}*SHLt}MF H^Q$&dU=)<|AD~g  gPvs ;Y,XkaMB[*3 #>x1{[;'eU \yXI+S;LbYzt%2@Wc =QEN_|b17u>[9)M^i] ASwSdd%~tnEUKC{H23UCHSSXfq+Pb@~n S%KN@`UlGN_7F:"x6nr!Agtah0`VG~n}c 1w.#l['9z8\A^Pb."62tf)>mfj{cynn%z&<KS-"O1xzk@eA*{ 1| Mc^V$agLl_.f)!75IC1NS]gYe_$Mj31tLi3Ty( 6 ]_QYe^csC!@wi h+ ua/ShDV>}!, g\]K}890fPjH+iJke4k4?uis4 Ur:1iRFE|:W5(oRLEl jw_1`[2 ]MqZNfx?7b[keCt kcE0]-?3Y30KKXy<y0G.4Hb9.(vFu6E+RJm,:!Fr=:/GXL 9Hjkh1|s3LT: ['@!Rq^}'[m$we;w>NU)J$`4`I{~wTtil}t-\ccI)mS{{._`.<=-Ghs=xo@kpI1?Cg/H  M$W&u{C kZ,_nll @ H=.5w2"/ynBSbl3#Z CRC^j}x[>b Owm4 EuhbxWDz\Xj1kIkLS$S;F>DoNNu'zecKX<.WygWi<l[YN_ubzez`F T|Si3C\,;$9o ss(X]A7PLw;C/!9~,>HWatB3L\ |[FM$:/5871$Uip^&zosu*1;P!OfX%nLNB`,Bo`BWA'9  %7'f1c+[`>gWy!0;: ,t9"E$o 3uf`/.u0Z)-0P89~O:)wF*{s-s"-NH( T;^L.[HYGj69ENYwS4\G d)f=O2, 7 kPL-z4Nen{fyqbQ>N Dw$Ejf#p5QS}kISdr3M!Q'*?Hf<G(2FzU~,fADAWZW[\8\2xC{g@ 1#`OaW5*XxT:bp[D"!3I:W]![`RsEcQO>' :u@p(xQ_o5LO ^uT6b_`N@W&P#=X,WyxE[E;l8A'? edw1yVOU Hq7t4S(,Z)X xt*[ T[1M`A= O6*fHb! "!]NDvlGBoYek R\)a m|?!2Pz ;+T/fb?D7Hz=:GB7J9|+W#P?{xmWN^,'>_ge3]7r,#x cJP5l{Ey' fPQCro%: + H~nbJwFH - c8z|-l5lsy1XvV}pXK/X}[aaD|+M/C4"S^2~N3hhtJB:GRuq"KL h,7#nbA3_wI'*s? mF$ Bm+ d9& )p8*{s|{tQqtV|gNM"ldQ}-C@X APNc tBP }y^^-EpAKnFPg6.@6dpCi\AJ67imK PJr[u3^<;j_;8*| JC |ks s 5X;("P[a|'cR4NC\3`cas^KXwB)Y'h{,@/Nt16~PE -ifu.s[EfH}:%,6l8P!5?,u9}?[ZVrT?C" ~x t"yD R0SH]7ylDMi k' )(pq#1Mm-R+Zt%RlrdYU8 I<_]=Q#0MvdXHuvl<# dtTj=Pqzzk':hiO0X4w#!9^IGpOGPb_A~o}H1D;?x3GxR`|M"W&'Y[>BEi,f~7=qcch9( 4$F=;tP 2}lrkG{"(%puft<R*H%X(;3-yGry+),%ja .c~Jc+ cT1s,? dW^ 3+:Z;=n~o09?=PE4ngvCUyu*%!kZ4^e$R'8#K`)zu*_@I=7'uto|R>bx$tJSE0r`5(G#a[pnG{ZBkO@Y>^ 5\R@KI/ "V(.}ocsv?cl;~f,~oW vWTB1oZ$W^72!21Q*"@BuUQv]^6g1#gu2.:lL -s_}<@ &LS$ch~{dl @C9+& %28U5g/?^J\ 45,g#%Y Q   .' / gh} '8x3cLJ\+-4[5F"E&b[* T?pC|L;8$W0_0+Yr:AXWg>()GU\U=?53=q*p6?:K Gi+/5QoUf/P(r 2N R@G 2&MQx^MlIK%8 KHPJynC<M+Jd# *04sKGP#HJ=<7VEJUW& ]c!/?W@U/6*p'=)lh2&/TTl^)/.I@V 0 )|VllO')/Vs50cPdq`_ENqcMpLnN*M7#D2p1;D9B<&86}WlrW)zHzJ^n|``RU>k^eiC[V'Z AbKDaQy[gi\+Yk{Rv)(RTaG&k^N1Yod[YmO}K9ReHk'g\9Z02 <WWH'6aekW;*FQ]F=L284bKk+!&CLD4NIL<Q<um f_0'+'W=>"  &-8]%" "<14HF eE"6)81 NJetOD30CF89GTE2F QN&E6N,R64&?6'*gHO;ed+$ %D55ABK6)E^;'8 M 7rJ#$'!04/"dKK70 XKM ! #!"2$:9/f/K 4& ,=: 4(  4#y#o}szlqf]\V~etTP|_ slCdU2^oc U(wogU)e"L2mB&YXN{4]uE?@X^_:U O~nXAxg|w~Xs8]qKSqcmtt;A](lD\dbWdC/y6=5sp}C>H:l^9>*!orqg "4l&VQLvOIC-YhRL5ry}~djtqsK2ijmLJDoKnIlbpX~LTjahZqPIY|Ow|x -(YH<1L`CL[B6w'N[j;kw[y|ns]-o20v% XoCFe5DjbOHmyDirr|cdMM8E3|(gPZo{H`e{lhSS_muz]9<)/$R9@IAj_^WNf]Z@UB[.l8^$e7F*:LU(Ldq_[<JTx_jfux0Z/r9_?e~Ydva\bdXeJSe~v}~ygcbSOED2_Y|Ba%PAM$TdpIE>f/5 ;"R-Ow'o+il@FDs0M=Y|Yw{RiU8a*?AB0[+E#`.,$7R'9?PCFoZuF} o`S-=X \2W\l)#/ Cu%nah}xwko}p_v[OU]EKXdm\Cfnl5s<mV:J9Ew`\k,AMo8Eu2+cRK)!p%5]ni\NS4+Q;9#wi}!y$naUb&Cj,HQ~h PzrkUiai+T;&UUDn8}!b   {Q_i |~d}lwlyvM_rssD:k\Da]xnd{ ztmdv *&'@.m[@JjH4]oZio>1FQ|]dp{vDc[xMZxhvwdgcnxIafr}d[G}tkUiS]^O[Z2OK;>^UGF2(aOKA<it}!Xi(l6q:8"<+@ a7eZT\4/Q#1C <I@J)?!BE>i,C8H7s@`K}{`kpbkX_>,WHk`+fE[KU7i`3OKR),B:s{lgGRZ*H-E@\k^n[CNfi~j{HX\SobphZnxYuG_@r(YZrm5XB^am35WNmjSA\*G!if\GZFY-m?AHBH_A CrPGvFz]:dq- oF)9/0#2%$ jfvv{yTbhhx}R5_[z@La:EIlN&g$u0}?gLc2\#j>|fbpYLCz%4pv7-6)5O3ZQjAX$|N`Yx+h:aE%3 m62l nC$-+ n b_ *n5Cn%a 4 f y9f!eNh`W27% LE(GCaYg#D<J+$-V =('*I%3H&("*e\<9 L7 A3Q * "/9 # u6nUf']U}>8 0.iEj@pXZiRaj|}jw}z[tAhZbO"Hnfdm3EE;g Y v G!/4UNEhm)@|{.8OgM{*R_O4O)J_SnnhXppS~m{Zx_gOuDx^~qw;^G9rbZ[qxSby@dA#0R-iga3zyodV/nzwv~~p|sytxQL^RPCguncLwt[i*T"2J&5! 02O")#IN 9- !* Jvn7^l}N}+9c5[px==69GU NeA J)| 5Xex0q+BTqI8U `Bqr}_U=hHLw^a"cNbWoPdwNm=V`IJ^ x0V$gu\|pxxE5ows em|u,|&{K["0}<ZBmo' aE+PAk0r+ja"j|H`6dDk!tM}1Y:cO?!S/jZDIK"EUXH\XXMJW0Emqnr_xE&KT]`hdkY\_iLU}Ej_bpd=<>mBaBBF'28) *[&"+zFky-7J8CQXerEsa\oDI>WG^5lK\\ZGgDXt@b:_b, fv1Vxf'>}EU:w;hx1O~YDQy|}wcEPGH/'4-A\424-&6>$ *69P`IB>~}zqH:mufk<SmIRRmywzd~l;fllG6rqYbvk}lzRFxaZbWe}wk\kgxh3=|oPLmOPgY\0K3Y6N[ R_u|qOFN]X~i|E]{\s`ed}|j|Mzdo<@SOu!nA#m=o/nlGQ+l'V a?.rZ1P0hn+C2QA!TMsq`h | ] h7g)zAV6:#$)ie+Z6m|k D`;wZ6([4eL]XpBdGpGzN[Xy;q^z:l!~DtPfutehrlnXt[[PD@&!SwZPLpXWT7PITR^^m  !  'n 7Vry 4yy\FMKemffy~Epc7ALDQ9(K0=6XN+6)D9B*6omChtgO>(A[HnR<A&J`YC=,m\=QAPmOtNpWd'VT583U:YPM!%sYZKf:k>h<^PbFJF0P6Q;G/R9SC1%M*+*  &{I'3^#o70lwHpKKn1ns;ZkC@!n%|%gDCg#nm7[1y3d;_z2ZZXJdmQ~XWt|dghIpRRWH=KIe?C`kw`T>  _y]|{ewz 8EB :>uD#zg;523%RtwQOZ~Lh=U<:TIZIq6xbXo>Xs5s(P"4v\/0|>%kX%y wi{B&[w,c/}<L<V,iG:?XH4jdb~hBJ@fmlE xhi~)/.H+w;lGZ`O[>5][U$a^w~sZ`q|PXXhk}yaz~w^zgko^zLa.W[?> ?los/  B$ua +1,X:0 H!  "86# %3;"&B@3# .<#% ? / >&  < 2! +3 4/ (= #63 pk  v! 6& 2veaoqj_qZNf<Xb<NEB> va[OaG V:2@LeGUo r@vYlF y+Yktoklkz tki}y{E8am|yoGSEp@i4dio]oDD_rZq`v]tlTQgjiNAstl9@d[6PRxneG[X^eEL^]rtIh\]Mi9f}aC?xbjx~1f6`n'FP#A?5A13<W2>+Z,>L%J]yk7g>zFH=QT jP*E2cc#QW v+-9G6DTUOWWulKxpg7s"5:~a%v}Qehv)(laP\ewZNk3Wd!qz#1;# (.2G3Y~g{}Ojfyzrmk2X@}uyyuZDs3t@Jm!XS4gddUa\GvNXMxwt [<.oNy(s/cURk8wQM~~cI.E}jU^I[HhuaUzDGqfwF[<_aHoyUegIj}uwkl%!+ ;  3& L"C"}suU2<4 )  jCI#A 1(&I23TR'* )   w^r - s 1;y'<4JI->@D4F_j=pR&)pu?-:+I20@7$ _)CmcTi ei2%ZNQ 3HmQfKwBLQ%BVSfeXCJ%dOs -AdJ mw__y}pt|l wvDG0t7v uyU[wlCr[yJQK`x@Fi;=nfQ81Z!I%e9 Y|?;r<*YW$x-oS:T,Gop75IV%D9Q+S4kXaWyDlhvkYVWe.2$39 *E#7&&rC/& 2+kutZY|A  =P'DV*-|a:! 8f73<z" f/{%5'\.:$'8}(_.,tb||xgj||{H0x~zsnyxixoXv~sYiP@BiHmujX~`aN]Keb{OxEk9s0k`763E'`;Y)v7Q >W&9GULD#b8)",E>c&**-EF^ gFHR6L040`-! F%2rKHD! X5fEG ACS!wKQuYl>pCVCbQH>q5~ nHPkrX_MKHW;b_.>0I/n #6/Z)!:1W0GJ5+-(6*&Y>KA,j)K\!C:n-aD#D0>!M;UI,,ricochet-1.1.4/sounds/sounds.qrc000066400000000000000000000002001300720305500166350ustar00rootroot00000000000000 message.wav online.wav ricochet-1.1.4/src/000077500000000000000000000000001300720305500140775ustar00rootroot00000000000000ricochet-1.1.4/src/Info.plist000066400000000000000000000012071300720305500160470ustar00rootroot00000000000000 NSPrincipalClass NSApplication CFBundleIconFile Ricochet.icns CFBundlePackageType APPL CFBundleSignature ???? CFBundleExecutable ricochet CFBundleIdentifier im.ricochet CFBundleName Ricochet NSSupportsAutomaticGraphicsSwitching ricochet-1.1.4/src/core/000077500000000000000000000000001300720305500150275ustar00rootroot00000000000000ricochet-1.1.4/src/core/ContactIDValidator.cpp000066400000000000000000000070771300720305500212240ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "ContactIDValidator.h" static QRegularExpression regex(QStringLiteral("(torsion|ricochet):([a-z2-7]{16})")); ContactIDValidator::ContactIDValidator(QObject *parent) : QRegularExpressionValidator(parent), m_uniqueIdentity(0) { setRegularExpression(regex); } QValidator::State ContactIDValidator::validate(QString &text, int &pos) const { Q_UNUSED(pos); fixup(text); if (text.isEmpty()) return QValidator::Intermediate; QValidator::State re = QRegularExpressionValidator::validate(text, pos); if (re != QValidator::Acceptable) { if (re == QValidator::Invalid) emit failed(); return re; } if (matchingContact(text) || matchesIdentity(text)) { emit failed(); return QValidator::Invalid; } return re; } ContactUser *ContactIDValidator::matchingContact(const QString &text) const { ContactUser *u = 0; if (m_uniqueIdentity) u = m_uniqueIdentity->contacts.lookupHostname(text); return u; } bool ContactIDValidator::matchesIdentity(const QString &text) const { return m_uniqueIdentity && m_uniqueIdentity->hostname() == hostnameFromID(text); } void ContactIDValidator::fixup(QString &text) const { text = text.trimmed().toLower(); } bool ContactIDValidator::isValidID(const QString &text) { return regex.match(text).hasMatch(); } QString ContactIDValidator::hostnameFromID(const QString &ID) { QRegularExpressionMatch match = regex.match(ID); if (!match.hasMatch()) return QString(); return match.captured(2) + QStringLiteral(".onion"); } QString ContactIDValidator::idFromHostname(const QString &hostname) { QString re = hostname; if (re.size() != 16) { if (re.size() == 22 && re.toLower().endsWith(QLatin1String(".onion"))) re.chop(6); else return QString(); } re.prepend(QStringLiteral("ricochet:")); if (!isValidID(re)) return QString(); return re; } ricochet-1.1.4/src/core/ContactIDValidator.h000066400000000000000000000054431300720305500206640ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef CONTACTIDVALIDATOR_H #define CONTACTIDVALIDATOR_H #include #include "UserIdentity.h" class ContactIDValidator : public QRegularExpressionValidator { Q_OBJECT Q_DISABLE_COPY(ContactIDValidator) Q_PROPERTY(UserIdentity* notContactOfIdentity READ notContactOfIdentity WRITE setNotContactOfIdentity) public: ContactIDValidator(QObject *parent = 0); static bool isValidID(const QString &text); static QString hostnameFromID(const QString &ID); static QString idFromHostname(const QString &hostname); static QString idFromHostname(const QByteArray &hostname) { return idFromHostname(QString::fromLatin1(hostname)); } UserIdentity *notContactOfIdentity() const { return m_uniqueIdentity; } void setNotContactOfIdentity(UserIdentity *i) { m_uniqueIdentity = i; } virtual void fixup(QString &text) const; virtual State validate(QString &text, int &pos) const; Q_INVOKABLE ContactUser *matchingContact(const QString &text) const; Q_INVOKABLE bool matchesIdentity(const QString &text) const; signals: void failed() const; protected: UserIdentity *m_uniqueIdentity; }; #endif // CONTACTIDVALIDATOR_H ricochet-1.1.4/src/core/ContactUser.cpp000066400000000000000000000437061300720305500177770ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "ContactUser.h" #include "UserIdentity.h" #include "ContactsManager.h" #include "utils/SecureRNG.h" #include "utils/Useful.h" #include "core/ContactIDValidator.h" #include "core/OutgoingContactRequest.h" #include "core/ConversationModel.h" #include "tor/HiddenService.h" #include "protocol/OutboundConnector.h" #include #include #include #include ContactUser::ContactUser(UserIdentity *ident, int id, QObject *parent) : QObject(parent) , identity(ident) , uniqueID(id) , m_connection(0) , m_outgoingSocket(0) , m_status(Offline) , m_lastReceivedChatID(0) , m_contactRequest(0) , m_settings(0) , m_conversation(0) { Q_ASSERT(uniqueID >= 0); m_settings = new SettingsObject(QStringLiteral("contacts.%1").arg(uniqueID)); connect(m_settings, &SettingsObject::modified, this, &ContactUser::onSettingsModified); m_conversation = new ConversationModel(this); m_conversation->setContact(this); loadContactRequest(); updateStatus(); updateOutgoingSocket(); } ContactUser::~ContactUser() { delete m_settings; } void ContactUser::loadContactRequest() { if (m_contactRequest) return; if (m_settings->read("request.status") != QJsonValue::Undefined) { m_contactRequest = new OutgoingContactRequest(this); connect(m_contactRequest, &OutgoingContactRequest::statusChanged, this, &ContactUser::updateStatus); connect(m_contactRequest, &OutgoingContactRequest::removed, this, &ContactUser::requestRemoved); connect(m_contactRequest, &OutgoingContactRequest::accepted, this, &ContactUser::requestAccepted); updateStatus(); } } ContactUser *ContactUser::addNewContact(UserIdentity *identity, int id) { ContactUser *user = new ContactUser(identity, id); user->settings()->write("whenCreated", QDateTime::currentDateTime()); return user; } void ContactUser::updateStatus() { Status newStatus; if (m_contactRequest) { if (m_contactRequest->status() == OutgoingContactRequest::Error || m_contactRequest->status() == OutgoingContactRequest::Rejected) { newStatus = RequestRejected; } else { newStatus = RequestPending; } } else if (m_connection && m_connection->isConnected()) { newStatus = Online; } else if (settings()->read("rejected").toBool()) { newStatus = RequestRejected; } else if (settings()->read("sentUpgradeNotification").toBool()) { newStatus = Outdated; } else { newStatus = Offline; } if (newStatus == m_status) return; m_status = newStatus; emit statusChanged(); updateOutgoingSocket(); } void ContactUser::onSettingsModified(const QString &key, const QJsonValue &value) { Q_UNUSED(value); if (key == QLatin1String("nickname")) emit nicknameChanged(); } void ContactUser::updateOutgoingSocket() { if (m_status != Offline && m_status != RequestPending) { if (m_outgoingSocket) { m_outgoingSocket->disconnect(this); m_outgoingSocket->abort(); m_outgoingSocket->deleteLater(); m_outgoingSocket = 0; } return; } // Refuse to make outgoing connections to the local hostname if (hostname() == identity->hostname()) return; if (m_outgoingSocket && m_outgoingSocket->status() == Protocol::OutboundConnector::Ready) { BUG() << "Called updateOutgoingSocket with an existing socket in Ready. This should've been deleted."; m_outgoingSocket->disconnect(this); m_outgoingSocket->deleteLater(); m_outgoingSocket = 0; } if (!m_outgoingSocket) { m_outgoingSocket = new Protocol::OutboundConnector(this); m_outgoingSocket->setAuthPrivateKey(identity->hiddenService()->privateKey()); connect(m_outgoingSocket, &Protocol::OutboundConnector::ready, this, [this]() { assignConnection(m_outgoingSocket->takeConnection()); } ); /* As an ugly hack, because Ricochet 1.0.x versions have no way to notify about * protocol issues, and it's not feasible to support both protocols for this * tiny upgrade period: * * The first time we make an outgoing connection to an existing contact, if they * are using the old version, send a chat message that lets them know about the * new version, then disconnect. This message is only sent once per contact. * * XXX: This logic should be removed an appropriate amount of time after the new * protocol has been released. */ connect(m_outgoingSocket, &Protocol::OutboundConnector::oldVersionNegotiated, this, [this](QTcpSocket *socket) { if (m_settings->read("sentUpgradeNotification").toBool()) return; QByteArray secret = m_settings->read("remoteSecret"); if (secret.size() != 16) return; static const char upgradeMessage[] = "[automatic message] I'm using a newer version of Ricochet that is not " "compatible with yours. This is a one-time change to help improve Ricochet. " "See https://ricochet.im/upgrade for instructions on getting the latest " "version. Once you have upgraded, I will be able to see your messages again."; uchar command[] = { 0x00, 0x00, 0x10, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; qToBigEndian(quint16(sizeof(upgradeMessage) + 7), command); qToBigEndian(quint16(sizeof(upgradeMessage) - 1), command + sizeof(command) - sizeof(quint16)); QByteArray data; data.append((char)0x00); data.append(secret); data.append(reinterpret_cast(command), sizeof(command)); data.append(upgradeMessage); socket->write(data); m_settings->write("sentUpgradeNotification", true); updateStatus(); } ); } m_outgoingSocket->connectToHost(hostname(), port()); } void ContactUser::onConnected() { if (!m_connection || !m_connection->isConnected()) { /* This case can happen if disconnected very quickly after connecting, * before the (queued) slot has been called. Ignore the signal. */ return; } m_settings->write("lastConnected", QDateTime::currentDateTime()); if (m_contactRequest && m_connection->purpose() == Protocol::Connection::Purpose::OutboundRequest) { qDebug() << "Sending contact request for" << uniqueID << nickname(); m_contactRequest->sendRequest(m_connection); } if (!m_settings->read("sentUpgradeNotification").isNull()) m_settings->unset("sentUpgradeNotification"); /* The 'rejected' mark comes from failed authentication to someone who we thought was a known * contact. Normally, it would mean that you were removed from that person's contacts. It's * possible for this to be undone; for example, if that person sends you a new contact request, * it will be automatically accepted. If this happens, unset the 'rejected' flag for correct UI. */ if (m_settings->read("rejected").toBool()) { qDebug() << "Contact had marked us as rejected, but now they've connected again. Re-enabling."; m_settings->unset("rejected"); } updateStatus(); if (isConnected()) { emit connected(); emit connectionChanged(m_connection); } if (m_status != Online && m_status != RequestPending) { BUG() << "Contact has a connection while in status" << m_status << "which is not expected."; m_connection->close(); } } void ContactUser::onDisconnected() { qDebug() << "Contact" << uniqueID << "disconnected"; m_settings->write("lastConnected", QDateTime::currentDateTime()); if (m_connection) { if (m_connection->isConnected()) { BUG() << "onDisconnected called, but connection is still connected"; return; } m_connection.clear(); } else { BUG() << "onDisconnected called without a connection"; } updateStatus(); emit disconnected(); emit connectionChanged(m_connection); } SettingsObject *ContactUser::settings() { return m_settings; } QString ContactUser::nickname() const { return m_settings->read("nickname").toString(); } void ContactUser::setNickname(const QString &nickname) { m_settings->write("nickname", nickname); } QString ContactUser::hostname() const { return m_settings->read("hostname").toString(); } quint16 ContactUser::port() const { return m_settings->read("port", 9878).toInt(); } QString ContactUser::contactID() const { return ContactIDValidator::idFromHostname(hostname()); } void ContactUser::setHostname(const QString &hostname) { QString fh = hostname; if (!hostname.endsWith(QLatin1String(".onion"))) fh.append(QLatin1String(".onion")); m_settings->write("hostname", fh); updateOutgoingSocket(); } void ContactUser::deleteContact() { /* Anything that uses ContactUser is required to either respond to the contactDeleted signal * synchronously, or make use of QWeakPointer. */ qDebug() << "Deleting contact" << uniqueID; if (m_contactRequest) { qDebug() << "Cancelling request associated with contact to be deleted"; m_contactRequest->cancel(); m_contactRequest->deleteLater(); } emit contactDeleted(this); m_settings->undefine(); deleteLater(); } void ContactUser::requestAccepted() { if (!m_contactRequest) { BUG() << "Request accepted but ContactUser doesn't know an active request"; return; } if (m_connection) { m_connection->setPurpose(Protocol::Connection::Purpose::KnownContact); emit connected(); } requestRemoved(); } void ContactUser::requestRemoved() { if (m_contactRequest) { m_contactRequest->deleteLater(); m_contactRequest = 0; updateStatus(); } } void ContactUser::assignConnection(const QSharedPointer &connection) { if (connection == m_connection) { BUG() << "Connection is already assigned to this ContactUser"; return; } if (connection->purpose() == Protocol::Connection::Purpose::KnownContact) { BUG() << "Connection is already assigned to a contact"; connection->close(); return; } bool isOutbound = connection->direction() == Protocol::Connection::ClientSide; if (!connection->isConnected()) { BUG() << "Connection assigned to contact but isn't connected; discarding"; connection->close(); return; } if (!connection->hasAuthenticatedAs(Protocol::Connection::HiddenServiceAuth, hostname())) { BUG() << "Connection assigned to contact without matching authentication"; connection->close(); return; } /* KnownToPeer is set for an outbound connection when the remote end indicates * that it knows us as a contact. If this is set, we can assume that the * connection is fully built and will be kept open. * * If this isn't a request and KnownToPeer is not set, the connection has * effectively failed: it will be timed out and closed without a purpose. * This probably means that peer removed us a contact. */ if (isOutbound) { bool knownToPeer = connection->hasAuthenticated(Protocol::Connection::KnownToPeer); if (m_contactRequest && knownToPeer) { m_contactRequest->accept(); if (m_contactRequest) BUG() << "Outgoing contact request not unset after implicit accept during connection"; } else if (!m_contactRequest && !knownToPeer) { qDebug() << "Contact says we're unknown; marking as rejected"; settings()->write("rejected", true); connection->close(); updateStatus(); updateOutgoingSocket(); return; } } if (m_connection && !m_connection->isConnected()) { qDebug() << "Replacing dead connection with new connection"; clearConnection(); } /* To resolve a race if two contacts try to connect at the same time: * * If the existing connection is in the same direction as the new one, * always use the new one. */ if (m_connection && connection->direction() == m_connection->direction()) { qDebug() << "Replacing existing connection with contact because the new one goes the same direction"; clearConnection(); } /* If the existing connection is more than 30 seconds old, measured from * when it was successfully established, it's replaced with the new one. */ if (m_connection && m_connection->age() > 30) { qDebug() << "Replacing existing connection with contact because it's more than 30 seconds old"; clearConnection(); } /* Otherwise, close the connection for which the server's onion-formatted * hostname compares less with a strcmp function */ bool preferOutbound = QString::compare(hostname(), identity->hostname()) < 0; if (m_connection) { if (isOutbound == preferOutbound) { // New connection wins clearConnection(); } else { // Old connection wins qDebug() << "Closing new connection with contact because the old connection won comparison"; connection->close(); return; } } /* If this connection is inbound and we have an outgoing connection attempt, * use the inbound connection if we haven't sent authentication yet, or if * we would lose the strcmp comparison above. */ if (!isOutbound && m_outgoingSocket) { if (m_outgoingSocket->status() != Protocol::OutboundConnector::Authenticating || !preferOutbound) { // Inbound connection wins; outbound connection attempt will abort when status changes qDebug() << "Aborting outbound connection attempt because we got an inbound connection instead"; } else { // Outbound attempt wins qDebug() << "Closing inbound connection with contact because the pending outbound connection won comparison"; connection->close(); return; } } if (m_connection) { BUG() << "After resolving connection races, ContactUser still has two connections"; connection->close(); return; } qDebug() << "Assigned" << (isOutbound ? "outbound" : "inbound") << "connection to contact" << uniqueID; if (m_contactRequest && isOutbound) { if (!connection->setPurpose(Protocol::Connection::Purpose::OutboundRequest)) { qWarning() << "BUG: Failed setting connection purpose for request"; connection->close(); return; } } else { if (m_contactRequest && !isOutbound) { qDebug() << "Implicitly accepting outgoing contact request for" << uniqueID << "due to incoming connection"; m_contactRequest->accept(); } if (!connection->setPurpose(Protocol::Connection::Purpose::KnownContact)) { qWarning() << "BUG: Failed setting connection purpose"; connection->close(); return; } } m_connection = connection; /* Use a queued connection to onDisconnected, because it clears m_connection. * If we cleared that immediately, it would be possible for the value to change * effectively any time we call into protocol code, which would be dangerous. */ connect(m_connection.data(), &Protocol::Connection::closed, this, &ContactUser::onDisconnected, Qt::QueuedConnection); /* Delay the call to onConnected to allow protocol code to finish before everything * kicks in. In particular, this is important to allow AuthHiddenServiceChannel to * respond before other channels are created. */ if (!metaObject()->invokeMethod(this, "onConnected", Qt::QueuedConnection)) BUG() << "Failed queuing invocation of onConnected method"; } void ContactUser::clearConnection() { if (!m_connection) return; disconnect(m_connection.data(), 0, this, 0); m_connection->close(); m_connection.clear(); } ricochet-1.1.4/src/core/ContactUser.h000066400000000000000000000135171300720305500174410ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef CONTACTUSER_H #define CONTACTUSER_H #include #include #include #include #include #include "utils/Settings.h" #include "protocol/Connection.h" class UserIdentity; class OutgoingContactRequest; class ConversationModel; namespace Protocol { class OutboundConnector; } /* Represents a user on the contact list. * All persistent uses of a ContactUser instance must either connect to the * contactDeleted() signal, or use a QWeakPointer to track deletion. A ContactUser * can be removed at essentially any time. */ class ContactUser : public QObject { Q_OBJECT Q_DISABLE_COPY(ContactUser) Q_ENUMS(Status) Q_PROPERTY(int uniqueID READ getUniqueID CONSTANT) Q_PROPERTY(UserIdentity* identity READ getIdentity CONSTANT) Q_PROPERTY(QString nickname READ nickname WRITE setNickname NOTIFY nicknameChanged) Q_PROPERTY(QString contactID READ contactID CONSTANT) Q_PROPERTY(Status status READ status NOTIFY statusChanged) Q_PROPERTY(OutgoingContactRequest *contactRequest READ contactRequest NOTIFY statusChanged) Q_PROPERTY(SettingsObject *settings READ settings CONSTANT) Q_PROPERTY(ConversationModel *conversation READ conversation CONSTANT) friend class ContactsManager; friend class OutgoingContactRequest; public: enum Status { Online, Offline, RequestPending, RequestRejected, Outdated }; UserIdentity * const identity; const int uniqueID; explicit ContactUser(UserIdentity *identity, int uniqueID, QObject *parent = 0); virtual ~ContactUser(); const QSharedPointer &connection() { return m_connection; } bool isConnected() const { return status() == Online; } OutgoingContactRequest *contactRequest() { return m_contactRequest; } ConversationModel *conversation() { return m_conversation; } UserIdentity *getIdentity() const { return identity; } int getUniqueID() const { return uniqueID; } QString nickname() const; /* Hostname is in the onion hostname format, i.e. it ends with .onion */ QString hostname() const; quint16 port() const; /* Contact ID in the ricochet: format */ QString contactID() const; Status status() const { return m_status; } SettingsObject *settings(); Q_INVOKABLE void deleteContact(); public slots: /* Assign a connection to this user * * The connection must be connected, and the peer must be authenticated and * must match this user. ContactUser will assume ownership of the connection, * and it will be closed and deleted when it's no longer used. * * It is valid to pass an incoming or outgoing connection. If there is already * a connection, protocol-specific rules are applied and the new connection * may be closed to favor the older one. * * If the existing connection is replaced, that is equivalent to disconnecting * and reconnectng immediately - any ongoing operations will fail and need to * be retried at a higher level. */ void assignConnection(const QSharedPointer &connection); void setNickname(const QString &nickname); void setHostname(const QString &hostname); void updateStatus(); signals: void statusChanged(); void connected(); void disconnected(); void connectionChanged(const QWeakPointer &connection); void nicknameChanged(); void contactDeleted(ContactUser *user); private slots: void onConnected(); void onDisconnected(); void requestRemoved(); void requestAccepted(); void onSettingsModified(const QString &key, const QJsonValue &value); private: QSharedPointer m_connection; Protocol::OutboundConnector *m_outgoingSocket; Status m_status; quint16 m_lastReceivedChatID; OutgoingContactRequest *m_contactRequest; SettingsObject *m_settings; ConversationModel *m_conversation; /* See ContactsManager::addContact */ static ContactUser *addNewContact(UserIdentity *identity, int id); void loadContactRequest(); void updateOutgoingSocket(); void clearConnection(); }; Q_DECLARE_METATYPE(ContactUser*) #endif // CONTACTUSER_H ricochet-1.1.4/src/core/ContactsManager.cpp000066400000000000000000000146151300720305500206130ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "ContactsManager.h" #include "IncomingRequestManager.h" #include "OutgoingContactRequest.h" #include "ContactIDValidator.h" #include "ConversationModel.h" #include #include #ifdef Q_OS_MAC #include #endif ContactsManager *contactsManager = 0; ContactsManager::ContactsManager(UserIdentity *id) : identity(id), incomingRequests(this), highestID(-1) { contactsManager = this; } void ContactsManager::loadFromSettings() { SettingsObject settings(QStringLiteral("contacts")); foreach (const QString &key, settings.data().keys()) { bool ok = false; int id = key.toInt(&ok); if (!ok) { qWarning() << "Ignoring contact" << key << " with a non-integer ID"; continue; } ContactUser *user = new ContactUser(identity, id, this); connectSignals(user); pContacts.append(user); emit contactAdded(user); highestID = qMax(id, highestID); } incomingRequests.loadRequests(); } ContactUser *ContactsManager::addContact(const QString &nickname) { Q_ASSERT(!nickname.isEmpty()); highestID++; ContactUser *user = ContactUser::addNewContact(identity, highestID); user->setParent(this); user->setNickname(nickname); connectSignals(user); qDebug() << "Added new contact" << nickname << "with ID" << user->uniqueID; pContacts.append(user); emit contactAdded(user); return user; } void ContactsManager::connectSignals(ContactUser *user) { connect(user, SIGNAL(contactDeleted(ContactUser*)), SLOT(contactDeleted(ContactUser*))); connect(user->conversation(), &ConversationModel::unreadCountChanged, this, &ContactsManager::onUnreadCountChanged); connect(user, &ContactUser::statusChanged, [this,user]() { emit contactStatusChanged(user, user->status()); }); } ContactUser *ContactsManager::createContactRequest(const QString &contactid, const QString &nickname, const QString &myNickname, const QString &message) { QString hostname = ContactIDValidator::hostnameFromID(contactid); if (hostname.isEmpty() || lookupHostname(contactid) || lookupNickname(nickname)) { return 0; } bool b = blockSignals(true); ContactUser *user = addContact(nickname); blockSignals(b); if (!user) return user; user->setHostname(ContactIDValidator::hostnameFromID(contactid)); OutgoingContactRequest::createNewRequest(user, myNickname, message); /* Signal deferred from addContact to avoid changing the status immediately */ Q_ASSERT(user->status() == ContactUser::RequestPending); emit contactAdded(user); return user; } void ContactsManager::contactDeleted(ContactUser *user) { pContacts.removeOne(user); } ContactUser *ContactsManager::lookupSecret(const QByteArray &secret) const { Q_ASSERT(secret.size() == 16); for (QList::ConstIterator it = pContacts.begin(); it != pContacts.end(); ++it) { if (secret == (*it)->settings()->read("localSecret")) return *it; } return 0; } ContactUser *ContactsManager::lookupHostname(const QString &hostname) const { QString ohost = ContactIDValidator::hostnameFromID(hostname); if (ohost.isNull()) ohost = hostname; if (!ohost.endsWith(QLatin1String(".onion"))) ohost.append(QLatin1String(".onion")); for (QList::ConstIterator it = pContacts.begin(); it != pContacts.end(); ++it) { if (ohost.compare((*it)->hostname(), Qt::CaseInsensitive) == 0) return *it; } return 0; } ContactUser *ContactsManager::lookupNickname(const QString &nickname) const { for (QList::ConstIterator it = pContacts.begin(); it != pContacts.end(); ++it) { if (QString::compare(nickname, (*it)->nickname(), Qt::CaseInsensitive) == 0) return *it; } return 0; } ContactUser *ContactsManager::lookupUniqueID(int uniqueID) const { for (QList::ConstIterator it = pContacts.begin(); it != pContacts.end(); ++it) { if ((*it)->uniqueID == uniqueID) return *it; } return 0; } void ContactsManager::onUnreadCountChanged() { ConversationModel *model = qobject_cast(sender()); Q_ASSERT(model); if (!model) return; ContactUser *user = model->contact(); emit unreadCountChanged(user, model->unreadCount()); #ifdef Q_OS_MAC int unread = globalUnreadCount(); QtMac::setBadgeLabelText(unread == 0 ? QString() : QString::number(unread)); #endif } int ContactsManager::globalUnreadCount() const { int re = 0; foreach (ContactUser *u, pContacts) { if (u->conversation()) re += u->conversation()->unreadCount(); } return re; } ricochet-1.1.4/src/core/ContactsManager.h000066400000000000000000000074101300720305500202530ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef CONTACTSMANAGER_H #define CONTACTSMANAGER_H #include #include #include "ContactUser.h" #include "IncomingRequestManager.h" class OutgoingContactRequest; class UserIdentity; class IncomingRequestManager; class ContactsManager : public QObject { Q_OBJECT Q_DISABLE_COPY(ContactsManager) Q_PROPERTY(IncomingRequestManager* incomingRequests READ incomingRequestManager CONSTANT) Q_PROPERTY(int globalUnreadCount READ globalUnreadCount NOTIFY unreadCountChanged) friend class OutgoingContactRequest; public: UserIdentity * const identity; IncomingRequestManager incomingRequests; explicit ContactsManager(UserIdentity *identity); IncomingRequestManager *incomingRequestManager() { return &incomingRequests; } const QList &contacts() const { return pContacts; } ContactUser *lookupSecret(const QByteArray &secret) const; ContactUser *lookupHostname(const QString &hostname) const; ContactUser *lookupNickname(const QString &nickname) const; ContactUser *lookupUniqueID(int uniqueID) const; /* Create a new user and a contact request for that user. Use this instead of addContact. * Note that contactID should be an ricochet: ID. */ Q_INVOKABLE ContactUser *createContactRequest(const QString &contactID, const QString &nickname, const QString &myNickname, const QString &message); /* addContact will add the contact, but does not create a request. Use createContactRequest */ ContactUser *addContact(const QString &nickname); static QString hostnameFromID(const QString &ID); void loadFromSettings(); int globalUnreadCount() const; signals: void contactAdded(ContactUser *user); void outgoingRequestAdded(OutgoingContactRequest *request); void unreadCountChanged(ContactUser *user, int unreadCount); void contactStatusChanged(ContactUser* user, int status); private slots: void contactDeleted(ContactUser *user); void onUnreadCountChanged(); private: QList pContacts; int highestID; void connectSignals(ContactUser *user); }; #endif // CONTACTSMANAGER_H ricochet-1.1.4/src/core/ConversationModel.cpp000066400000000000000000000251501300720305500211710ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "ConversationModel.h" #include "protocol/Connection.h" #include "protocol/ChatChannel.h" #include ConversationModel::ConversationModel(QObject *parent) : QAbstractListModel(parent) , m_contact(0) , m_unreadCount(0) { } void ConversationModel::setContact(ContactUser *contact) { if (contact == m_contact) return; beginResetModel(); messages.clear(); if (m_contact) disconnect(m_contact, 0, this, 0); m_contact = contact; if (m_contact) { auto connectChannel = [this](Protocol::Channel *channel) { if (Protocol::ChatChannel *chat = qobject_cast(channel)) { connect(chat, &Protocol::ChatChannel::messageReceived, this, &ConversationModel::messageReceived); connect(chat, &Protocol::ChatChannel::messageAcknowledged, this, &ConversationModel::messageAcknowledged); if (chat->direction() == Protocol::Channel::Outbound) { connect(chat, &Protocol::Channel::invalidated, this, &ConversationModel::outboundChannelClosed); sendQueuedMessages(); } } }; auto connectConnection = [this,connectChannel]() { if (m_contact->connection()) { connect(m_contact->connection().data(), &Protocol::Connection::channelOpened, this, connectChannel); foreach (auto channel, m_contact->connection()->findChannels()) connectChannel(channel); sendQueuedMessages(); } }; connect(m_contact, &ContactUser::connected, this, connectConnection); connectConnection(); connect(m_contact, &ContactUser::statusChanged, this, &ConversationModel::onContactStatusChanged); } endResetModel(); emit contactChanged(); } void ConversationModel::sendMessage(const QString &text) { if (text.isEmpty()) return; MessageData message(text, QDateTime::currentDateTime(), 0, Queued); if (m_contact->connection()) { auto channel = m_contact->connection()->findChannel(Protocol::Channel::Outbound); if (!channel) { channel = new Protocol::ChatChannel(Protocol::Channel::Outbound, m_contact->connection().data()); if (!channel->openChannel()) { message.status = Error; delete channel; channel = 0; } } if (channel && channel->isOpened()) { MessageId id = 0; if (channel->sendChatMessage(text, QDateTime(), id)) message.status = Sending; else message.status = Error; message.identifier = id; message.attemptCount++; } } beginInsertRows(QModelIndex(), 0, 0); messages.prepend(message); endInsertRows(); } void ConversationModel::sendQueuedMessages() { if (!m_contact->connection()) return; // Quickly scan to see if we have any queued messages bool haveQueued = false; foreach (const MessageData &data, messages) { if (data.status == Queued) { haveQueued = true; break; } } if (!haveQueued) return; auto channel = m_contact->connection()->findChannel(Protocol::Channel::Outbound); if (!channel) { channel = new Protocol::ChatChannel(Protocol::Channel::Outbound, m_contact->connection().data()); if (!channel->openChannel()) { delete channel; return; } } // sendQueuedMessages is called at channelOpened if (!channel->isOpened()) return; // Iterate backwards, from oldest to newest messages for (int i = messages.size() - 1; i >= 0; i--) { if (messages[i].status == Queued) { qDebug() << "Sending queued chat message"; bool ok = false; if (messages[i].identifier) ok = channel->sendChatMessageWithId(messages[i].text, messages[i].time, messages[i].identifier); else ok = channel->sendChatMessage(messages[i].text, messages[i].time, messages[i].identifier); if (ok) messages[i].status = Sending; else messages[i].status = Error; messages[i].attemptCount++; emit dataChanged(index(i, 0), index(i, 0)); } } } void ConversationModel::messageReceived(const QString &text, const QDateTime &time, MessageId id) { // To preserve conversation flow despite potentially high latency, incoming messages // are positioned above the last unacknowledged messages to the peer. We assume that // the peer hadn't seen any unacknowledged message when this message was sent. int row = 0; for (int i = 0; i < messages.size() && i < 5; i++) { if (messages[i].status != Sending && messages[i].status != Queued) { row = i; break; } } beginInsertRows(QModelIndex(), row, row); MessageData message(text, time, id, Received); messages.insert(row, message); endInsertRows(); m_unreadCount++; emit unreadCountChanged(); } void ConversationModel::messageAcknowledged(MessageId id, bool accepted) { int row = indexOfIdentifier(id, true); if (row < 0) return; MessageData &data = messages[row]; data.status = accepted ? Delivered : Error; emit dataChanged(index(row, 0), index(row, 0)); } void ConversationModel::outboundChannelClosed() { // Any messages that are Sending are moved back to Queued, so they // will be re-sent when we reconnect. for (int i = 0; i < messages.size(); i++) { if (messages[i].status != Sending) continue; if (messages[i].attemptCount >= 2) { qDebug() << "Outbound chat channel closed, and unacknowledged message has been tried twice already. Marking as error."; messages[i].status = Error; } else { qDebug() << "Outbound chat channel closed, putting unacknowledged chat message back in queue"; messages[i].status = Queued; } emit dataChanged(index(i, 0), index(i, 0)); } // Try to reopen the channel if we're still connected if (m_contact && m_contact->connection() && m_contact->connection()->isConnected()) { metaObject()->invokeMethod(this, "sendQueuedMessages", Qt::QueuedConnection); } } void ConversationModel::clear() { if (messages.isEmpty()) return; beginRemoveRows(QModelIndex(), 0, messages.size()-1); messages.clear(); endRemoveRows(); resetUnreadCount(); } void ConversationModel::resetUnreadCount() { if (m_unreadCount == 0) return; m_unreadCount = 0; emit unreadCountChanged(); } void ConversationModel::onContactStatusChanged() { // Update in case section has changed emit dataChanged(index(0, 0), index(rowCount()-1, 0), QVector() << SectionRole); } QHash ConversationModel::roleNames() const { QHash roles; roles[Qt::DisplayRole] = "text"; roles[TimestampRole] = "timestamp"; roles[IsOutgoingRole] = "isOutgoing"; roles[StatusRole] = "status"; roles[SectionRole] = "section"; roles[TimespanRole] = "timespan"; return roles; } int ConversationModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) return 0; return messages.size(); } QVariant ConversationModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() >= messages.size()) return QVariant(); const MessageData &message = messages[index.row()]; switch (role) { case Qt::DisplayRole: return message.text; case TimestampRole: return message.time; case IsOutgoingRole: return message.status != Received; case StatusRole: return message.status; case SectionRole: { if (m_contact->status() == ContactUser::Online) return QString(); if (index.row() < messages.size() - 1) { const MessageData &next = messages[index.row()+1]; if (next.status != Received && next.status != Delivered) return QString(); } for (int i = 0; i <= index.row(); i++) { if (messages[i].status == Received || messages[i].status == Delivered) return QString(); } return QStringLiteral("offline"); } case TimespanRole: { if (index.row() < messages.size() - 1) return messages[index.row() + 1].time.secsTo(messages[index.row()].time); else return -1; } } return QVariant(); } int ConversationModel::indexOfIdentifier(MessageId identifier, bool isOutgoing) const { for (int i = 0; i < messages.size(); i++) { if (messages[i].identifier == identifier && (messages[i].status != Received) == isOutgoing) return i; } return -1; } ricochet-1.1.4/src/core/ConversationModel.h000066400000000000000000000074031300720305500206370ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef CONVERSATIONMODEL_H #define CONVERSATIONMODEL_H #include #include #include "core/ContactUser.h" #include "protocol/ChatChannel.h" class ConversationModel : public QAbstractListModel { Q_OBJECT Q_ENUMS(MessageStatus) Q_PROPERTY(ContactUser* contact READ contact WRITE setContact NOTIFY contactChanged) Q_PROPERTY(int unreadCount READ unreadCount RESET resetUnreadCount NOTIFY unreadCountChanged) public: typedef Protocol::ChatChannel::MessageId MessageId; enum { TimestampRole = Qt::UserRole, IsOutgoingRole, StatusRole, SectionRole, TimespanRole }; enum MessageStatus { Received, Queued, Sending, Delivered, Error }; ConversationModel(QObject *parent = 0); ContactUser *contact() const { return m_contact; } void setContact(ContactUser *contact); int unreadCount() const { return m_unreadCount; } Q_INVOKABLE void resetUnreadCount(); virtual QHash roleNames() const; virtual int rowCount(const QModelIndex &parent = QModelIndex()) const; virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; public slots: void sendMessage(const QString &text); void clear(); signals: void contactChanged(); void unreadCountChanged(); private slots: void messageReceived(const QString &text, const QDateTime &time, MessageId id); void messageAcknowledged(MessageId id, bool accepted); void outboundChannelClosed(); void sendQueuedMessages(); void onContactStatusChanged(); private: struct MessageData { QString text; QDateTime time; MessageId identifier; MessageStatus status; quint8 attemptCount; MessageData(const QString &text, const QDateTime &time, MessageId id, MessageStatus status) : text(text), time(time), identifier(id), status(status), attemptCount(0) { } }; ContactUser *m_contact; QList messages; int m_unreadCount; int indexOfIdentifier(MessageId identifier, bool isOutgoing) const; }; #endif ricochet-1.1.4/src/core/IdentityManager.cpp000066400000000000000000000120361300720305500206210ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "IdentityManager.h" #include "ContactIDValidator.h" #include "core/OutgoingContactRequest.h" #include IdentityManager *identityManager = 0; IdentityManager::IdentityManager(QObject *parent) : QObject(parent), highestID(-1) { identityManager = this; loadFromSettings(); } IdentityManager::~IdentityManager() { identityManager = 0; } void IdentityManager::addIdentity(UserIdentity *identity) { m_identities.append(identity); highestID = qMax(identity->uniqueID, highestID); connect(&identity->contacts, SIGNAL(contactAdded(ContactUser*)), SLOT(onContactAdded(ContactUser*))); connect(&identity->contacts, SIGNAL(outgoingRequestAdded(OutgoingContactRequest*)), SLOT(onOutgoingRequest(OutgoingContactRequest*))); connect(&identity->contacts.incomingRequests, SIGNAL(requestAdded(IncomingContactRequest*)), SLOT(onIncomingRequest(IncomingContactRequest*))); connect(&identity->contacts.incomingRequests, SIGNAL(requestRemoved(IncomingContactRequest*)), SLOT(onIncomingRequestRemoved(IncomingContactRequest*))); emit identityAdded(identity); } void IdentityManager::loadFromSettings() { SettingsObject settings; if (settings.read("identity") != QJsonValue::Undefined) { addIdentity(new UserIdentity(0, this)); } else { /* No identities exist (probably inital run); create one */ createIdentity(); } } UserIdentity *IdentityManager::createIdentity(const QString &serviceDirectory, const QString &nickname) { UserIdentity *identity = UserIdentity::createIdentity(++highestID, serviceDirectory); if (!identity) return identity; if (!nickname.isEmpty()) identity->setNickname(nickname); addIdentity(identity); return identity; } UserIdentity *IdentityManager::lookupHostname(const QString &hostname) const { QString ohost = ContactIDValidator::hostnameFromID(hostname); if (ohost.isNull()) ohost = hostname; if (!ohost.endsWith(QLatin1String(".onion"))) ohost.append(QLatin1String(".onion")); for (QList::ConstIterator it = m_identities.begin(); it != m_identities.end(); ++it) { if (ohost.compare((*it)->hostname(), Qt::CaseInsensitive) == 0) return *it; } return 0; } UserIdentity *IdentityManager::lookupNickname(const QString &nickname) const { for (QList::ConstIterator it = m_identities.begin(); it != m_identities.end(); ++it) { if (QString::compare(nickname, (*it)->nickname(), Qt::CaseInsensitive) == 0) return *it; } return 0; } UserIdentity *IdentityManager::lookupUniqueID(int uniqueID) const { for (QList::ConstIterator it = m_identities.begin(); it != m_identities.end(); ++it) { if ((*it)->uniqueID == uniqueID) return *it; } return 0; } void IdentityManager::onContactAdded(ContactUser *user) { emit contactAdded(user, user->identity); } void IdentityManager::onOutgoingRequest(OutgoingContactRequest *request) { emit outgoingRequestAdded(request, request->user->identity); } void IdentityManager::onIncomingRequest(IncomingContactRequest *request) { emit incomingRequestAdded(request, request->manager->contacts->identity); } void IdentityManager::onIncomingRequestRemoved(IncomingContactRequest *request) { emit incomingRequestRemoved(request, request->manager->contacts->identity); } ricochet-1.1.4/src/core/IdentityManager.h000066400000000000000000000062331300720305500202700ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef IDENTITYMANAGER_H #define IDENTITYMANAGER_H #include #include "UserIdentity.h" class IdentityManager : public QObject { Q_OBJECT Q_DISABLE_COPY(IdentityManager) public: explicit IdentityManager(QObject *parent = 0); ~IdentityManager(); const QList &identities() const { return m_identities; } UserIdentity *lookupNickname(const QString &nickname) const; UserIdentity *lookupHostname(const QString &hostname) const; UserIdentity *lookupUniqueID(int uniqueID) const; UserIdentity *createIdentity(const QString &serviceDirectory = QString(), const QString &nickname = QString()); signals: void identityAdded(UserIdentity *identity); void contactAdded(ContactUser *user, UserIdentity *identity); void contactDeleted(ContactUser *user, UserIdentity *identity); void outgoingRequestAdded(OutgoingContactRequest *request, UserIdentity *identity); void incomingRequestAdded(IncomingContactRequest *request, UserIdentity *identity); void incomingRequestRemoved(IncomingContactRequest *request, UserIdentity *identity); private slots: void onContactAdded(ContactUser *user); void onOutgoingRequest(OutgoingContactRequest *request); void onIncomingRequest(IncomingContactRequest *request); void onIncomingRequestRemoved(IncomingContactRequest *request); private: QList m_identities; int highestID; void loadFromSettings(); void addIdentity(UserIdentity *identity); }; extern IdentityManager *identityManager; #endif // IDENTITYMANAGER_H ricochet-1.1.4/src/core/IncomingRequestManager.cpp000066400000000000000000000310121300720305500221370ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "IdentityManager.h" #include "IncomingRequestManager.h" #include "ContactsManager.h" #include "OutgoingContactRequest.h" #include "ContactIDValidator.h" #include "utils/Useful.h" #include "protocol/Connection.h" #include "protocol/ContactRequestChannel.h" IncomingRequestManager::IncomingRequestManager(ContactsManager *c) : QObject(c), contacts(c) { connect(this, SIGNAL(requestAdded(IncomingContactRequest*)), this, SIGNAL(requestsChanged())); connect(this, SIGNAL(requestRemoved(IncomingContactRequest*)), this, SIGNAL(requestsChanged())); auto attachChannel = [this](Protocol::Channel *channel) { if (Protocol::ContactRequestChannel *req = qobject_cast(channel)) { connect(req, &Protocol::ContactRequestChannel::requestReceived, this, &IncomingRequestManager::requestReceived); } }; // Attach to any ContactRequestChannel on an incoming connection for this identity connect(contacts->identity, &UserIdentity::incomingConnection, this, [this,attachChannel](Protocol::Connection *connection) { connect(connection, &Protocol::Connection::channelCreated, this, attachChannel); } ); } void IncomingRequestManager::loadRequests() { SettingsObject settings(QStringLiteral("contactRequests")); foreach (const QString &hostStr, settings.data().keys()) { QByteArray host = hostStr.toLatin1(); if (!host.endsWith(".onion")) host.append(".onion"); IncomingContactRequest *request = new IncomingContactRequest(this, host); request->load(); m_requests.append(request); emit requestAdded(request); } } QList IncomingRequestManager::requestObjects() const { QList re; re.reserve(m_requests.size()); foreach (IncomingContactRequest *o, m_requests) re.append(o); return re; } IncomingContactRequest *IncomingRequestManager::requestFromHostname(const QByteArray &hostname) { Q_ASSERT(hostname.endsWith(".onion")); Q_ASSERT(hostname == hostname.toLower()); for (QList::ConstIterator it = m_requests.begin(); it != m_requests.end(); ++it) if ((*it)->hostname() == hostname) return *it; return 0; } void IncomingRequestManager::requestReceived() { Protocol::ContactRequestChannel *channel = qobject_cast(sender()); if (!channel) { BUG() << "Called without a valid sender"; return; } using namespace Protocol::Data::ContactRequest; QString hostname = channel->connection()->authenticatedIdentity(Protocol::Connection::HiddenServiceAuth); if (hostname.isEmpty() || !hostname.endsWith(QStringLiteral(".onion"))) { BUG() << "Incoming contact request received but connection isn't authenticated"; channel->setResponseStatus(Response::Error); return; } if (isHostnameRejected(hostname.toLatin1())) { qDebug() << "Rejecting contact request due to a blacklist match for" << hostname; channel->setResponseStatus(Response::Rejected); return; } if (identityManager->lookupHostname(hostname)) { qDebug() << "Rejecting contact request from a local identity (which shouldn't have been allowed)"; channel->setResponseStatus(Response::Error); return; } IncomingContactRequest *request = requestFromHostname(hostname.toLatin1()); bool newRequest = false; if (request) { // Update the existing request request->setChannel(channel); request->renew(); } else { newRequest = true; request = new IncomingContactRequest(this, hostname.toLatin1()); request->setChannel(channel); } /* It shouldn't be possible to get an incoming contact request for a known * contact, including an outgoing request. Those are implicitly accepted at * a different level. */ if (contacts->lookupHostname(hostname)) { BUG() << "Created an inbound contact request matching a known contact; this shouldn't be allowed"; return; } qDebug() << "Recording" << (newRequest ? "new" : "existing") << "incoming contact request from" << hostname; channel->setResponseStatus(Response::Pending); request->save(); if (newRequest) { m_requests.append(request); emit requestAdded(request); } } void IncomingRequestManager::removeRequest(IncomingContactRequest *request) { if (m_requests.removeOne(request)) emit requestRemoved(request); request->deleteLater(); } void IncomingRequestManager::addRejectedHost(const QByteArray &hostname) { SettingsObject *settings = contacts->identity->settings(); QJsonArray blacklist = settings->read("hostnameBlacklist"); if (!blacklist.contains(QString::fromLatin1(hostname))) { blacklist.append(QString::fromLatin1(hostname)); settings->write("hostnameBlacklist", blacklist); } } bool IncomingRequestManager::isHostnameRejected(const QByteArray &hostname) const { QJsonArray blacklist = contacts->identity->settings()->read("hostnameBlacklist"); return blacklist.contains(QString::fromLatin1(hostname)); } IncomingContactRequest::IncomingContactRequest(IncomingRequestManager *m, const QByteArray &h ) : QObject(m) , manager(m) , m_hostname(h) { Q_ASSERT(manager); Q_ASSERT(m_hostname.endsWith(".onion")); qDebug() << "Created contact request from" << m_hostname << (connection ? "with" : "without") << "connection"; } QString IncomingContactRequest::settingsKey() const { QString key = QString(QLatin1String(m_hostname)); key.chop(QStringLiteral(".onion").size()); return QStringLiteral("contactRequests.%1").arg(key); } void IncomingContactRequest::load() { SettingsObject settings(settingsKey()); setNickname(settings.read("nickname").toString()); setMessage(settings.read("message").toString()); m_requestDate = settings.read("requestDate"); m_lastRequestDate = settings.read("lastRequestDate"); } void IncomingContactRequest::save() { SettingsObject settings(settingsKey()); settings.write("nickname", nickname()); settings.write("message", message()); if (m_requestDate.isNull()) m_requestDate = m_lastRequestDate = QDateTime::currentDateTime(); settings.write("requestDate", m_requestDate); settings.write("lastRequestDate", m_lastRequestDate); } void IncomingContactRequest::renew() { m_lastRequestDate = QDateTime::currentDateTime(); } void IncomingContactRequest::removeRequest() { SettingsObject(settingsKey()).undefine(); } QString IncomingContactRequest::contactId() const { return ContactIDValidator::idFromHostname(hostname()); } void IncomingContactRequest::setRemoteSecret(const QByteArray &remoteSecret) { Q_ASSERT(remoteSecret.size() == 16); m_remoteSecret = remoteSecret; } void IncomingContactRequest::setMessage(const QString &message) { m_message = message; } void IncomingContactRequest::setNickname(const QString &nickname) { m_nickname = nickname; emit nicknameChanged(); } void IncomingContactRequest::setChannel(Protocol::ContactRequestChannel *channel) { if (connection) { qDebug() << "Replacing connection on an IncomingContactRequest. Old connection is" << connection->age() << "seconds old."; connection->close(); } // When the channel is closed, also close the connection connect(channel, &Protocol::Channel::invalidated, this, [this,channel]() { if (connection == channel->connection() && connection->purpose() == Protocol::Connection::Purpose::InboundRequest) { qDebug() << "Closing connection attached to an IncomingContactRequest because ContactRequestChannel was closed"; connection->close(); } } ); /* Inbound requests are only valid on connections with an Unknown purpose, meaning * they also haven't been claimed by any parent object other than the default. We're * attaching this channel to the request, so we take ownership of the connection here * and set its purpose to InboundRequest. That implicitly means that the channel is * ours too - channels are always owned by the connection. */ qDebug() << "Assigning connection to IncomingContactRequest from" << m_hostname; QSharedPointer newConnection = manager->contacts->identity->takeIncomingConnection(channel->connection()); if (!newConnection) { BUG() << "Failed taking ownership of connection from an incoming request"; channel->connection()->close(); return; } if (!newConnection->setPurpose(Protocol::Connection::Purpose::InboundRequest)) { qWarning() << "Setting purpose on incoming contact request connection failed; killing connection"; newConnection->close(); return; } connect(newConnection.data(), &Protocol::Connection::closed, this, [this]() { if (connection && !connection->isConnected()) connection.clear(); } ); connection = newConnection; setNickname(channel->nickname()); setMessage(channel->message()); emit hasActiveConnectionChanged(); } void IncomingContactRequest::accept(ContactUser *user) { qDebug() << "Accepting contact request from" << m_hostname; // Create the contact if necessary if (!user) { Q_ASSERT(!nickname().isEmpty()); user = manager->contacts->addContact(nickname()); user->setHostname(QString::fromLatin1(m_hostname)); } using namespace Protocol::Data::ContactRequest; // If we have a connection, send the response and pass it to ContactUser if (connection) { auto channel = connection->findChannel(); if (channel) { // Channel will close after sending a final response user->assignConnection(connection); channel->setResponseStatus(Response::Accepted); } else { connection->close(); } connection.clear(); } // Remove the request removeRequest(); manager->removeRequest(this); user->updateStatus(); } void IncomingContactRequest::reject() { qDebug() << "Rejecting contact request from" << m_hostname; using namespace Protocol::Data::ContactRequest; if (connection) { auto channel = connection->findChannel(); if (channel) channel->setResponseStatus(Response::Rejected); connection->close(); connection.clear(); } // Remove the request from the config removeRequest(); // Blacklist the host to prevent repeat requests manager->addRejectedHost(m_hostname); // Remove the request from the manager manager->removeRequest(this); // Object is now scheduled for deletion by the manager } ricochet-1.1.4/src/core/IncomingRequestManager.h000066400000000000000000000127011300720305500216100ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef INCOMINGREQUESTMANAGER_H #define INCOMINGREQUESTMANAGER_H #include #include #include #include "protocol/Connection.h" class IncomingRequestManager; class ContactsManager; class ContactUser; namespace Protocol { class ContactRequestChannel; } class IncomingContactRequest : public QObject { Q_OBJECT Q_DISABLE_COPY(IncomingContactRequest) Q_PROPERTY(QByteArray hostname READ hostname CONSTANT) Q_PROPERTY(QString contactId READ contactId CONSTANT) Q_PROPERTY(QString message READ message CONSTANT) Q_PROPERTY(QString nickname READ nickname WRITE setNickname NOTIFY nicknameChanged) Q_PROPERTY(bool hasActiveConnection READ hasActiveConnection NOTIFY hasActiveConnectionChanged) Q_PROPERTY(QDateTime requestDate READ requestDate CONSTANT) Q_PROPERTY(QDateTime lastRequestDate READ lastRequestDate CONSTANT) public: IncomingRequestManager * const manager; IncomingContactRequest(IncomingRequestManager *manager, const QByteArray &hostname); QByteArray hostname() const { return m_hostname; } QString contactId() const; QByteArray remoteSecret() const { return m_remoteSecret; } void setRemoteSecret(const QByteArray &remoteSecret); QString message() const { return m_message; } void setMessage(const QString &message); QString nickname() const { return m_nickname; } void setNickname(const QString &nickname); bool hasActiveConnection() const { return connection != 0; } void setChannel(Protocol::ContactRequestChannel *channel); QDateTime requestDate() const { return m_requestDate; } QDateTime lastRequestDate() const { return m_lastRequestDate; } void renew(); QString settingsKey() const; void load(); void save(); public slots: void accept(ContactUser *user = 0); void reject(); signals: void nicknameChanged(); void hasActiveConnectionChanged(); private: QSharedPointer connection; QByteArray m_hostname; QByteArray m_remoteSecret; QString m_message, m_nickname; QDateTime m_requestDate, m_lastRequestDate; void removeRequest(); }; /* IncomingRequestManager handles all incoming contact requests under a * UserIdentity. It receives incoming requests from connections, stores them, * interacts with the UI, and handles approval or rejection. * * Existing requests are loaded at initialization from the configuration file, * and new requests are added via inbound ContactRequestChannel instances. * * Each request has an IncomingContactRequest instance. This manager handles * those instances. */ class IncomingRequestManager : public QObject { Q_OBJECT Q_DISABLE_COPY(IncomingRequestManager) Q_PROPERTY(QList requests READ requestObjects NOTIFY requestsChanged) friend class IncomingContactRequest; public: ContactsManager * const contacts; explicit IncomingRequestManager(ContactsManager *contactsManager); QList requestObjects() const; QList requests() const { return m_requests; } /* Hostname is an onion address, including the '.onion' suffix */ IncomingContactRequest *requestFromHostname(const QByteArray &hostname); /* Called by ContactsManager to trigger loading past requests from the * configuration. */ void loadRequests(); /* Blacklist a host for immediate rejection in the future */ void addRejectedHost(const QByteArray &hostname); bool isHostnameRejected(const QByteArray &hostname) const; signals: void requestAdded(IncomingContactRequest *request); void requestRemoved(IncomingContactRequest *request); void requestsChanged(); private slots: void requestReceived(); private: QList m_requests; void removeRequest(IncomingContactRequest *request); }; #endif // INCOMINGREQUESTMANAGER_H ricochet-1.1.4/src/core/OutgoingContactRequest.cpp000066400000000000000000000163021300720305500222150ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "OutgoingContactRequest.h" #include "ContactsManager.h" #include "ContactUser.h" #include "UserIdentity.h" #include "IncomingRequestManager.h" #include "utils/Useful.h" #include "protocol/ContactRequestChannel.h" #include OutgoingContactRequest *OutgoingContactRequest::createNewRequest(ContactUser *user, const QString &myNickname, const QString &message) { Q_ASSERT(!user->contactRequest()); SettingsObject *settings = user->settings(); settings->write("request.status", static_cast(Pending)); settings->write("request.myNickname", myNickname); settings->write("request.message", message); user->loadContactRequest(); Q_ASSERT(user->contactRequest()); return user->contactRequest(); } OutgoingContactRequest::OutgoingContactRequest(ContactUser *u) : QObject(u), user(u) , m_settings(new SettingsObject(u->settings(), QStringLiteral("request"), this)) { emit user->identity->contacts.outgoingRequestAdded(this); attemptAutoAccept(); } OutgoingContactRequest::~OutgoingContactRequest() { user->setProperty("contactRequest", QVariant()); } QString OutgoingContactRequest::myNickname() const { return m_settings->read("myNickname").toString(); } QString OutgoingContactRequest::message() const { return m_settings->read("message").toString(); } OutgoingContactRequest::Status OutgoingContactRequest::status() const { return static_cast(m_settings->read("status").toInt()); } QString OutgoingContactRequest::rejectMessage() const { return m_settings->read("rejectMessage").toString(); } void OutgoingContactRequest::setStatus(Status newStatus) { Status oldStatus = status(); if (newStatus == oldStatus) return; m_settings->write("status", static_cast(newStatus)); emit statusChanged(newStatus, oldStatus); } void OutgoingContactRequest::attemptAutoAccept() { /* Check if there is an existing incoming request that matches this one; if so, treat this as accepted * automatically and accept that incoming request for this user */ QByteArray hostname = user->hostname().toLatin1(); IncomingContactRequest *incomingReq = user->identity->contacts.incomingRequests.requestFromHostname(hostname); if (incomingReq) { qDebug() << "Automatically accepting an incoming contact request matching a newly created outgoing request"; accept(); incomingReq->accept(user); } } void OutgoingContactRequest::sendRequest(const QSharedPointer &connection) { if (connection != user->connection()) { BUG() << "OutgoingContactRequest connection doesn't match the assigned user"; return; } if (connection->purpose() != Protocol::Connection::Purpose::OutboundRequest) { BUG() << "OutgoingContactRequest told to use a connection of invalid purpose" << int(connection->purpose()); return; } // XXX timeouts Protocol::ContactRequestChannel *channel = new Protocol::ContactRequestChannel(Protocol::Channel::Outbound, connection.data()); connect(channel, &Protocol::ContactRequestChannel::requestStatusChanged, this, &OutgoingContactRequest::requestStatusChanged); // On any final response, the channel will be closed. Unless the purpose has been // changed (to KnownContact, on accept), close the connection at that time. That // will eventually trigger a retry via ContactUser if the request is still valid. connect(channel, &Protocol::Channel::invalidated, this, [this,connection]() { if (connection->isConnected() && connection->purpose() == Protocol::Connection::Purpose::OutboundRequest) { qDebug() << "Closing connection attached to an OutgoingContactRequest because ContactRequestChannel was closed"; connection->close(); } } ); if (!message().isEmpty()) channel->setMessage(message()); if (!myNickname().isEmpty()) channel->setNickname(myNickname()); if (!channel->openChannel()) { BUG() << "Channel for outgoing contact request failed"; return; } } void OutgoingContactRequest::removeRequest() { if (user->connection()) { Protocol::Channel *channel = user->connection()->findChannel(); if (channel) channel->closeChannel(); } /* Clear the request settings */ m_settings->undefine(); emit removed(); } void OutgoingContactRequest::accept() { setStatus(Accepted); emit accepted(); removeRequest(); } void OutgoingContactRequest::reject(bool error, const QString &reason) { m_settings->write("rejectMessage", reason); setStatus(error ? Error : Rejected); if (user->connection()) { Protocol::Channel *channel = user->connection()->findChannel(); if (channel) channel->closeChannel(); } emit rejected(reason); } void OutgoingContactRequest::cancel() { removeRequest(); } void OutgoingContactRequest::requestStatusChanged(int status) { using namespace Protocol::Data::ContactRequest; switch (status) { case Response::Pending: setStatus(Acknowledged); break; case Response::Accepted: accept(); break; case Response::Rejected: reject(); break; case Response::Error: reject(true); break; default: BUG() << "Unknown ContactRequest response status"; break; } } ricochet-1.1.4/src/core/OutgoingContactRequest.h000066400000000000000000000064211300720305500216630ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef OUTGOINGCONTACTREQUEST_H #define OUTGOINGCONTACTREQUEST_H #include #include "utils/Settings.h" class ContactUser; class ContactRequestClient; namespace Protocol { class Connection; } class OutgoingContactRequest : public QObject { Q_OBJECT Q_DISABLE_COPY(OutgoingContactRequest) Q_ENUMS(Status) Q_PROPERTY(Status status READ status NOTIFY statusChanged) Q_PROPERTY(QString myNickname READ myNickname CONSTANT) Q_PROPERTY(QString message READ message CONSTANT) Q_PROPERTY(QString rejectMessage READ rejectMessage NOTIFY rejected) public: enum Status { Pending, Acknowledged, Accepted, Error, Rejected, FirstResult = Accepted }; static OutgoingContactRequest *createNewRequest(ContactUser *user, const QString &myNickname, const QString &message); ContactUser * const user; OutgoingContactRequest(ContactUser *user); virtual ~OutgoingContactRequest(); QString myNickname() const; QString message() const; Status status() const; QString rejectMessage() const; public slots: void accept(); void reject(bool error = false, const QString &reason = QString()); void cancel(); void sendRequest(const QSharedPointer &connection); signals: void statusChanged(int newStatus, int oldStatus); void accepted(); void rejected(const QString &reason); void removed(); private slots: void requestStatusChanged(int status); private: SettingsObject *m_settings; void setStatus(Status newStatus); void removeRequest(); void attemptAutoAccept(); }; #endif // OUTGOINGCONTACTREQUEST_H ricochet-1.1.4/src/core/UserIdentity.cpp000066400000000000000000000241261300720305500201700ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "UserIdentity.h" #include "tor/TorControl.h" #include "tor/HiddenService.h" #include "core/ContactIDValidator.h" #include "protocol/Connection.h" #include "utils/Useful.h" #include #include #include #include using namespace Protocol; UserIdentity::UserIdentity(int id, QObject *parent) : QObject(parent) , uniqueID(id) , contacts(this) , m_settings(0) , m_hiddenService(0) , m_incomingServer(0) { m_settings = new SettingsObject(QStringLiteral("identity"), this); connect(m_settings, &SettingsObject::modified, this, &UserIdentity::onSettingsModified); setupService(); contacts.loadFromSettings(); } UserIdentity *UserIdentity::createIdentity(int uniqueID, const QString &dataDirectory) { // There is actually no support for multiple identities currently. Q_ASSERT(uniqueID == 0); if (uniqueID != 0) return 0; SettingsObject settings(QStringLiteral("identity")); settings.write("initializing", true); if (dataDirectory.isEmpty()) settings.write("dataDirectory", QString::fromLatin1("data-%1").arg(uniqueID)); else settings.write("dataDirectory", dataDirectory); return new UserIdentity(uniqueID); } // TODO: Handle the error cases of this function in a useful way void UserIdentity::setupService() { QString keyData = m_settings->read("serviceKey").toString(); QString legacyDir = m_settings->read("dataDirectory").toString(); if (!keyData.isEmpty()) { CryptoKey key; if (!key.loadFromData(QByteArray::fromBase64(keyData.toLatin1()), CryptoKey::PrivateKey, CryptoKey::DER)) { qWarning() << "Cannot load service key from configuration"; return; } m_hiddenService = new Tor::HiddenService(key, legacyDir, this); } else if (!legacyDir.isEmpty() && QFile::exists(legacyDir + QLatin1String("/private_key"))) { qDebug() << "Attempting to load key from legacy filesystem format in" << legacyDir; CryptoKey key; if (!key.loadFromFile(legacyDir + QLatin1String("/private_key"), CryptoKey::PrivateKey)) { qWarning() << "Cannot load legacy format key from" << legacyDir << "for conversion"; return; } else { keyData = QString::fromLatin1(key.encodedPrivateKey(CryptoKey::DER).toBase64()); m_settings->write("serviceKey", keyData); m_hiddenService = new Tor::HiddenService(key, legacyDir, this); } } else if (!m_settings->read("initializing").toBool()) { qWarning() << "Missing private key for initialized identity"; return; } else { m_hiddenService = new Tor::HiddenService(legacyDir, this); connect(m_hiddenService, &Tor::HiddenService::privateKeyChanged, this, [&]() { QString key = QString::fromLatin1(m_hiddenService->privateKey().encodedPrivateKey(CryptoKey::DER).toBase64()); m_settings->write("serviceKey", key); } ); } Q_ASSERT(m_hiddenService); connect(m_hiddenService, SIGNAL(statusChanged(int,int)), SLOT(onStatusChanged(int,int))); // Generally, these are not used, and we bind to localhost and port 0 // for an automatic (and portable) selection. QHostAddress address(m_settings->read("localListenAddress").toString()); if (address.isNull()) address = QHostAddress::LocalHost; quint16 port = (quint16)m_settings->read("localListenPort").toInt(); m_incomingServer = new QTcpServer(this); if (!m_incomingServer->listen(address, port)) { // XXX error case qWarning() << "Failed to open incoming socket:" << m_incomingServer->errorString(); return; } connect(m_incomingServer, &QTcpServer::newConnection, this, &UserIdentity::onIncomingConnection); m_hiddenService->addTarget(9878, m_incomingServer->serverAddress(), m_incomingServer->serverPort()); torControl->addHiddenService(m_hiddenService); } SettingsObject *UserIdentity::settings() { return m_settings; } QString UserIdentity::hostname() const { return m_hiddenService ? m_hiddenService->hostname() : QString(); } QString UserIdentity::contactID() const { return ContactIDValidator::idFromHostname(hostname()); } QString UserIdentity::nickname() const { return m_settings->read("nickname").toString(); } void UserIdentity::setNickname(const QString &nick) { m_settings->write("nickname", nick); } void UserIdentity::onSettingsModified(const QString &key, const QJsonValue &value) { Q_UNUSED(value); if (key == QLatin1String("nickname")) emit nicknameChanged(); } void UserIdentity::onStatusChanged(int newStatus, int oldStatus) { if (oldStatus == Tor::HiddenService::NotCreated && newStatus > oldStatus) { m_settings->write("initializing", QJsonValue::Undefined); emit contactIDChanged(); } emit statusChanged(); } bool UserIdentity::isServiceOnline() const { return m_hiddenService && m_hiddenService->status() == Tor::HiddenService::Online; } /* Handle an incoming connection to this service * * A Protocol::Connection is created to handle this socket. The * connection initially has a purpose of Unknown. It times out * and automatically closes after ConnectionPrivate::UnknownPurposeTimeout * seconds, unless the purpose is changed. * * If the connection successfully completes authentication, * handleIncomingAuthedConnection is called to link it to a ContactUser * (if applicable) and set the purpose. */ void UserIdentity::onIncomingConnection() { while (m_incomingServer->hasPendingConnections()) { QTcpSocket *socket = m_incomingServer->nextPendingConnection(); /* The localHostname property is used by Connection to determine the * server onion hostname that this socket is connected to, which is * used by the serverHostname() method. */ socket->setProperty("localHostname", m_hiddenService->hostname()); qDebug() << "Accepted new incoming connection"; QSharedPointer conn(new Connection(socket, Connection::ServerSide), &QObject::deleteLater); Q_ASSERT(socket->parent()); m_incomingConnections.append(conn); Connection *connPtr = conn.data(); /* When the connection is closed, if it's not claimed, take it out of the * incoming connection list and destroy the reference */ connect(connPtr, &Connection::closed, this, [this,connPtr]() { QSharedPointer conn(takeIncomingConnection(connPtr)); if (conn) qDebug() << "Deleting closed incoming connection that was never claimed by an owner"; } ); connect(connPtr, &Connection::authenticated, this, [this,connPtr](Connection::AuthenticationType type) { if (type == Connection::HiddenServiceAuth) handleIncomingAuthedConnection(connPtr); } ); emit incomingConnection(connPtr); } } void UserIdentity::handleIncomingAuthedConnection(Connection *conn) { if (conn->purpose() != Connection::Purpose::Unknown) return; QString clientName = conn->authenticatedIdentity(Connection::HiddenServiceAuth); if (clientName.isEmpty()) { BUG() << "Called to handle incoming authed connection without any authed name"; return; } ContactUser *user = contacts.lookupHostname(clientName); if (!user) { // This client can start a contact request, for example. The purpose stays unknown, and the // connection will be killed if the purpose isn't changed before the timeout. qDebug() << "Have an incoming connection authenticated as unknown client" << clientName; return; } QSharedPointer connPtr(takeIncomingConnection(conn)); if (!connPtr) { BUG() << "Called to handle incoming authed connection, but the connection is already out of the incoming list"; return; } qDebug() << "Incoming connection authenticated as contact" << user->uniqueID << "with hostname" << clientName; user->assignConnection(connPtr); } QSharedPointer UserIdentity::takeIncomingConnection(Connection *match) { for (auto it = m_incomingConnections.begin(); it != m_incomingConnections.end(); it++) { if (it->data() == match) { QSharedPointer re = *it; m_incomingConnections.erase(it); return re; } } return QSharedPointer(); } ricochet-1.1.4/src/core/UserIdentity.h000066400000000000000000000106561300720305500176400ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef USERIDENTITY_H #define USERIDENTITY_H #include "ContactsManager.h" #include #include #include #include namespace Tor { class HiddenService; } namespace Protocol { class Connection; } class QTcpServer; /* UserIdentity represents the local identity offered by the user. * * In particular, it represents the published hidden service, and * theoretically holds the list of contacts. * * At present, implementation (and settings) assumes that there is * only one identity, but some code is confusingly written to allow * for several. */ class UserIdentity : public QObject { Q_OBJECT Q_DISABLE_COPY(UserIdentity) friend class IdentityManager; Q_PROPERTY(int uniqueID READ getUniqueID CONSTANT) Q_PROPERTY(QString nickname READ nickname WRITE setNickname NOTIFY nicknameChanged) Q_PROPERTY(QString contactID READ contactID NOTIFY contactIDChanged) Q_PROPERTY(bool isOnline READ isServiceOnline NOTIFY statusChanged) Q_PROPERTY(ContactsManager *contacts READ getContacts CONSTANT) Q_PROPERTY(SettingsObject *settings READ settings CONSTANT) public: const int uniqueID; ContactsManager contacts; explicit UserIdentity(int uniqueID, QObject *parent = 0); /* Properties */ int getUniqueID() const { return uniqueID; } QString nickname() const; /* Hostname is .onion format, like ContactUser */ QString hostname() const; QString contactID() const; ContactsManager *getContacts() { return &contacts; } void setNickname(const QString &nickname); /* State */ bool isServiceOnline() const; Tor::HiddenService *hiddenService() const { return m_hiddenService; } SettingsObject *settings(); /* Take ownership of an inbound connection. Returns the shared pointer to * the connection, and releases the reference held by UserIdentity. */ QSharedPointer takeIncomingConnection(Protocol::Connection *connection); signals: void statusChanged(); void contactIDChanged(); // only possible during creation void nicknameChanged(); void settingsChanged(const QString &key); void incomingConnection(Protocol::Connection *connection); private slots: void onStatusChanged(int newStatus, int oldStatus); void onSettingsModified(const QString &key, const QJsonValue &value); void onIncomingConnection(); private: SettingsObject *m_settings; Tor::HiddenService *m_hiddenService; QTcpServer *m_incomingServer; QVector> m_incomingConnections; static UserIdentity *createIdentity(int uniqueID, const QString &dataDirectory = QString()); void handleIncomingAuthedConnection(Protocol::Connection *connection); void setupService(); }; Q_DECLARE_METATYPE(UserIdentity*) #endif // USERIDENTITY_H ricochet-1.1.4/src/main.cpp000066400000000000000000000325401300720305500155330ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "ui/MainWindow.h" #include "core/IdentityManager.h" #include "tor/TorManager.h" #include "tor/TorControl.h" #include "utils/CryptoKey.h" #include "utils/SecureRNG.h" #include "utils/Settings.h" #include #include #include #include #include #include #include #include #include #include #include #include static bool initSettings(SettingsFile *settings, QLockFile **lockFile, QString &errorMessage); static bool importLegacySettings(SettingsFile *settings, const QString &oldPath); static void initTranslation(); int main(int argc, char *argv[]) { /* Disable rwx memory. This will also ensure full PAX/Grsecurity protections. */ qputenv("QV4_FORCE_INTERPRETER", "1"); qputenv("QT_ENABLE_REGEXP_JIT", "0"); /* Use QtQuick 2D renderer by default; ignored if not available */ if (qEnvironmentVariableIsEmpty("QMLSCENE_DEVICE")) qputenv("QMLSCENE_DEVICE", "softwarecontext"); QApplication a(argc, argv); a.setApplicationVersion(QLatin1String("1.1.4")); a.setOrganizationName(QStringLiteral("Ricochet")); #if !defined(Q_OS_WIN) && !defined(Q_OS_MAC) a.setWindowIcon(QIcon(QStringLiteral(":/icons/ricochet.svg"))); #endif QScopedPointer settings(new SettingsFile); SettingsObject::setDefaultFile(settings.data()); QString error; QLockFile *lock = 0; if (!initSettings(settings.data(), &lock, error)) { QMessageBox::critical(0, qApp->translate("Main", "Ricochet Error"), error); return 1; } QScopedPointer lockFile(lock); initTranslation(); /* Initialize OpenSSL's allocator */ #if OPENSSL_VERSION_NUMBER < 0x10100000L CRYPTO_malloc_init(); #else OPENSSL_malloc_init(); #endif /* Seed the OpenSSL RNG */ if (!SecureRNG::seed()) qFatal("Failed to initialize RNG"); qsrand(SecureRNG::randomInt(UINT_MAX)); /* Tor control manager */ Tor::TorManager *torManager = Tor::TorManager::instance(); torManager->setDataDirectory(QFileInfo(settings->filePath()).path() + QStringLiteral("/tor/")); torControl = torManager->control(); torManager->start(); /* Identities */ identityManager = new IdentityManager; QScopedPointer scopedIdentityManager(identityManager); /* Window */ QScopedPointer w(new MainWindow); if (!w->showUI()) return 1; return a.exec(); } static QString userConfigPath() { QString path = QStandardPaths::writableLocation(QStandardPaths::DataLocation); QString oldPath = path; oldPath.replace(QStringLiteral("Ricochet"), QStringLiteral("Torsion"), Qt::CaseInsensitive); if (QFile::exists(oldPath)) return oldPath; return path; } #ifdef Q_OS_MAC static QString appBundlePath() { QString path = QApplication::applicationDirPath(); int p = path.lastIndexOf(QLatin1String(".app/")); if (p >= 0) { p = path.lastIndexOf(QLatin1Char('/'), p); path = path.left(p+1); } return path; } #endif // Writes default settings to settings object. Does not care about any // preexisting values, therefore this is best used on a fresh object. static void loadDefaultSettings(SettingsFile *settings) { settings->root()->write("ui.combinedChatWindow", true); } static bool initSettings(SettingsFile *settings, QLockFile **lockFile, QString &errorMessage) { /* If built in portable mode (default), configuration is stored in the 'config' * directory next to the binary. If not writable, launching fails. * * Portable OS X is an exception. In that case, configuration is stored in a * 'config.ricochet' folder next to the application bundle, unless the application * path contains "/Applications", in which case non-portable mode is used. * * When not in portable mode, a platform-specific per-user config location is used. * * This behavior may be overriden by passing a folder path as the first argument. */ QString configPath; QStringList args = qApp->arguments(); if (args.size() > 1) { configPath = args[1]; } else { #ifndef RICOCHET_NO_PORTABLE # ifdef Q_OS_MAC if (!qApp->applicationDirPath().contains(QStringLiteral("/Applications"))) { // Try old configuration path first configPath = appBundlePath() + QStringLiteral("config.torsion"); if (!QFile::exists(configPath)) configPath = appBundlePath() + QStringLiteral("config.ricochet"); } # else configPath = qApp->applicationDirPath() + QStringLiteral("/config"); # endif #endif if (configPath.isEmpty()) configPath = userConfigPath(); } QDir dir(configPath); if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) { errorMessage = QStringLiteral("Cannot create directory: %1").arg(dir.path()); return false; } // Reset to config directory for consistency; avoid depending on this behavior for paths if (QDir::setCurrent(dir.absolutePath()) && dir.isRelative()) dir.setPath(QStringLiteral(".")); QLockFile *lock = new QLockFile(dir.filePath(QStringLiteral("ricochet.json.lock"))); *lockFile = lock; lock->setStaleLockTime(0); if (!lock->tryLock()) { if (lock->error() == QLockFile::LockFailedError) { // This happens if a stale lock file exists and another process uses that PID. // Try removing the stale file, which will fail if a real process is holding a // file-level lock. A false error is more problematic than not locking properly // on corner-case systems. if (!lock->removeStaleLockFile() || !lock->tryLock()) { errorMessage = QStringLiteral("Configuration file is already in use"); return false; } else qDebug() << "Removed stale lock file"; } else { errorMessage = QStringLiteral("Cannot write configuration file (failed to acquire lock)"); return false; } } settings->setFilePath(dir.filePath(QStringLiteral("ricochet.json"))); if (settings->hasError()) { errorMessage = settings->errorMessage(); return false; } if (settings->root()->data().isEmpty()) { QString filePath = dir.filePath(QStringLiteral("Torsion.ini")); if (!QFile::exists(filePath)) filePath = dir.filePath(QStringLiteral("ricochet.ini")); if (QFile::exists(filePath)) importLegacySettings(settings, filePath); } // if still empty, load defaults here if (settings->root()->data().isEmpty()) { loadDefaultSettings(settings); } return true; } static void copyKeys(QSettings &old, SettingsObject *object) { foreach (const QString &key, old.childKeys()) { QVariant value = old.value(key); if ((QMetaType::Type)value.type() == QMetaType::QDateTime) object->write(key, value.toDateTime()); else if ((QMetaType::Type)value.type() == QMetaType::QByteArray) object->write(key, Base64Encode(value.toByteArray())); else object->write(key, value.toString()); } } static bool importLegacySettings(SettingsFile *settings, const QString &oldPath) { QSettings old(oldPath, QSettings::IniFormat); SettingsObject *root = settings->root(); QVariant value; qDebug() << "Importing legacy format settings from" << oldPath; if (!(value = old.value(QStringLiteral("tor/controlIp"))).isNull()) root->write("tor.controlAddress", value.toString()); if (!(value = old.value(QStringLiteral("tor/controlPort"))).isNull()) root->write("tor.controlPort", value.toInt()); if (!(value = old.value(QStringLiteral("tor/authPassword"))).isNull()) root->write("tor.controlPassword", value.toString()); if (!(value = old.value(QStringLiteral("tor/socksIp"))).isNull()) root->write("tor.socksAddress", value.toString()); if (!(value = old.value(QStringLiteral("tor/socksPort"))).isNull()) root->write("tor.socksPort", value.toInt()); if (!(value = old.value(QStringLiteral("tor/executablePath"))).isNull()) root->write("tor.executablePath", value.toString()); if (!(value = old.value(QStringLiteral("core/neverPublishService"))).isNull()) root->write("tor.neverPublishServices", value.toBool()); if (!(value = old.value(QStringLiteral("identity/0/dataDirectory"))).isNull()) root->write("identity.dataDirectory", value.toString()); if (!(value = old.value(QStringLiteral("identity/0/createNewService"))).isNull()) root->write("identity.initializing", value.toBool()); if (!(value = old.value(QStringLiteral("core/listenIp"))).isNull()) root->write("identity.localListenAddress", value.toString()); if (!(value = old.value(QStringLiteral("core/listenPort"))).isNull()) root->write("identity.localListenPort", value.toInt()); { old.beginGroup(QStringLiteral("contacts")); QStringList ids = old.childGroups(); foreach (const QString &id, ids) { old.beginGroup(id); SettingsObject userObject(root, QStringLiteral("contacts.%1").arg(id)); copyKeys(old, &userObject); if (old.childGroups().contains(QStringLiteral("request"))) { old.beginGroup(QStringLiteral("request")); QStringList requestKeys = old.childKeys(); foreach (const QString &key, requestKeys) userObject.write(QStringLiteral("request.") + key, old.value(key).toString()); old.endGroup(); } old.endGroup(); } old.endGroup(); } { old.beginGroup(QStringLiteral("contactRequests")); QStringList contacts = old.childGroups(); foreach (const QString &hostname, contacts) { old.beginGroup(hostname); SettingsObject requestObject(root, QStringLiteral("contactRequests.%1").arg(hostname)); copyKeys(old, &requestObject); old.endGroup(); } old.endGroup(); } if (!(value = old.value(QStringLiteral("core/hostnameBlacklist"))).isNull()) { QStringList blacklist = value.toStringList(); root->write("identity.hostnameBlacklist", QJsonArray::fromStringList(blacklist)); } return true; } static void initTranslation() { QTranslator *translator = new QTranslator; bool ok = false; QString appPath = qApp->applicationDirPath(); QString resPath = QLatin1String(":/lang/"); QLocale locale = QLocale::system(); if (!qgetenv("RICOCHET_LOCALE").isEmpty()) { locale = QLocale(QString::fromLatin1(qgetenv("RICOCHET_LOCALE"))); qDebug() << "Forcing locale" << locale << "from environment" << locale.uiLanguages(); } SettingsObject settings; QString settingsLanguage(settings.read("ui.language").toString()); if (!settingsLanguage.isEmpty()) { locale = settingsLanguage; } else { //write an empty string to get "System default" language selected automatically in preferences settings.write(QStringLiteral("ui.language"), QString()); } ok = translator->load(locale, QStringLiteral("ricochet"), QStringLiteral("_"), appPath); if (!ok) ok = translator->load(locale, QStringLiteral("ricochet"), QStringLiteral("_"), resPath); if (ok) { qApp->installTranslator(translator); QTranslator *qtTranslator = new QTranslator; ok = qtTranslator->load(QStringLiteral("qt_") + locale.name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath)); if (ok) qApp->installTranslator(qtTranslator); else delete qtTranslator; } else delete translator; } ricochet-1.1.4/src/protocol/000077500000000000000000000000001300720305500157405ustar00rootroot00000000000000ricochet-1.1.4/src/protocol/AuthHiddenService.proto000066400000000000000000000011061300720305500223610ustar00rootroot00000000000000package Protocol.Data.AuthHiddenService; import "ControlChannel.proto"; extend Control.OpenChannel { optional bytes client_cookie = 7200; // 16 random bytes } extend Control.ChannelResult { optional bytes server_cookie = 7200; // 16 random bytes } message Packet { optional Proof proof = 1; optional Result result = 2; } message Proof { optional bytes public_key = 1; // DER encoded public key optional bytes signature = 2; // RSA signature } message Result { required bool accepted = 1; optional bool is_known_contact = 2; } ricochet-1.1.4/src/protocol/AuthHiddenServiceChannel.cpp000066400000000000000000000303711300720305500232770ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "AuthHiddenServiceChannel.h" #include "AuthHiddenService.pb.h" #include "Connection.h" #include "Channel_p.h" #include "utils/SecureRNG.h" #include "utils/CryptoKey.h" #include "utils/Useful.h" #include using namespace Protocol; namespace Protocol { class AuthHiddenServiceChannelPrivate : public ChannelPrivate { public: CryptoKey privateKey; QByteArray clientCookie, serverCookie; bool accepted; AuthHiddenServiceChannelPrivate(Channel *q, Channel::Direction direction, Connection *conn) : ChannelPrivate(q, QStringLiteral("im.ricochet.auth.hidden-service"), direction, conn) , accepted(false) { } QByteArray getProofData(const QString &clientHostname); }; } AuthHiddenServiceChannel::AuthHiddenServiceChannel(Direction dir, Connection *conn) : Channel(new AuthHiddenServiceChannelPrivate(this, dir, conn)) { if (direction() == Outbound) connect(this, &Channel::channelOpened, this, &AuthHiddenServiceChannel::sendAuthMessage); connect(this, &Channel::invalidated, this, [this]() { Q_D(AuthHiddenServiceChannel); if (d->accepted) emit authSuccessful(); else emit authFailed(); } ); } void AuthHiddenServiceChannel::setPrivateKey(const CryptoKey &key) { Q_D(AuthHiddenServiceChannel); if (isOpened()) { BUG() << "Channel is already open"; return; } if (!key.isLoaded() || !key.isPrivate()) { BUG() << "AuthHiddenServiceChannel cannot authenticate without a valid private key"; return; } d->privateKey = key; } bool AuthHiddenServiceChannel::allowInboundChannelRequest(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result) { Q_D(AuthHiddenServiceChannel); using namespace Data::Control; if (connection()->direction() != Connection::ServerSide) { // Hidden service authentication is only allowed from the client-side connection qDebug() << "Rejecting AuthHiddenServiceChannel from server side"; result->set_common_error(ChannelResult::BadUsageError); return false; } if (connection()->hasAuthenticated(Connection::HiddenServiceAuth)) { // You can only authenticate a connection once qDebug() << "Rejecting AuthHiddenServiceChannel on authenticated connection"; result->set_common_error(ChannelResult::BadUsageError); return false; } if (connection()->findChannel()) { // Refuse if another channel already exists qDebug() << "Rejecting instance of AuthHiddenServiceChannel on a connection that already has one"; result->set_common_error(ChannelResult::BadUsageError); return false; } // Store client cookie std::string clientCookie = request->GetExtension(Data::AuthHiddenService::client_cookie); if (clientCookie.size() != 16) { qDebug() << "Received OpenChannel for" << type() << "with no valid client_cookie"; result->set_common_error(ChannelResult::BadUsageError); return false; } d->clientCookie = QByteArray(clientCookie.c_str(), clientCookie.size()); // Generate a random cookie and return result d->serverCookie = SecureRNG::random(16); if (d->serverCookie.isEmpty()) return false; qDebug() << "Accepted inbound AuthHiddenServiceChannel"; result->SetExtension(Data::AuthHiddenService::server_cookie, std::string(d->serverCookie.constData(), d->serverCookie.size())); return true; } bool AuthHiddenServiceChannel::allowOutboundChannelRequest(Data::Control::OpenChannel *request) { Q_D(AuthHiddenServiceChannel); if (!d->privateKey.isLoaded()) { BUG() << "AuthHiddenServiceChannel can't be opened without a private key"; return false; } d->clientCookie = SecureRNG::random(16); if (d->clientCookie.isEmpty()) return false; request->SetExtension(Data::AuthHiddenService::client_cookie, std::string(d->clientCookie.constData(), d->clientCookie.size())); return true; } bool AuthHiddenServiceChannel::processChannelOpenResult(const Data::Control::ChannelResult *result) { Q_D(AuthHiddenServiceChannel); if (result->opened()) { std::string cookie = result->GetExtension(Data::AuthHiddenService::server_cookie); if (cookie.size() != 16) { qDebug() << "Received ChannelResult for" << type() << "with no valid server_cookie"; return false; } d->serverCookie = QByteArray(cookie.c_str(), cookie.size()); return true; } return false; } void AuthHiddenServiceChannel::sendAuthMessage() { Q_D(AuthHiddenServiceChannel); if (direction() != Outbound) { BUG() << "Proof message is only sent from outbound channels"; return; } if (!isOpened()) return; if (d->clientCookie.size() != 16 || d->serverCookie.size() != 16) { BUG() << "AuthHiddenServiceChannel can't create a proof without valid cookies"; closeChannel(); return; } QByteArray publicKey = d->privateKey.encodedPublicKey(CryptoKey::DER); if (publicKey.size() > 150) { BUG() << "Unexpected size for encoded public key"; closeChannel(); return; } QByteArray signature; QByteArray proofData = d->getProofData(d->privateKey.torServiceID()); if (!proofData.isEmpty()) { QByteArray proofHMAC = QMessageAuthenticationCode::hash(proofData, d->clientCookie + d->serverCookie, QCryptographicHash::Sha256); signature = d->privateKey.signSHA256(proofHMAC); } if (signature.isEmpty()) { BUG() << "Creating proof on AuthHiddenServiceChannel failed"; closeChannel(); return; } QScopedPointer proof(new Data::AuthHiddenService::Proof); proof->set_public_key(std::string(publicKey.constData(), publicKey.size())); proof->set_signature(std::string(signature.constData(), signature.size())); Data::AuthHiddenService::Packet message; message.set_allocated_proof(proof.take()); sendMessage(message); qDebug() << "AuthHiddenServiceChannel sent outbound authentication packet"; } QByteArray AuthHiddenServiceChannelPrivate::getProofData(const QString &client) { QByteArray serverHostname = connection->serverHostname().toLatin1().mid(0, 16); QByteArray clientHostname = client.toLatin1(); if (clientHostname.size() != 16 || serverHostname.size() != 16) { BUG() << "AuthHiddenServiceChannel can't figure out the client and server hostnames"; return QByteArray(); } return clientHostname + serverHostname; } void AuthHiddenServiceChannel::receivePacket(const QByteArray &packet) { Data::AuthHiddenService::Packet message; if (!message.ParseFromArray(packet.constData(), packet.size())) { closeChannel(); return; } if (message.has_proof()) { handleProof(message.proof()); } else if (message.has_result()) { handleResult(message.result()); } else { qWarning() << "Unrecognized message on" << type(); closeChannel(); } } void AuthHiddenServiceChannel::handleProof(const Data::AuthHiddenService::Proof &message) { Q_D(AuthHiddenServiceChannel); if (direction() != Inbound) { qWarning() << "Received unexpected proof on outbound" << type(); closeChannel(); return; } if (d->clientCookie.size() != 16 || d->serverCookie.size() != 16) { BUG() << "AuthHiddenServiceChannel can't create a proof without valid cookies"; closeChannel(); return; } QByteArray publicKeyData(message.public_key().c_str(), message.public_key().size()); QByteArray signature(message.signature().c_str(), message.signature().size()); QScopedPointer result(new Data::AuthHiddenService::Result); result->set_accepted(false); // Hidden services always use a 1024bit key. A valid signature will always be exactly 128 bytes. CryptoKey publicKey; if (signature.size() != 128) { qWarning() << "Received invalid signature (size" << signature.size() << ") on" << type(); } else if (publicKeyData.size() > 150) { qWarning() << "Received invalid public key (size" << publicKeyData.size() << ") on" << type(); } else if (!publicKey.loadFromData(publicKeyData, CryptoKey::PublicKey, CryptoKey::DER)) { qWarning() << "Unable to parse public key from" << type(); } else if (publicKey.bits() != 1024) { qWarning() << "Received invalid public key (" << publicKey.bits() << "bits) on" << type(); } else { bool ok = false; QByteArray proofData = d->getProofData(publicKey.torServiceID()); if (!proofData.isEmpty()) { QByteArray proofHMAC = QMessageAuthenticationCode::hash(proofData, d->clientCookie + d->serverCookie, QCryptographicHash::Sha256); ok = publicKey.verifySHA256(proofHMAC, signature); } if (!ok) { qWarning() << "Signature verification failed on" << type(); result->set_accepted(false); } else { result->set_accepted(true); qDebug() << type() << "accepted inbound authentication for" << publicKey.torServiceID(); } } if (result->accepted()) { connection()->grantAuthentication(Connection::HiddenServiceAuth, publicKey.torServiceID() + QStringLiteral(".onion")); d->accepted = true; result->set_is_known_contact(connection()->purpose() == Connection::Purpose::KnownContact); } else { d->accepted = false; } Data::AuthHiddenService::Packet resultMessage; resultMessage.set_allocated_result(result.data()); sendMessage(resultMessage); // Clear QScopedPointer, value is now owned by the Packet result.take(); // In all cases, close the channel afterwards. This also emits the // authSucceeded or authFailed signals. closeChannel(); } void AuthHiddenServiceChannel::handleResult(const Data::AuthHiddenService::Result &message) { Q_D(AuthHiddenServiceChannel); if (direction() != Outbound) { qWarning() << "Received invalid message on AuthHiddenServiceChannel"; closeChannel(); return; } if (message.accepted()) { qDebug() << "AuthHiddenServiceChannel succeeded as" << (message.is_known_contact() ? "known" : "unknown") << "contact"; d->accepted = true; if (message.is_known_contact()) connection()->grantAuthentication(Connection::KnownToPeer); } else { qWarning() << "AuthHiddenServiceChannel rejected"; d->accepted = false; } closeChannel(); } ricochet-1.1.4/src/protocol/AuthHiddenServiceChannel.h000066400000000000000000000054001300720305500227370ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef PROTOCOL_AUTHHIDDENSERVICECHANNEL_H #define PROTOCOL_AUTHHIDDENSERVICECHANNEL_H #include "Channel.h" #include "utils/CryptoKey.h" #include "AuthHiddenService.pb.h" namespace Protocol { class AuthHiddenServiceChannelPrivate; class AuthHiddenServiceChannel : public Channel { Q_OBJECT Q_DISABLE_COPY(AuthHiddenServiceChannel) Q_DECLARE_PRIVATE(AuthHiddenServiceChannel) public: explicit AuthHiddenServiceChannel(Direction direction, Connection *connection); void setPrivateKey(const CryptoKey &key); signals: void authSuccessful(); void authFailed(); private slots: void sendAuthMessage(); protected: virtual bool allowInboundChannelRequest(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result); virtual bool allowOutboundChannelRequest(Data::Control::OpenChannel *request); virtual bool processChannelOpenResult(const Data::Control::ChannelResult *result); virtual void receivePacket(const QByteArray &packet); private: void handleProof(const Data::AuthHiddenService::Proof &message); void handleResult(const Data::AuthHiddenService::Result &message); }; } #endif ricochet-1.1.4/src/protocol/Channel.cpp000066400000000000000000000235141300720305500200210ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "Channel_p.h" #include "Connection_p.h" #include "ControlChannel.h" #include "utils/Useful.h" #include #include "AuthHiddenServiceChannel.h" #include "ChatChannel.h" #include "ContactRequestChannel.h" using namespace Protocol; Channel *Channel::create(const QString &type, Direction direction, Connection *connection) { if (!connection) return 0; if (type == QStringLiteral("im.ricochet.auth.hidden-service")) { return new AuthHiddenServiceChannel(direction, connection); } else if (type == QStringLiteral("im.ricochet.chat")) { return new ChatChannel(direction, connection); } else if (type == QStringLiteral("im.ricochet.contact.request")) { return new ContactRequestChannel(direction, connection); } else { return 0; } } Channel::Channel(const QString &type, Direction direction, Connection *connection) : QObject(connection) , d_ptr(new ChannelPrivate(this, type, direction, connection)) { } Channel::Channel(ChannelPrivate *d_ptr) : QObject(d_ptr->connection) , d_ptr(d_ptr) { } Channel::~Channel() { Q_D(Channel); if (d->identifier >= 0 && !d->isInvalidated) d->connection->d->removeChannel(this); } QString Channel::type() const { Q_D(const Channel); return d->type; } // May return -1 for unassigned channels int Channel::identifier() const { Q_D(const Channel); if (d->identifier > UINT16_MAX) return -1; return d->identifier; } Channel::Direction Channel::direction() const { Q_D(const Channel); return d->direction; } Connection *Channel::connection() { Q_D(Channel); return d->connection; } bool Channel::isOpened() const { Q_D(const Channel); if (d->isOpened && d->identifier < 0) BUG() << "Channel is marked as open, but has no identifier"; return d->isOpened; } bool Channel::openChannel() { Q_D(Channel); if (direction() != Channel::Outbound || isOpened() || identifier() >= 0) { BUG() << "Cannot send request to open" << type() << "channel in an incorrect state"; if (isOpened()) closeChannel(); d->invalidate(); return false; } else if (!connection()->findChannel()->sendOpenChannel(this)) { if (isOpened()) { BUG() << "Channel somehow opened instantly in an impossible situation"; closeChannel(); } d->invalidate(); return false; } return true; } void Channel::closeChannel() { Q_D(Channel); if (!d->hasSentClose && d->identifier >= 0 && connection()->isConnected()) { d->hasSentClose = true; bool ok = connection()->d->writePacket(this, QByteArray()); if (!ok) qDebug() << "Failed sending channel close message"; } // Invalidate will remove and eventually destroy the Channel d->isOpened = false; d->invalidate(); } /* Called by ControlChannel to handle an inbound OpenChannel message. * This Channel must be in a clean, inbound state. The Channel subclass * decides whether to accept the request, and can add data to the result. */ bool ChannelPrivate::openChannelInbound(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result) { Q_Q(Channel); result->set_opened(false); if (direction != Channel::Inbound || isOpened || identifier >= 0 || hasSentClose || isInvalidated) { BUG() << "Handling inbound open channel request on a channel in an unexpected state; rejecting"; return false; } if (request->channel_identifier() <= 0) { BUG() << "Invalid channel identifier in inboundOpenChannel handler"; return false; } // The Connection::channelCreated signal must emit once, after the Channel // is fully constructed, but before it's used. This is that moment: just // before we check whether to allow an inbound/outbound request. emit connection->channelCreated(q); if (!q->allowInboundChannelRequest(request, result)) return false; if (isInvalidated) { // This can happen as a result of something calling closeChannel under the // allowInboundChannelRequest handler, which can't be easily ruled out due // to signals. Treat is as a generic error and fail the channel. result->set_opened(false); return false; } if (result->has_common_error()) { BUG() << "Accepted inbound OpenChannel request, but result has error details set. Assuming it's actually an error."; result->set_opened(false); return false; } result->set_opened(true); identifier = request->channel_identifier(); isOpened = true; emit q->channelOpened(); return true; } bool ChannelPrivate::openChannelOutbound(Data::Control::OpenChannel *request) { Q_Q(Channel); if (direction != Channel::Outbound || isOpened || identifier >= 0) { BUG() << "Handling outbound open channel request on a channel in an unexpected state; rejecting"; return false; } // The Connection::channelCreated signal must emit once, after the Channel // is fully constructed, but before it's used. This is that moment: just // before we check whether to allow an inbound/outbound request. emit connection->channelCreated(q); if (!q->allowOutboundChannelRequest(request)) return false; request->set_channel_type(type.toStdString()); identifier = request->channel_identifier(); return true; } bool ChannelPrivate::openChannelResult(const Data::Control::ChannelResult *result) { Q_Q(Channel); // ControlChannel should weed out clearly invalid messages, so assert here if it didn't if (direction != Channel::Outbound || isOpened || identifier < 0) { BUG() << "Handling response for outbound open channel on a channel in an unexpected state; ignoring"; return false; } bool ok = result->opened(); if (!q->processChannelOpenResult(result)) { // If the peer thinks the channel was opened successfully, send a close if (result->opened()) q->closeChannel(); ok = false; } if (ok) { isOpened = true; emit q->channelOpened(); } else { Data::Control::ChannelResult::CommonError error = Data::Control::ChannelResult::GenericError; if (result->has_common_error()) error = result->common_error(); emit q->channelRejected(error); invalidate(); } return ok; } bool Channel::processChannelOpenResult(const Data::Control::ChannelResult *result) { Q_UNUSED(result); return true; } bool Channel::sendPacket(const QByteArray &packet) { Q_D(Channel); if (d->identifier < 0) { BUG() << "Cannot send packet to channel" << type() << "without an assigned identifier"; return false; } if (packet.size() == 0) { BUG() << "Cannot send empty packet to channel" << type(); return false; } if (packet.size() > ConnectionPrivate::PacketMaxDataSize) { BUG() << "Packet is too big on channel" << type(); return false; } return connection()->d->writePacket(this, packet); } void Channel::requestInboundApproval() { if (direction() != Channel::Inbound || isOpened()) { BUG() << "Called in an unexpected channel state"; return; } emit connection()->channelRequestingInboundApproval(this); } ChannelPrivate::ChannelPrivate(Channel *q, const QString &type, Channel::Direction direction, Connection *conn) : q_ptr(q) , connection(conn) , type(type) , identifier(-1) , direction(direction) , isOpened(false) , hasSentClose(false) , isInvalidated(false) { } ChannelPrivate::~ChannelPrivate() { Q_Q(Channel); if (identifier >= 0 && !isInvalidated) { BUG() << "Channel of type" << type << "was deleted without being invalidated"; connection->d->removeChannel(q); } } void ChannelPrivate::invalidate() { Q_Q(Channel); if (isInvalidated) return; Q_ASSERT(!isOpened); qDebug() << "Invalidating channel" << q << "type" << type << "id" << identifier; isInvalidated = true; emit q->invalidated(); if (identifier >= 0) { connection->d->removeChannel(q); Q_ASSERT(!connection->channel(identifier)); } q->deleteLater(); } ricochet-1.1.4/src/protocol/Channel.h000066400000000000000000000227331300720305500174700ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef PROTOCOL_CHANNEL_H #define PROTOCOL_CHANNEL_H #include #include #include "ControlChannel.pb.h" namespace Protocol { class Connection; class ChannelPrivate; /* Base representation of a channel inside of a connection * * Channel is subclassed by channel type implementations to handle channel * requests and inbound or outbound packets. Generally, the channel subclass * implements low-level communication, and a higher level class will wrap * the channel with real functionality. * * Outbound channels are opened by creating an instance of the channel type, * setting up any necessary properties, and sending it to * ControlChannel::openChannel. The result is reported through the * outboundOpenResult callback, which will emit channelOpened or channelRejected. * * Incoming channel requests create an instance by name via the create method * and call the inboundOpenChannel method. If that method indicates success, the * channel is inserted. If failed, the channel will be closed and destroyed. * * When a channel is closed, the instance is invalidated and will be deleted * automatically. Pointers to channel should be stored using QPointer, or should * be reset immediately when the invalidated() signal is emitted. */ class Channel : public QObject { Q_OBJECT Q_DISABLE_COPY(Channel) Q_DECLARE_PRIVATE(Channel) friend class ControlChannel; friend class Connection; friend class ConnectionPrivate; public: enum Direction { Invalid = -1, Inbound, Outbound }; /* Create a Channel instance of the specified type * * Returns null if 'type' is unrecognized. */ static Channel *create(const QString &type, Direction direction, Connection *connection); QString type() const; int identifier() const; Direction direction() const; Connection *connection(); bool isOpened() const; /* Send the OpenChannel request for this channel * * Only valid when the channel hasn't been opened yet. If successful, * identifier() will be set and this function returns true. The channel * isn't open until the response arrives, signalled by the channelOpened * or channelRejected signals. * * If the channel is rejected, it will asynchronously emit the channelRejected * signal, and will be invalidated and deleted. * * If this function returns false, the request wasn't sent due to a local * error. In this case, the channel is also invalidated and will be deleted. */ bool openChannel(); signals: void channelOpened(); void channelRejected(Data::Control::ChannelResult::CommonError error); /* Emitted when the channel has become invalid and will be destroyed * * This signal is emitted when a channel is closed, an outbound channel request is * rejected, or the connection is lost. It indicates that the channel is no longer * valid and will be deleted once control reaches the event loop (i.e. * QObject::deleteLater). * * Any object using the channel must clear all references to it when this signal * is emitted. */ void invalidated(); public slots: void closeChannel(); protected: explicit Channel(const QString &type, Direction direction, Connection *connection); explicit Channel(ChannelPrivate *d); virtual ~Channel(); /* Determine the response to an inbound OpenChannel request * * Subclasses must implement this method to accept inbound OpenChannel requests. * The subclass implements any type-specific rules, and may update the result * object with error messages or other data. * * Basic sanity checking is performed before this method is called; it may * assume that the channel is in a sane state to receive an inbound request. * * The channel will be opened if this method returns true. */ virtual bool allowInboundChannelRequest(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result) = 0; /* Determine whether to send an outbound OpenChannel request * * Subclasses may implement this method to approve outbound OpenChannel requests, * and attach type-specific data to the request. * * Basic sanity checking is performed before this method is called; it may * assume that the channel is in a sane state to send an outbound request. * * Return true to send the OpenChannel request, false to cancel. */ virtual bool allowOutboundChannelRequest(Data::Control::OpenChannel *request) = 0; /* Process data from the response to an outbound OpenChannel request * * Subclasses may implement this method to handle data attached to a * ChannelResult message, received in response to an outbound OpenChannel * request. This method is called for all valid responses, including failure. * * Basic sanity checking is performed before this method is called; it may * assume that the channel was waiting for a response to an outbound request. * * Regardless of the result, the channel is not yet open when this method is * called. The channel will be opened (or failed and invalidated) afterwards. * * Return true to continue handling the response, false to cancel and close the * channel. The default implementation always returns true. */ virtual bool processChannelOpenResult(const Data::Control::ChannelResult *result); /* Process data from an inbound packet for this channel * * Subclasses must implement this method to handle inbound packets for this * channel. 'packet' is raw data from the packet, and will not be empty. * * Generally, a channel will parse packets using the protobuf ParseFromArray * method of their packet message type, and call appropriate handlers for * the messages it contains. */ virtual void receivePacket(const QByteArray &packet) = 0; /* Send raw data as a packet on this channel * * Sends the contents of 'packet' as a packet for this channel. Often, you * will not use this method directly, in favor of a method like sendMessage * that handles data serialization as well. 'packet' must not be empty. * * If this method returns false, the packet was not sent due to an error * with the state or contents of the packet. The caller is responsible for * handling any response to that failure. * * If this method returns true, but the packet later fails to send due to * a network issue, the channel will be closed. */ bool sendPacket(const QByteArray &packet); /* Serialize a protobuf message and send it as a packet on this channel * * This function behaves like sendPacket, except that it accepts a * templated subclass of google::protobuf::Message, and serializes that * message into the packet. In addition to the cases where sendPacket * returns false, this function will return false if serialization fails. */ template bool sendMessage(const T &message); /* Get approval for an inbound channel from the Connection's handlers * * Channels that require approval from higher-layer functionality before * opening can use this method to emit the * Connection::channelRequestingInboundApproval signal. For example, this * can be used to look up whether an identifier for a channel is recognized, * and allow higher layers to attach signals and update data before the * channel is fully open. * * Approval should be signaled to the channel by means of some * channel-specific API; in many cases, that might involve setting certain * properties that the channel requires. * * This method may only be called from within the * allowInboundChannelRequest handler. */ void requestInboundApproval(); QScopedPointer d_ptr; }; } #endif ricochet-1.1.4/src/protocol/Channel_p.h000066400000000000000000000070711300720305500200050ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef PROTOCOL_CHANNEL_P_H #define PROTOCOL_CHANNEL_P_H #include "Channel.h" #include "Connection_p.h" #include "utils/Useful.h" #include namespace Protocol { class ChannelPrivate : public QObject { Q_OBJECT Q_DISABLE_COPY(ChannelPrivate) Q_DECLARE_PUBLIC(Channel) public: explicit ChannelPrivate(Channel *q, const QString &type, Channel::Direction direction, Connection *conn); virtual ~ChannelPrivate(); Channel *q_ptr; Connection *connection; QString type; int identifier; Channel::Direction direction; bool isOpened; bool hasSentClose; bool isInvalidated; void invalidate(); // Called by ControlChannel to act on valid channel request/result messages bool openChannelInbound(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result); bool openChannelOutbound(Data::Control::OpenChannel *request); bool openChannelResult(const Data::Control::ChannelResult *result); }; template bool Channel::sendMessage(const T &message) { int size = message.ByteSize(); if (size > ConnectionPrivate::PacketMaxDataSize) { BUG() << "Message on" << type() << "channel is too big -" << size << "bytes:" << QString::fromStdString(message.DebugString()); return false; } if (size < 1) { BUG() << "Message on" << type() << "channel encoded as invalid length; this isn't possible to send:" << QString::fromStdString(message.DebugString()); return false; } QByteArray packet(size, 0); quint8 *end = message.SerializeWithCachedSizesToArray(reinterpret_cast(packet.data())); quint8 *expected_end = reinterpret_cast(packet.data() + size); if (end != expected_end) { BUG() << "Unexpected packet size after message serialization. Expected" << size << "but got" << qptrdiff(end - expected_end); return false; } return sendPacket(packet); } } #endif ricochet-1.1.4/src/protocol/ChatChannel.cpp000066400000000000000000000161451300720305500206230ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "ChatChannel.h" #include "Channel_p.h" #include "Connection.h" #include "utils/SecureRNG.h" #include "utils/Useful.h" using namespace Protocol; ChatChannel::ChatChannel(Direction direction, Connection *connection) : Channel(QStringLiteral("im.ricochet.chat"), direction, connection) { // The peer might use recent message IDs between connections to handle // re-send. Start at a random ID to reduce chance of collisions, then increment lastMessageId = SecureRNG::randomInt(UINT32_MAX); } bool ChatChannel::allowInboundChannelRequest(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result) { Q_UNUSED(request); if (connection()->purpose() != Connection::Purpose::KnownContact) { qDebug() << "Rejecting request for" << type() << "channel from connection with purpose" << int(connection()->purpose()); result->set_common_error(Data::Control::ChannelResult::UnauthorizedError); return false; } if (connection()->findChannel(Channel::Inbound)) { qDebug() << "Rejecting request for" << type() << "channel because one is already open"; return false; } return true; } bool ChatChannel::allowOutboundChannelRequest(Data::Control::OpenChannel *request) { Q_UNUSED(request); if (connection()->findChannel(Channel::Outbound)) { BUG() << "Rejecting outbound request for" << type() << "channel because one is already open on this connection"; return false; } if (connection()->purpose() != Connection::Purpose::KnownContact) { BUG() << "Rejecting outbound request for" << type() << "channel for connection with unexpected purpose" << int(connection()->purpose()); return false; } return true; } void ChatChannel::receivePacket(const QByteArray &packet) { Data::Chat::Packet message; if (!message.ParseFromArray(packet.constData(), packet.size())) { closeChannel(); return; } if (message.has_chat_message()) { handleChatMessage(message.chat_message()); } else if (message.has_chat_acknowledge()) { handleChatAcknowledge(message.chat_acknowledge()); } else { qWarning() << "Unrecognized message on" << type(); closeChannel(); } } bool ChatChannel::sendChatMessage(QString text, QDateTime time, MessageId &id) { id = ++lastMessageId; return sendChatMessageWithId(text, time, id); } bool ChatChannel::sendChatMessageWithId(QString text, QDateTime time, MessageId id) { if (direction() != Outbound) { BUG() << "Chat channels are unidirectional, and this is not an outbound channel"; return false; } QScopedPointer message(new Data::Chat::ChatMessage); message->set_message_id(id); if (text.isEmpty()) { BUG() << "Chat message is empty, and it should've been discarded"; return false; } else if (text.size() > MessageMaxCharacters) { BUG() << "Chat message is too long (" << text.size() << "characters), and it should've been limited already. Truncated."; text.truncate(MessageMaxCharacters); } // Also converts to UTF-8 message->set_message_text(text.toStdString()); if (!time.isNull()) message->set_time_delta(qMin(QDateTime::currentDateTime().secsTo(time), qint64(0))); Data::Chat::Packet packet; packet.set_allocated_chat_message(message.take()); if (!Channel::sendMessage(packet)) return false; pendingMessages.insert(id); return true; } void ChatChannel::handleChatMessage(const Data::Chat::ChatMessage &message) { QScopedPointer response(new Data::Chat::ChatAcknowledge); // QString::fromStdString decodes the string as UTF-8, replacing all invalid sequences and // codepoints with the unicode replacement character. QString text = QString::fromStdString(message.message_text()); if (direction() != Inbound) { qWarning() << "Rejected inbound message on an outbound chat channel"; response->set_accepted(false); } else if (text.isEmpty()) { qWarning() << "Rejected empty chat message"; response->set_accepted(false); } else if (text.size() > MessageMaxCharacters) { qWarning() << "Rejected oversize chat message of" << text.size() << "characters"; response->set_accepted(false); } else { QDateTime time = QDateTime::currentDateTime(); if (message.has_time_delta() && message.time_delta() <= 0) time = time.addSecs(message.time_delta()); emit messageReceived(text, time, message.message_id()); response->set_accepted(true); } if (message.has_message_id()) { response->set_message_id(message.message_id()); Data::Chat::Packet packet; packet.set_allocated_chat_acknowledge(response.take()); Channel::sendMessage(packet); } } void ChatChannel::handleChatAcknowledge(const Data::Chat::ChatAcknowledge &message) { if (direction() != Outbound) { qWarning() << "Rejected inbound acknowledgement on an inbound chat channel"; closeChannel(); return; } if (!message.has_message_id()) { qDebug() << "Chat acknowledgement doesn't have a message ID we understand"; closeChannel(); return; } MessageId id = message.message_id(); if (pendingMessages.remove(id)) { emit messageAcknowledged(id, message.accepted()); } else { qDebug() << "Received chat acknowledgement for unknown message" << id; } } ricochet-1.1.4/src/protocol/ChatChannel.h000066400000000000000000000055011300720305500202620ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef PROTOCOL_CHATCHANNEL_H #define PROTOCOL_CHATCHANNEL_H #include "Channel.h" #include "ChatChannel.pb.h" #include #include namespace Protocol { class ChatChannel : public Channel { Q_OBJECT Q_DISABLE_COPY(ChatChannel) public: typedef quint32 MessageId; static const int MessageMaxCharacters = 2000; explicit ChatChannel(Direction direction, Connection *connection); bool sendChatMessage(QString text, QDateTime time, MessageId &id); bool sendChatMessageWithId(QString text, QDateTime time, MessageId id); signals: void messageAcknowledged(MessageId id, bool accepted); void messageReceived(const QString &text, const QDateTime &time, MessageId id); protected: virtual bool allowInboundChannelRequest(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result); virtual bool allowOutboundChannelRequest(Data::Control::OpenChannel *request); virtual void receivePacket(const QByteArray &packet); private: QSet pendingMessages; MessageId lastMessageId; void handleChatMessage(const Data::Chat::ChatMessage &message); void handleChatAcknowledge(const Data::Chat::ChatAcknowledge &message); }; } #endif ricochet-1.1.4/src/protocol/ChatChannel.proto000066400000000000000000000007701300720305500212010ustar00rootroot00000000000000package Protocol.Data.Chat; message Packet { optional ChatMessage chat_message = 1; optional ChatAcknowledge chat_acknowledge = 2; } message ChatMessage { required string message_text = 1; optional uint32 message_id = 2; // Random ID for ack optional int64 time_delta = 3; // Delta in seconds between now and when message was written } message ChatAcknowledge { optional uint32 message_id = 1; optional bool accepted = 2 [default = true]; } ricochet-1.1.4/src/protocol/Connection.cpp000066400000000000000000000544771300720305500205640ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "Connection_p.h" #include "ControlChannel.h" #include "utils/Useful.h" #include #include #include #include using namespace Protocol; Connection::Connection(QTcpSocket *socket, Direction direction) : QObject() , d(new ConnectionPrivate(this)) { d->setSocket(socket, direction); } ConnectionPrivate::ConnectionPrivate(Connection *qq) : QObject(qq) , q(qq) , socket(0) , direction(Connection::ClientSide) , purpose(Connection::Purpose::Unknown) , wasClosed(false) , handshakeDone(false) , nextOutboundChannelId(-1) { ageTimer.start(); QTimer *timeout = new QTimer(this); timeout->setSingleShot(true); timeout->setInterval(UnknownPurposeTimeout * 1000); connect(timeout, &QTimer::timeout, this, [this,timeout]() { if (purpose == Connection::Purpose::Unknown) { qDebug() << "Closing connection" << q << "with unknown purpose after timeout"; q->close(); } timeout->deleteLater(); } ); timeout->start(); } Connection::~Connection() { qDebug() << this << "Destroying connection"; // When we call closeImmediately, the list of channels will be cleared. // In the normal case, they will all use deleteLater to be freed at the // next event loop. Since the connection is being destructed immediately, // and we want to be certain that channels don't outlive it, copy the // list before it's cleared and delete them immediately afterwards. auto channels = d->channels; d->closeImmediately(); // These would be deleted by QObject ownership as well, but we want to // give them a chance to destruct before the connection d pointer is reset. foreach (Channel *c, channels) delete c; // Reset d pointer, so we'll crash nicely if anything tries to call // into Connection after this. d = 0; } ConnectionPrivate::~ConnectionPrivate() { // Reset q pointer, for the same reason as above q = 0; } Connection::Direction Connection::direction() const { return d->direction; } bool Connection::isConnected() const { bool re = d->socket && d->socket->state() == QAbstractSocket::ConnectedState; if (d->wasClosed) { Q_ASSERT(!re); } return re; } QString Connection::serverHostname() const { QString hostname; if (direction() == ClientSide) hostname = d->socket->peerName(); else if (direction() == ServerSide) hostname = d->socket->property("localHostname").toString(); if (!hostname.endsWith(QStringLiteral(".onion"))) { BUG() << "Connection does not have a valid server hostname:" << hostname; return QString(); } return hostname; } int Connection::age() const { return qRound(d->ageTimer.elapsed() / 1000.0); } void ConnectionPrivate::setSocket(QTcpSocket *s, Connection::Direction d) { if (socket) { BUG() << "Connection already has a socket"; return; } socket = s; direction = d; connect(socket, &QAbstractSocket::disconnected, this, &ConnectionPrivate::socketDisconnected); connect(socket, &QIODevice::readyRead, this, &ConnectionPrivate::socketReadable); socket->setParent(q); if (socket->state() != QAbstractSocket::ConnectedState) { BUG() << "Connection created with socket in a non-connected state" << socket->state(); } Channel *control = new ControlChannel(direction == Connection::ClientSide ? Channel::Outbound : Channel::Inbound, q); // Closing the control channel must also close the connection connect(control, &Channel::invalidated, q, &Connection::close); insertChannel(control); if (!control->isOpened() || control->identifier() != 0 || q->channel(0) != control) { BUG() << "Control channel on new connection is not set up properly"; q->close(); return; } if (direction == Connection::ClientSide) { // The server side is implicitly authenticated (by the transport) as the correct service, so grant that QString serverName = q->serverHostname(); if (serverName.isEmpty()) { BUG() << "Server side of connection doesn't have an authenticated name, aborting"; q->close(); return; } q->grantAuthentication(Connection::HiddenServiceAuth, serverName); // Send the introduction version handshake message char intro[] = { 0x49, 0x4D, 0x02, ProtocolVersion, 0 }; if (socket->write(intro, sizeof(intro)) < (int)sizeof(intro)) { qDebug() << "Failed writing introduction message to socket"; q->close(); return; } } } void Connection::close() { if (isConnected()) { Q_ASSERT(!d->wasClosed); qDebug() << "Disconnecting socket for connection" << this; d->socket->disconnectFromHost(); // If not fully closed in 5 seconds, abort QTimer *timeout = new QTimer(this); timeout->setSingleShot(true); connect(timeout, &QTimer::timeout, d, &ConnectionPrivate::closeImmediately); timeout->start(5000); } } void ConnectionPrivate::closeImmediately() { if (socket) socket->abort(); if (!wasClosed) { BUG() << "Socket was forcefully closed but never emitted closed signal"; wasClosed = true; emit q->closed(); } if (!channels.isEmpty()) { foreach (Channel *c, channels) qDebug() << "Open channel:" << c << c->type() << c->connection(); BUG() << "Channels remain open after forcefully closing connection socket"; } } void ConnectionPrivate::socketDisconnected() { qDebug() << "Connection" << this << "disconnected"; closeAllChannels(); if (!wasClosed) { wasClosed = true; emit q->closed(); } } void ConnectionPrivate::socketReadable() { if (!handshakeDone) { qint64 available = socket->bytesAvailable(); if (direction == Connection::ClientSide && available >= 1) { // Expecting a single byte in response with the chosen version uchar version = ProtocolVersionFailed; if (socket->read(reinterpret_cast(&version), 1) < 1) { qDebug() << "Connection socket error" << socket->error() << "during read:" << socket->errorString(); socket->abort(); return; } handshakeDone = true; if (version == 0) { qDebug() << "Server in outbound connection is using the version 1.0 protocol"; emit q->oldVersionNegotiated(socket); q->close(); return; } else if (version != ProtocolVersion) { qDebug() << "Version negotiation failed on outbound connection"; emit q->versionNegotiationFailed(); socket->abort(); return; } else emit q->ready(); } else if (direction == Connection::ServerSide && available >= 3) { // Expecting at least 3 bytes uchar intro[3] = { 0 }; qint64 re = socket->peek(reinterpret_cast(intro), sizeof(intro)); if (re < (int)sizeof(intro)) { qDebug() << "Connection socket error" << socket->error() << "during read:" << socket->errorString(); socket->abort(); return; } quint8 nVersions = intro[2]; if (intro[0] != 0x49 || intro[1] != 0x4D || nVersions == 0) { qDebug() << "Invalid introduction sequence on inbound connection"; socket->abort(); return; } if (available < (qint64)sizeof(intro) + nVersions) return; // Discard intro header re = socket->read(reinterpret_cast(intro), sizeof(intro)); QByteArray versions(nVersions, 0); re = socket->read(versions.data(), versions.size()); if (re != versions.size()) { qDebug() << "Connection socket error" << socket->error() << "during read:" << socket->errorString(); socket->abort(); return; } quint8 selectedVersion = ProtocolVersionFailed; foreach (quint8 v, versions) { if (v == ProtocolVersion) { selectedVersion = v; break; } } re = socket->write(reinterpret_cast(&selectedVersion), 1); if (re != 1) { qDebug() << "Connection socket error" << socket->error() << "during write:" << socket->errorString(); socket->abort(); return; } handshakeDone = true; if (selectedVersion != ProtocolVersion) { qDebug() << "Version negotiation failed on inbound connection"; emit q->versionNegotiationFailed(); // Close gracefully to allow the response to write q->close(); return; } else emit q->ready(); } else { return; } } qint64 available; while ((available = socket->bytesAvailable()) >= PacketHeaderSize) { uchar header[PacketHeaderSize]; // Peek at the header first, to read the size of the packet and make sure // the entire thing is available within the buffer. qint64 re = socket->peek(reinterpret_cast(header), PacketHeaderSize); if (re < 0) { qDebug() << "Connection socket error" << socket->error() << "during read:" << socket->errorString(); socket->abort(); return; } else if (re < PacketHeaderSize) { BUG() << "Socket had" << available << "bytes available but peek only returned" << re; return; } Q_STATIC_ASSERT(PacketHeaderSize == 4); quint16 packetSize = qFromBigEndian(header); quint16 channelId = qFromBigEndian(&header[2]); if (packetSize < PacketHeaderSize) { qWarning() << "Corrupted data from connection (packet size is too small); disconnecting"; socket->abort(); return; } if (packetSize > available) break; // Read header out of the buffer and discard re = socket->read(reinterpret_cast(header), PacketHeaderSize); if (re != PacketHeaderSize) { if (re < 0) { qDebug() << "Connection socket error" << socket->error() << "during read:" << socket->errorString(); } else { // Because of QTcpSocket buffering, we can expect that up to 'available' bytes // will read. Treat anything less as an error condition. BUG() << "Socket read was unexpectedly small;" << available << "bytes should've been available but we read" << re; } socket->abort(); return; } // Read data QByteArray data(packetSize - PacketHeaderSize, 0); re = (data.size() == 0) ? 0 : socket->read(data.data(), data.size()); if (re != data.size()) { if (re < 0) { qDebug() << "Connection socket error" << socket->error() << "during read:" << socket->errorString(); } else { // As above BUG() << "Socket read was unexpectedly small;" << available << "bytes should've been available but we read" << re; } socket->abort(); return; } Channel *channel = q->channel(channelId); if (!channel) { // XXX We should sanity-check and rate limit these responses better if (data.isEmpty()) { qDebug() << "Ignoring channel close message for non-existent channel" << channelId; } else { qDebug() << "Ignoring" << data.size() << "byte packet for non-existent channel" << channelId; // Send channel close message writePacket(channelId, QByteArray()); } continue; } if (channel->connection() != q) { // If this fails, something is extremely broken. It may be dangerous to continue // processing any data at all. Crash gracefully. BUG() << "Channel" << channelId << "found on connection" << this << "but its connection is" << channel->connection(); qFatal("Connection mismatch while handling packet"); return; } if (data.isEmpty()) { channel->closeChannel(); } else { channel->receivePacket(data); } } } bool ConnectionPrivate::writePacket(Channel *channel, const QByteArray &data) { if (channel->connection() != q) { // As above, dangerously broken, crash the process to avoid damage BUG() << "Writing packet for channel" << channel->identifier() << "on connection" << this << "but its connection is" << channel->connection(); qFatal("Connection mismatch while writing packet"); return false; } return writePacket(channel->identifier(), data); } bool ConnectionPrivate::writePacket(int channelId, const QByteArray &data) { if (channelId < 0 || channelId > UINT16_MAX) { BUG() << "Cannot write packet for channel with invalid identifier" << channelId; return false; } if (data.size() > PacketMaxDataSize) { BUG() << "Cannot write oversized packet of" << data.size() << "bytes to channel" << channelId; return false; } if (!q->isConnected()) { qDebug() << "Cannot write packet to closed connection"; return false; } Q_STATIC_ASSERT(PacketHeaderSize + PacketMaxDataSize <= UINT16_MAX); Q_STATIC_ASSERT(PacketHeaderSize == 4); uchar header[PacketHeaderSize] = { 0 }; qToBigEndian(static_cast(PacketHeaderSize + data.size()), header); qToBigEndian(static_cast(channelId), &header[2]); qint64 re = socket->write(reinterpret_cast(header), PacketHeaderSize); if (re != PacketHeaderSize) { qDebug() << "Connection socket error" << socket->error() << "during write:" << socket->errorString(); socket->abort(); return false; } re = socket->write(data); if (re != data.size()) { qDebug() << "Connection socket error" << socket->error() << "during write:" << socket->errorString(); socket->abort(); return false; } return true; } int ConnectionPrivate::availableOutboundChannelId() { // Server opens even-nubmered channels, client opens odd-numbered bool evenNumbered = (direction == Connection::ServerSide); const int minId = evenNumbered ? 2 : 1; const int maxId = evenNumbered ? (UINT16_MAX-1) : UINT16_MAX; if (nextOutboundChannelId < minId || nextOutboundChannelId > maxId) nextOutboundChannelId = minId; // Find an unused id, trying a maximum of 100 times, using a random step to avoid collision for (int i = 0; i < 100 && channels.contains(nextOutboundChannelId); i++) { nextOutboundChannelId += 1 + (qrand() % 200); if (evenNumbered) nextOutboundChannelId += nextOutboundChannelId % 2; if (nextOutboundChannelId > maxId) nextOutboundChannelId = minId; } if (channels.contains(nextOutboundChannelId)) { // Abort the connection if we still couldn't find an id, because it's probably a nasty bug BUG() << "Can't find an available outbound channel ID for connection; aborting connection"; socket->abort(); return -1; } if (nextOutboundChannelId < minId || nextOutboundChannelId > maxId) { BUG() << "Selected a channel id that isn't within range"; return -1; } if (evenNumbered == bool(nextOutboundChannelId % 2)) { BUG() << "Selected a channel id that isn't valid for this side of the connection"; return -1; } int re = nextOutboundChannelId; nextOutboundChannelId += 2; return re; } bool ConnectionPrivate::isValidAvailableChannelId(int id, Connection::Direction side) { if (id < 1 || id > UINT16_MAX) return false; bool evenNumbered = bool(id % 2); if (evenNumbered == (side == Connection::ServerSide)) return false; if (channels.contains(id)) return false; return true; } bool ConnectionPrivate::insertChannel(Channel *channel) { if (channel->connection() != q) { BUG() << "Connection tried to insert a channel assigned to a different connection"; return false; } if (channel->identifier() < 0) { BUG() << "Connection tried to insert a channel without a valid identifier"; return false; } if (channels.contains(channel->identifier())) { BUG() << "Connection tried to insert a channel with a duplicate id" << channel->identifier() << "- we have" << channels.value(channel->identifier()) << "and inserted" << channel; return false; } if (channel->parent() != q) { BUG() << "Connection inserted a channel without expected parent object. Fixing."; channel->setParent(q); } channels.insert(channel->identifier(), channel); return true; } void ConnectionPrivate::removeChannel(Channel *channel) { if (channel->connection() != q) { BUG() << "Connection tried to remove a channel assigned to a different connection"; return; } // Out of caution, find the channel by pointer instead of identifier. This will make sure // it's always removed from the list, even if the identifier was somehow reset or lost. for (auto it = channels.begin(); it != channels.end(); ) { if (*it == channel) it = channels.erase(it); else it++; } } void ConnectionPrivate::closeAllChannels() { // Takes a copy, won't be broken by removeChannel calls foreach (Channel *channel, channels) channel->closeChannel(); if (!channels.isEmpty()) BUG() << "Channels remain open on connection after calling closeAllChannels"; } QHash Connection::channels() { return d->channels; } Channel *Connection::channel(int identifier) { return d->channels.value(identifier); } Connection::Purpose Connection::purpose() const { return d->purpose; } bool Connection::setPurpose(Purpose value) { if (d->purpose == value) return true; switch (value) { case Purpose::Unknown: BUG() << "A connection can't reset to unknown purpose"; return false; case Purpose::KnownContact: if (!hasAuthenticated(HiddenServiceAuth)) { BUG() << "Connection purpose cannot be KnownContact without authenticating a service"; return false; } break; case Purpose::OutboundRequest: if (d->direction != ClientSide) { BUG() << "Connection purpose cannot be OutboundRequest on an inbound connection"; return false; } else if (d->purpose != Purpose::Unknown) { BUG() << "Connection purpose cannot change from" << int(d->purpose) << "to OutboundRequest"; return false; } break; case Purpose::InboundRequest: if (d->direction != ServerSide) { BUG() << "Connection purpose cannot be InboundRequest on an outbound connection"; return false; } else if (d->purpose != Purpose::Unknown) { BUG() << "Connection purpose cannot change from" << int(d->purpose) << "to InboundRequest"; return false; } break; default: BUG() << "Purpose type" << int(value) << "is not defined"; return false; } Purpose old = d->purpose; d->purpose = value; emit purposeChanged(d->purpose, old); return true; } bool Connection::hasAuthenticated(AuthenticationType type) const { return d->authentication.contains(type); } bool Connection::hasAuthenticatedAs(AuthenticationType type, const QString &identity) const { auto it = d->authentication.find(type); if (!identity.isEmpty() && it != d->authentication.end()) return *it == identity; return false; } QString Connection::authenticatedIdentity(AuthenticationType type) const { return d->authentication.value(type); } void Connection::grantAuthentication(AuthenticationType type, const QString &identity) { if (hasAuthenticated(type)) { BUG() << "Tried to redundantly grant" << type << "authentication to connection"; return; } qDebug() << "Granting" << type << "authentication as" << identity << "to connection"; d->authentication.insert(type, identity); emit authenticated(type, identity); } ricochet-1.1.4/src/protocol/Connection.h000066400000000000000000000217301300720305500202130ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef PROTOCOL_CONNECTION_H #define PROTOCOL_CONNECTION_H #include #include #include "Channel.h" class QTcpSocket; namespace Protocol { class ConnectionPrivate; /* Represents a protocol connection associated with a socket * * Connection is created to handle protocol communication over a socket. It * handles reading, writing, creating channels, and all protocol behavior. A * connection instance is created for a specific socket and cannot be changed. * When the socket is closed, the Connection closes all channels and cannot be * used again. * * All protocol behavior takes place by creating and using channels, represented * by subclasses of Channel. The channelCreated and channelOpened signals can be * used to attach to new channels. A new channel can be created by instantiating * it and calling its openChannel method. * * The socket and all channels are owned by the Connection instance. In * particular, channel instances will be deleted automatically after being * closed. Avoid storing pointers to channels, or use a safe pointer to do so. * * The channel's functionality is controlled by authentication grants and by its * assigned purpose. The purpose declares the current use of the channel (e.g. * for a known contact or an incoming contact request). Higher level classes * assign and change the connection's purpose. Connections with an Unknown * purpose are closed automatically after a short timeout. */ class Connection : public QObject { Q_OBJECT Q_DISABLE_COPY(Connection) friend class Channel; friend class ChannelPrivate; friend class ControlChannel; public: /* Direction of the underlying socket connection * * The protocol is peer-to-peer and doesn't differentiate between * server and client except for small behavior details. */ enum Direction { ClientSide, ServerSide }; /* Construct a connection handler for a socket * * This connection will take ownership of the socket, and * becomes invalid (but is not automatically deleted) once * the socket has disconnected. */ explicit Connection(QTcpSocket *socket, Direction direction); virtual ~Connection(); Direction direction() const; bool isConnected() const; /* Hostname of the server side of the connection * * For a ClientSide connection, this returns the hostname that * the socket has connected to. For a ServerSide connection, * the local hostname which accepted the socket is returned. * * In all cases, the returned hostname will end with ".onion" */ QString serverHostname() const; /* Age of the connection in seconds */ int age() const; /* Assigned purpose of this connection * * A purpose is assigned to the connection after the peer has * authenticated or otherwise indicated what the connection will * be used for. * * Purposes may be used to limit the features available on a * connection, change behavior, and impose restrictions. * * Connections with an unknown purpose are killed after a timeout. */ enum class Purpose { Unknown, KnownContact, OutboundRequest, InboundRequest }; Purpose purpose() const; bool setPurpose(Purpose purpose); QHash channels(); Channel *channel(int identifier); template T *findChannel(Channel::Direction direction = Channel::Invalid); template QList findChannels(Channel::Direction direction = Channel::Invalid); enum AuthenticationType { HiddenServiceAuth, KnownToPeer // For outbound connections, set when the peer indicates we are a known contact }; bool hasAuthenticated(AuthenticationType type) const; bool hasAuthenticatedAs(AuthenticationType type, const QString &identity) const; QString authenticatedIdentity(AuthenticationType type) const; void grantAuthentication(AuthenticationType type, const QString &identity = QString()); public slots: /* Close this connection and the underlying socket * * All pending data to write will be sent, and the socket will be * asynchronously closed. If data hasn't been written after 5 seconds, the * socket will timeout and close anyway. * * isConnected will return false immediately after this function is called. * The closed signal is emitted when the socket and all channels have closed. */ void close(); signals: /* Emitted when the socket is closed. All channels will be closed * automatically. It is not possible to re-use the same Connection instance, * or to reconnect the socket. */ void closed(); /* Emitted once, after version negotiation has finished and the connection * is ready to use. If negotiation fails, the versionNegotiationFailed * signal is emitted instead, and the socket is closed. */ void ready(); /* Emitted once when version negotiation has failed; meaning, there is no * protocol version that both peers will accept. The socket will be closed. */ void versionNegotiationFailed(); /* Hack to allow delivering an upgrade message to old clients * XXX: Remove this once enough time has passed for most clients to be upgraded. */ void oldVersionNegotiated(QTcpSocket *socket); void authenticated(AuthenticationType type, const QString &identity); void purposeChanged(Purpose after, Purpose before); /* Emitted when a new Channel instance is created, before it has opened * * This signal can be used to attach to signals on a channel before it's * opened. This signal is emitted for both inbound and outbound channels, * before the request is approved. If a request is rejected or fails, the * channel may be deleted shortly afterwards, without emitting its * channelClosed signal. */ void channelCreated(Channel *channel); /* Emitted when an inbound channel needs approval to open * * This signal is emitted for channel types that require approval by * higher-layer functionality before opening, based on the information * in the OpenChannel message. Handlers should use channel-specific methods * to approve the inbound channel. * * This signal is only emitted for channels that specifically invoke the * Channel::requestInboundApproval() method. */ void channelRequestingInboundApproval(Channel *channel); /* Emitted when a channel is opened * * This signal is emitted after an inbound or outbound channel has been * opened. At this point, the channel can be used or closed normally. */ void channelOpened(Channel *channel); private: ConnectionPrivate *d; }; template T *Connection::findChannel(Channel::Direction direction) { T *re = 0; foreach (Channel *c, channels()) { if (direction != Channel::Invalid && c->direction() != direction) continue; if ((re = qobject_cast(c))) return re; } return 0; } template QList Connection::findChannels(Channel::Direction direction) { QList re; T *tmp = 0; foreach (Channel *c, channels()) { if (direction != Channel::Invalid && c->direction() != direction) continue; if ((tmp = qobject_cast(c))) re.append(tmp); } return re; } } #endif ricochet-1.1.4/src/protocol/Connection_p.h000066400000000000000000000062571300720305500205410ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef PROTOCOL_CONNECTION_P_H #define PROTOCOL_CONNECTION_P_H #include "Connection.h" #include #include #include namespace Protocol { class ConnectionPrivate : public QObject { Q_OBJECT Q_DISABLE_COPY(ConnectionPrivate) public: static const quint8 ProtocolVersion = 1; static const quint8 ProtocolVersionFailed = 0xff; static const int PacketHeaderSize = 4; static const int PacketMaxDataSize = UINT16_MAX - PacketHeaderSize; // Time in seconds before a connection with a purpose of Unknown is killed static const int UnknownPurposeTimeout = 15; explicit ConnectionPrivate(Connection *q); virtual ~ConnectionPrivate(); Connection *q; QTcpSocket *socket; QHash channels; QMap authentication; QElapsedTimer ageTimer; Connection::Direction direction; Connection::Purpose purpose; bool wasClosed; bool handshakeDone; void setSocket(QTcpSocket *socket, Connection::Direction direction); int availableOutboundChannelId(); bool isValidAvailableChannelId(int channelId, Connection::Direction idDirection); bool insertChannel(Channel *channel); void removeChannel(Channel *channel); void closeAllChannels(); bool writePacket(Channel *channel, const QByteArray &data); bool writePacket(int channelId, const QByteArray &data); public slots: void closeImmediately(); private slots: void socketReadable(); void socketDisconnected(); private: int nextOutboundChannelId; }; } #endif ricochet-1.1.4/src/protocol/ContactRequestChannel.cpp000066400000000000000000000250271300720305500227070ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "ContactRequestChannel.h" #include "Channel_p.h" using namespace Protocol; /* Regarding message and nickname limitations: * * For messages, we should use limits the same as those of chat, including limits on the * length and content. * * For nicknames, we should be quite restrictive, and these should be applied consistently * with protocol and UI. It should be similar to the restrictions on filenames, particularly * by excluding control characters. Length limit is short. * * If the nickname duplicates an existing contact, it must be changed for the request, but * the peer must not be aware of this in any way. */ ContactRequestChannel::ContactRequestChannel(Direction direction, Connection *connection) : Channel(QStringLiteral("im.ricochet.contact.request"), direction, connection) , m_responseStatus(Data::ContactRequest::Response::Undefined) { } QString ContactRequestChannel::message() const { return m_message; } void ContactRequestChannel::setMessage(const QString &message) { if (direction() != Outbound) { BUG() << "Request messages can only be set on outbound messages"; return; } // Only valid before channel opened if (isOpened() || identifier() >= 0) { BUG() << "Request data must be set before opening channel"; return; } if (message.size() > Data::ContactRequest::MessageMaxCharacters) { BUG() << "Outbound contact request message is too long (" << message.size() << ")"; return; } m_message = message; } static bool isAcceptableNickname(const QString &input) { if (input.size() > Data::ContactRequest::NicknameMaxCharacters) return false; /* Although nicknames should always be escaped before being displayed * in a HTML-sensitive context, there's little value in allowing these * characters in nicknames, and it could prevent future bugs. */ QVector blacklist = QStringLiteral("\"<>&").toUcs4(); QVector chars = input.toUcs4(); foreach (uint value, chars) { QChar c(value); if (c.category() == QChar::Other_Format || c.category() == QChar::Other_Control || c.isNonCharacter() || blacklist.contains(value)) return false; } return true; } QString ContactRequestChannel::nickname() const { return m_nickname; } void ContactRequestChannel::setNickname(const QString &nickname) { if (direction() != Outbound) { BUG() << "Request messages can only be set on outbound messages"; return; } if (isOpened() || identifier() >= 0) { BUG() << "Request data must be set before opening channel"; return; } if (!isAcceptableNickname(nickname)) { BUG() << "Outbound contact request nickname isn't acceptable:" << nickname; return; } m_nickname = nickname; } bool ContactRequestChannel::allowInboundChannelRequest(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result) { using namespace Data::ContactRequest; using namespace Data::Control; // If this connection is already KnownContact, report that the request is accepted if (connection()->purpose() == Connection::Purpose::KnownContact) { QScopedPointer response(new Response); response->set_status(Response::Accepted); result->SetAllocatedExtension(Data::ContactRequest::response, response.take()); return false; } // We'll only accept requests on inbound connections with an unknown purpose if (connection()->direction() != Connection::ServerSide || connection()->purpose() != Connection::Purpose::Unknown) { result->set_common_error(ChannelResult::BadUsageError); return false; } // Only allow one ContactRequestChannel if (connection()->findChannel()) { result->set_common_error(ChannelResult::BadUsageError); return false; } // Require HiddenServiceAuth if (!connection()->hasAuthenticated(Connection::HiddenServiceAuth)) { result->set_common_error(ChannelResult::UnauthorizedError); return false; } if (!request->HasExtension(Data::ContactRequest::contact_request)) { result->set_common_error(ChannelResult::BadUsageError); return false; } ContactRequest contactData = request->GetExtension(Data::ContactRequest::contact_request); QString nickname = QString::fromStdString(contactData.nickname()); QString message = QString::fromStdString(contactData.message_text()); m_responseStatus = Response::Undefined; if (message.size() > Data::ContactRequest::MessageMaxCharacters || !isAcceptableNickname(nickname)) { qWarning() << "Rejecting incoming contact request with invalid nickname/message"; setResponseStatus(Response::Error); } else { m_nickname = nickname; m_message = message; emit requestReceived(); if (m_responseStatus == Response::Undefined) { BUG() << "No response to incoming contact request after requestReceived signal"; setResponseStatus(Response::Error); } } QScopedPointer response(new Response); response->set_status(m_responseStatus); result->SetAllocatedExtension(Data::ContactRequest::response, response.take()); // If the response is final, close the channel immediately once it's fully open if (m_responseStatus > Response::Pending) connect(this, &Channel::channelOpened, this, &Channel::closeChannel, Qt::QueuedConnection); return true; } void ContactRequestChannel::setResponseStatus(Status status) { if (m_responseStatus == status) return; if (direction() != Inbound) { BUG() << "Can't set the response on an outbound contact request"; return; } using namespace Data::ContactRequest; if (m_responseStatus > Response::Pending) BUG() << "Response status is already a final state" << m_responseStatus << "but was changed to" << status; m_responseStatus = status; // If the channel is already open, the response is sent as a separate packet if (isOpened()) { Response response; response.set_status(m_responseStatus); sendMessage(response); if (m_responseStatus > Response::Pending) closeChannel(); } } bool ContactRequestChannel::allowOutboundChannelRequest(Data::Control::OpenChannel *request) { if (connection()->direction() != Connection::ClientSide || connection()->purpose() != Connection::Purpose::OutboundRequest) { BUG() << "ContactRequestChannel can only be used on OutboundRequest connections. Has purpose" << int(connection()->purpose()); return false; } if (connection()->findChannel()) { BUG() << "ContactRequestChannel can only be used once per connection"; return false; } QScopedPointer contactData(new Data::ContactRequest::ContactRequest); if (!m_nickname.isEmpty()) contactData->set_nickname(m_nickname.toStdString()); if (!m_message.isEmpty()) contactData->set_message_text(m_message.toStdString()); request->SetAllocatedExtension(Data::ContactRequest::contact_request, contactData.take()); return true; } bool ContactRequestChannel::processChannelOpenResult(const Data::Control::ChannelResult *result) { if (!result->HasExtension(Data::ContactRequest::response)) { qDebug() << "Expected a response for the contact request"; return false; } Data::ContactRequest::Response response = result->GetExtension(Data::ContactRequest::response); return handleResponse(&response); } void ContactRequestChannel::receivePacket(const QByteArray &packet) { Data::ContactRequest::Response response; if (!response.ParseFromArray(packet.constData(), packet.size())) { qDebug() << "Invalid message received on contact request channel"; closeChannel(); return; } if (!handleResponse(&response)) closeChannel(); } bool ContactRequestChannel::handleResponse(const Data::ContactRequest::Response *response) { using namespace Data::ContactRequest; if (response->status() == Response::Undefined) { qDebug() << "Got an invalid response (undefined status) to a contact request"; return false; } if (m_responseStatus > Response::Pending) { qDebug() << "Received a response" << response->status() << "to a contact request which already had a final response" << m_responseStatus; return false; } m_responseStatus = response->status(); emit requestStatusChanged(m_responseStatus); // If the response is final, close the channel. Use a queued invoke to avoid any potential // issue when called from processChannelOpenResult if (m_responseStatus > Response::Pending) metaObject()->invokeMethod(this, "closeChannel", Qt::QueuedConnection); return true; } ricochet-1.1.4/src/protocol/ContactRequestChannel.h000066400000000000000000000061131300720305500223470ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef PROTOCOL_CONTACTREQUESTCHANNEL_H #define PROTOCOL_CONTACTREQUESTCHANNEL_H #include "Channel.h" #include "ContactRequestChannel.pb.h" namespace Protocol { class ContactRequestChannel : public Channel { Q_OBJECT Q_DISABLE_COPY(ContactRequestChannel) public: typedef Data::ContactRequest::Response::Status Status; explicit ContactRequestChannel(Direction direction, Connection *connection); QString message() const; QString nickname() const; // Outbound void setMessage(const QString &message); void setNickname(const QString &nickname); // Inbound void setResponseStatus(Status status); signals: /* Emitted during the inbound channel request handler, when a new request * arrives. A handler is expected to synchronously call setResponseStatus * if it claims this request; otherwise, it will be closed. */ void requestReceived(); void requestStatusChanged(Status status); protected: virtual bool allowInboundChannelRequest(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result); virtual bool allowOutboundChannelRequest(Data::Control::OpenChannel *request); virtual bool processChannelOpenResult(const Data::Control::ChannelResult *result); virtual void receivePacket(const QByteArray &packet); private: QString m_nickname; QString m_message; Status m_responseStatus; bool handleResponse(const Data::ContactRequest::Response *response); }; } #endif ricochet-1.1.4/src/protocol/ContactRequestChannel.proto000066400000000000000000000013241300720305500232620ustar00rootroot00000000000000package Protocol.Data.ContactRequest; import "ControlChannel.proto"; enum Limits { MessageMaxCharacters = 2000; NicknameMaxCharacters = 30; } extend Control.OpenChannel { optional ContactRequest contact_request = 200; } extend Control.ChannelResult { optional Response response = 201; } // Sent only as an attachment to OpenChannel message ContactRequest { optional string nickname = 1; optional string message_text = 2; } // Response is the only valid message to send on the channel message Response { enum Status { Undefined = 0; // Not valid on the wire Pending = 1; Accepted = 2; Rejected = 3; Error = 4; } required Status status = 1; } ricochet-1.1.4/src/protocol/ControlChannel.cpp000066400000000000000000000255331300720305500213650ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "ControlChannel.h" #include "Channel_p.h" #include "Connection_p.h" #include "utils/Useful.h" #include #include using namespace Protocol; ControlChannel::ControlChannel(Direction direction, Connection *connection) : Channel(QStringLiteral("control"), direction, connection) { if (connection->channel(0)) BUG() << "Created ControlChannel for connection which already has a channel 0"; Q_D(Channel); d->isOpened = true; d->identifier = 0; } bool ControlChannel::sendOpenChannel(Channel *channel) { if (channel->isOpened() || channel->direction() != Outbound || channel->identifier() >= 0) { BUG() << "openChannel called for a" << channel->type() << "channel in an unexpected state"; return false; } if (channel->connection() != connection()) { BUG() << "openChannel called for" << channel->type() << "channel on a different connection"; return false; } QScopedPointer request(new Data::Control::OpenChannel); int channelId = connection()->d->availableOutboundChannelId(); if (channelId <= 0) return false; request->set_channel_identifier(channelId); if (!channel->d_ptr->openChannelOutbound(request.data())) { qDebug() << "Outbound OpenChannel request of type" << channel->type() << "refused locally"; return false; } if (!request->has_channel_type() || !request->has_channel_identifier() || request->channel_identifier() < 0 || request->channel_identifier() > UINT16_MAX) { BUG() << "Outbound OpenChannel request isn't valid:" << QString::fromStdString(request->DebugString()); return false; } if (request->channel_identifier() != channel->identifier()) { BUG() << "Channel identifier doesn't match in OpenChannel request of type" << channel->type(); return false; } if (!connection()->d->insertChannel(channel)) { BUG() << "Valid channel refused by connection"; return false; } Data::Control::Packet packet; packet.set_allocated_open_channel(request.take()); return sendMessage(packet); } void ControlChannel::keepAlive() { Data::Control::KeepAlive *request = new Data::Control::KeepAlive; request->set_response_requested(true); Data::Control::Packet packet; packet.set_allocated_keep_alive(request); sendMessage(packet); } bool ControlChannel::allowInboundChannelRequest(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result) { Q_UNUSED(request); Q_UNUSED(result); BUG() << "ControlChannel should never receive channel requests"; return false; } bool ControlChannel::allowOutboundChannelRequest(Data::Control::OpenChannel *request) { Q_UNUSED(request); BUG() << "ControlChannel should never send channel requests"; return false; } bool ControlChannel::processChannelOpenResult(const Data::Control::ChannelResult *result) { Q_UNUSED(result); BUG() << "ControlChannel should never receive a channel request response"; return false; } void ControlChannel::receivePacket(const QByteArray &packet) { Data::Control::Packet message; if (!message.ParseFromArray(packet.constData(), packet.size())) { qWarning() << "Control channel failed parsing packet; connection will be killed"; closeChannel(); return; } if (message.has_open_channel()) { handleOpenChannel(message.open_channel()); } else if (message.has_channel_result()) { handleChannelResult(message.channel_result()); } else if (message.has_keep_alive()) { handleKeepAlive(message.keep_alive()); } else if (message.has_enable_features()) { handleEnableFeatures(message.enable_features()); } else if (message.has_features_enabled()) { handleFeaturesEnabled(message.features_enabled()); } else { qWarning() << "Unrecognized message on control channel; connection will be killed"; closeChannel(); return; } } void ControlChannel::handleOpenChannel(const Data::Control::OpenChannel &message) { // Validate channel_identifier int id = message.channel_identifier(); Connection::Direction peerSide = (connection()->direction() == Connection::ClientSide) ? Connection::ServerSide : Connection::ClientSide; if (!connection()->d->isValidAvailableChannelId(id, peerSide)) { qWarning() << "Received OpenChannel with invalid channel_identifier:" << QString::fromStdString(message.DebugString()); // Deliberately invalid behavior; kill the connection closeChannel(); return; } Data::Control::ChannelResult *response = new Data::Control::ChannelResult; response->set_channel_identifier(id); Channel *channel = Channel::create(QString::fromStdString(message.channel_type()), Inbound, connection()); if (!channel) { qDebug() << "Received OpenChannel for unknown channel type:" << QString::fromStdString(message.channel_type()); response->set_opened(false); response->set_common_error(Data::Control::ChannelResult::UnknownTypeError); } else { if (!channel->d_ptr->openChannelInbound(&message, response)) { if (response->opened()) BUG() << "openChannelInbound handler failed but response said successful. Assuming failure."; response->set_opened(false); } if (!response->has_opened()) { BUG() << "inboundOpenChannel handler for" << channel->type() << "did not update response message"; response->set_opened(false); response->set_common_error(Data::Control::ChannelResult::GenericError); } } if (response->opened()) { if (!channel || !channel->isOpened() || channel->direction() != Inbound || channel->identifier() != id) { BUG() << "Channel" << channel->type() << "in unexpected state after inbound open"; response->set_opened(false); // The channel may think it's open, so force it to close channel->closeChannel(); } else if (!connection()->d->insertChannel(channel)) { Q_ASSERT_X(false, "handleOpenChannel", "Valid channel refused by connection"); qWarning() << "BUG: Valid channel refused by connection"; response->set_opened(false); channel->closeChannel(); } } if (!response->opened()) { qDebug() << "Rejected OpenChannel request:" << QString::fromStdString(message.DebugString()) << "response:" << QString::fromStdString(response->DebugString()); // Clean up channel instance delete channel; channel = 0; } Data::Control::Packet responseMessage; responseMessage.set_allocated_channel_result(response); sendMessage(responseMessage); if (response->opened()) emit connection()->channelOpened(channel); } void ControlChannel::handleChannelResult(const Data::Control::ChannelResult &message) { int id = message.channel_identifier(); Channel *channel = connection()->channel(id); if (!channel) { qWarning() << "Received ChannelResult for unknown identifier, ignoring:" << QString::fromStdString(message.DebugString()); return; } if (channel->direction() != Outbound || channel->isOpened()) { qWarning() << "Received (duplicate?) ChannelResult for existing channel in an unexpected state:" << QString::fromStdString(message.DebugString()); return; } bool opened = channel->d_ptr->openChannelResult(&message); if (opened && !channel->isOpened()) { BUG() << "Outbound channel isn't open after successful ChannelResult"; channel->closeChannel(); } else if (!opened && channel->isOpened()) { BUG() << "Outbound channel is open after failed ChannelResult"; channel->closeChannel(); } // Channel::outboundOpenResult will invalidate on failure, causing the // instance to be deleted once it's safe to do so if (!opened || !channel->isOpened()) { if (connection()->channel(channel->identifier())) { BUG() << "Channel not invalidated after failed outbound OpenChannel request"; channel->closeChannel(); } } else { emit connection()->channelOpened(channel); } } void ControlChannel::handleKeepAlive(const Data::Control::KeepAlive &message) { if (message.response_requested()) { Data::Control::KeepAlive *pong = new Data::Control::KeepAlive; pong->set_response_requested(false); Data::Control::Packet response; response.set_allocated_keep_alive(pong); sendMessage(response); } else { emit keepAliveResponse(); } } void ControlChannel::handleEnableFeatures(const Data::Control::EnableFeatures &message) { Q_UNUSED(message); // This version does not support any features. Data::Control::Packet responseMessage; responseMessage.mutable_features_enabled(); sendMessage(responseMessage); } void ControlChannel::handleFeaturesEnabled(const Data::Control::FeaturesEnabled &message) { Q_UNUSED(message); // This version does not generate EnableFeatures messages, so receiving this is an error. qDebug() << "Unexpectedly received FeaturesEnabled message from peer, but we never send EnableFeatures"; closeChannel(); } ricochet-1.1.4/src/protocol/ControlChannel.h000066400000000000000000000054651300720305500210340ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef PROTOCOL_CONTROLCHANNEL_H #define PROTOCOL_CONTROLCHANNEL_H #include "Channel.h" #include "ControlChannel.pb.h" namespace Protocol { class ControlChannel : public Channel { Q_OBJECT Q_DISABLE_COPY(ControlChannel) friend class ConnectionPrivate; public: bool sendOpenChannel(Channel *channel); void keepAlive(); signals: void keepAliveResponse(); protected: explicit ControlChannel(Direction direction, Connection *connection); virtual bool allowInboundChannelRequest(const Data::Control::OpenChannel *request, Data::Control::ChannelResult *result); virtual bool allowOutboundChannelRequest(Data::Control::OpenChannel *request); virtual bool processChannelOpenResult(const Data::Control::ChannelResult *result); virtual void receivePacket(const QByteArray &packet); private: void handleOpenChannel(const Data::Control::OpenChannel &message); void handleChannelResult(const Data::Control::ChannelResult &message); void handleKeepAlive(const Data::Control::KeepAlive &message); void handleEnableFeatures(const Data::Control::EnableFeatures &message); void handleFeaturesEnabled(const Data::Control::FeaturesEnabled &message); }; } #endif ricochet-1.1.4/src/protocol/ControlChannel.proto000066400000000000000000000027131300720305500217410ustar00rootroot00000000000000package Protocol.Data.Control; message Packet { // Must contain exactly one field optional OpenChannel open_channel = 1; optional ChannelResult channel_result = 2; optional KeepAlive keep_alive = 3; optional EnableFeatures enable_features = 4; optional FeaturesEnabled features_enabled = 5; } message OpenChannel { required int32 channel_identifier = 1; // Arbitrary unique identifier for this channel instance required string channel_type = 2; // String identifying channel type; e.g. im.ricochet.chat // It is valid to extend the OpenChannel message to add fields specific // to the requested channel_type. extensions 100 to max; } message ChannelResult { required int32 channel_identifier = 1; // Matching the value from OpenChannel required bool opened = 2; // If the channel is now open enum CommonError { GenericError = 0; UnknownTypeError = 1; UnauthorizedError = 2; BadUsageError = 3; FailedError = 4; } optional CommonError common_error = 3; // As with OpenChannel, it is valid to extend this message with fields specific // to the channel type. extensions 100 to max; } message KeepAlive { required bool response_requested = 1; } message EnableFeatures { repeated string feature = 1; extensions 100 to max; } message FeaturesEnabled { repeated string feature = 1; extensions 100 to max; } ricochet-1.1.4/src/protocol/OutboundConnector.cpp000066400000000000000000000223041300720305500221170ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "OutboundConnector.h" #include "utils/Useful.h" #include "tor/TorSocket.h" #include "ControlChannel.h" #include "AuthHiddenServiceChannel.h" #include using namespace Protocol; namespace Protocol { class OutboundConnectorPrivate : public QObject { Q_OBJECT public: OutboundConnector *q; Tor::TorSocket *socket; QSharedPointer connection; QString hostname; quint16 port; OutboundConnector::Status status; CryptoKey authPrivateKey; QString errorMessage; QTimer errorRetryTimer; int errorRetryCount; OutboundConnectorPrivate(OutboundConnector *q) : QObject(q) , q(q) , socket(0) , port(0) , status(OutboundConnector::Inactive) , errorRetryCount(0) { connect(&errorRetryTimer, &QTimer::timeout, this, &OutboundConnectorPrivate::retryAfterError); } void setStatus(OutboundConnector::Status status); void setError(const QString &errorMessage); public slots: void onConnected(); void startAuthentication(); void abort(); void retryAfterError(); }; } OutboundConnector::OutboundConnector(QObject *parent) : QObject(parent), d(new OutboundConnectorPrivate(this)) { } OutboundConnector::~OutboundConnector() { } void OutboundConnector::setAuthPrivateKey(const CryptoKey &key) { if (!key.isLoaded() || !key.isPrivate()) { BUG() << "Cannot make outbound connection without a valid private key"; return; } d->authPrivateKey = key; } bool OutboundConnector::connectToHost(const QString &hostname, quint16 port) { if (port <= 0 || hostname.isEmpty()) { d->errorMessage = QStringLiteral("Invalid hostname or port"); d->setStatus(Error); return false; } if (d->status == Ready) { BUG() << "Reusing an OutboundConnector object"; d->errorMessage = QStringLiteral("Outbound connection handler was already used"); d->setStatus(Error); return false; } if (isActive() && hostname == d->hostname && port == d->port) return true; // There is no reason to be connecting to anything but onions for now, so add a safety net here if (!hostname.endsWith(QLatin1String(".onion"))) { d->errorMessage = QStringLiteral("Invalid (non-onion) hostname"); d->setStatus(Error); return false; } abort(); d->hostname = hostname; d->port = port; d->socket = new Tor::TorSocket(this); connect(d->socket, &Tor::TorSocket::connected, d, &OutboundConnectorPrivate::onConnected); d->setStatus(Connecting); d->socket->connectToHost(d->hostname, d->port); return true; } void OutboundConnector::abort() { d->abort(); d->hostname.clear(); d->port = 0; d->errorRetryCount = 0; d->errorRetryTimer.stop(); d->errorMessage.clear(); d->setStatus(Inactive); } void OutboundConnectorPrivate::abort() { if (connection) { connection->close(); connection.clear(); } if (socket) { socket->disconnect(this); delete socket; socket = 0; } } OutboundConnector::Status OutboundConnector::status() const { return d->status; } bool OutboundConnector::isActive() const { return d->status > Inactive && d->status < Ready; } QString OutboundConnector::errorMessage() const { return d->errorMessage; } QSharedPointer OutboundConnector::takeConnection() { QSharedPointer c(d->connection); if (status() != Ready || !c) { BUG() << "Cannot take connection when not in the Ready state"; return c; } Q_ASSERT(!d->socket); d->connection.clear(); d->setStatus(Inactive); return c; } void OutboundConnectorPrivate::setStatus(OutboundConnector::Status value) { if (status == value) return; bool wasActive = q->isActive(); status = value; emit q->statusChanged(); if (wasActive != q->isActive()) emit q->isActiveChanged(); } void OutboundConnectorPrivate::setError(const QString &message) { abort(); errorMessage = message; setStatus(OutboundConnector::Error); // XXX This is a bad solution, but it will hold until we can revisit the // reconnecting and connection error behavior as a whole. if (++errorRetryCount > 5) { qDebug() << "Outbound connection attempt has had five errors in a row, stopping attempts"; return; } errorRetryTimer.setSingleShot(true); errorRetryTimer.start(60 * 1000); qDebug() << "Retrying outbound connection attempt in 60 seconds after an error"; } void OutboundConnectorPrivate::retryAfterError() { if (status != OutboundConnector::Error) { qDebug() << "Error retry timer triggered, but not in an error state anymore. Ignoring."; return; } if (hostname.isEmpty() || port <= 0) { qDebug() << "Connection info cleared during error retry period, stopping OutboundConnector"; q->abort(); return; } q->connectToHost(hostname, port); } void OutboundConnectorPrivate::onConnected() { if (!socket || status != OutboundConnector::Connecting) { BUG() << "OutboundConnector connected in an unexpected state"; setError(QStringLiteral("Connected in an unexpected state")); return; } connection = QSharedPointer(new Connection(socket, Connection::ClientSide), &QObject::deleteLater); // Socket is now owned by connection Q_ASSERT(socket->parent() == connection); socket->setReconnectEnabled(false); socket = 0; connect(connection.data(), &Connection::ready, this, &OutboundConnectorPrivate::startAuthentication); // XXX Needs special treatment in UI (along with some other error types here) connect(connection.data(), &Connection::versionNegotiationFailed, this, [this]() { setError(QStringLiteral("Protocol version negotiation failed with peer")); } ); connect(connection.data(), &Connection::oldVersionNegotiated, q, &OutboundConnector::oldVersionNegotiated); setStatus(OutboundConnector::Initializing); } void OutboundConnectorPrivate::startAuthentication() { if (!connection || status != OutboundConnector::Initializing) { BUG() << "OutboundConnector startAuthentication in an unexpected state"; setError(QStringLiteral("Connected in an unexpected state")); return; } if (!authPrivateKey.isLoaded() || !authPrivateKey.isPrivate()) { qDebug() << "Skipping authentication for OutboundConnector without a private key"; setStatus(OutboundConnector::Ready); emit q->ready(); return; } // XXX Timeouts and errors and all of that AuthHiddenServiceChannel *authChannel = new AuthHiddenServiceChannel(Channel::Outbound, connection.data()); connect(authChannel, &AuthHiddenServiceChannel::authSuccessful, this, [this]() { setStatus(OutboundConnector::Ready); emit q->ready(); } ); connect(authChannel, &AuthHiddenServiceChannel::authFailed, this, [this]() { qDebug() << "Authentication failed for outbound connection to" << hostname; setError(QStringLiteral("Authentication failed")); } ); // Set the Authenticating state when we send the actual authentication message connect(authChannel, &Channel::channelOpened, this, [this]() { setStatus(OutboundConnector::Authenticating); } ); authChannel->setPrivateKey(authPrivateKey); if (!authChannel->openChannel()) { setError(QStringLiteral("Unable to open authentication channel")); } } #include "OutboundConnector.moc" ricochet-1.1.4/src/protocol/OutboundConnector.h000066400000000000000000000067721300720305500215770ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef PROTOCOL_OUTBOUNDCONNECTOR_H #define PROTOCOL_OUTBOUNDCONNECTOR_H #include #include #include "Connection.h" #include "utils/CryptoKey.h" namespace Protocol { class OutboundConnectorPrivate; /* Manages making and authenticating an outbound connection to peers * * OutboundConnector handles the process of establishing a connection * to a remote hidden service host (with appropriate timeout and retry * behavior) and authenticating itself. Once the connection is * established and authenticated, the ready() signal is emitted. */ class OutboundConnector : public QObject { Q_OBJECT Q_DISABLE_COPY(OutboundConnector) Q_ENUMS(Status) Q_PROPERTY(Status status READ status NOTIFY statusChanged) Q_PROPERTY(bool isActive READ isActive NOTIFY isActiveChanged) public: enum Status { Inactive, Connecting, Initializing, Authenticating, Ready, Error }; explicit OutboundConnector(QObject *parent); virtual ~OutboundConnector(); Status status() const; bool isActive() const; QString errorMessage() const; bool connectToHost(const QString &hostname, quint16 port); void setAuthPrivateKey(const CryptoKey &key); /* Take ownership of the Connection object when Ready * * This function is only valid in the Ready state. * OutboundConnector will release ownership of the connection * and reset to the inactive state. */ QSharedPointer takeConnection(); public slots: void abort(); signals: void ready(); void statusChanged(); void isActiveChanged(); /* Hack to allow sending an upgrade message to peers with old * software versions that don't have a good way to handle this * sort of situation. */ void oldVersionNegotiated(QTcpSocket *socket); private: OutboundConnectorPrivate *d; }; } #endif ricochet-1.1.4/src/ricochet.desktop000066400000000000000000000002731300720305500172740ustar00rootroot00000000000000[Desktop Entry] Encoding=UTF-8 Type=Application Name=Ricochet IM Categories=Network;InstantMessaging;Chat;FileTransfer;Qt Comment=Anonymous instant messaging Icon=ricochet Exec=ricochet ricochet-1.1.4/src/tor/000077500000000000000000000000001300720305500147035ustar00rootroot00000000000000ricochet-1.1.4/src/tor/AddOnionCommand.cpp000066400000000000000000000066471300720305500204160ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2016, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "AddOnionCommand.h" #include "tor/HiddenService.h" #include "utils/CryptoKey.h" #include "utils/StringUtil.h" using namespace Tor; AddOnionCommand::AddOnionCommand(HiddenService *service) : m_service(service) { Q_ASSERT(m_service); } bool AddOnionCommand::isSuccessful() const { return statusCode() == 250 && m_errorMessage.isEmpty(); } QByteArray AddOnionCommand::build() { QByteArray out("ADD_ONION"); if (m_service->privateKey().isLoaded()) { out += " RSA1024:"; out += m_service->privateKey().encodedPrivateKey(CryptoKey::DER).toBase64(); } else { out += " NEW:RSA1024"; } foreach (const HiddenService::Target &target, m_service->targets()) { out += " Port="; out += QByteArray::number(target.servicePort); out += ","; out += target.targetAddress.toString().toLatin1(); out += ":"; out += QByteArray::number(target.targetPort); } out.append("\r\n"); return out; } void AddOnionCommand::onReply(int statusCode, const QByteArray &data) { TorControlCommand::onReply(statusCode, data); if (statusCode != 250) { m_errorMessage = QString::fromLatin1(data); return; } const QByteArray keyPrefix("PrivateKey=RSA1024:"); if (data.startsWith(keyPrefix)) { QByteArray keyData(QByteArray::fromBase64(data.mid(keyPrefix.size()))); CryptoKey key; if (!key.loadFromData(keyData, CryptoKey::PrivateKey, CryptoKey::DER)) { m_errorMessage = QStringLiteral("Key decoding failed"); return; } m_service->setPrivateKey(key); } } void AddOnionCommand::onFinished(int statusCode) { TorControlCommand::onFinished(statusCode); if (isSuccessful()) emit succeeded(); else emit failed(statusCode); } ricochet-1.1.4/src/tor/AddOnionCommand.h000066400000000000000000000047041300720305500200530ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2016, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef ADDONIONCOMMAND_H #define ADDONIONCOMMAND_H #include "TorControlCommand.h" #include #include #include namespace Tor { class HiddenService; class AddOnionCommand : public TorControlCommand { Q_OBJECT Q_DISABLE_COPY(AddOnionCommand) Q_PROPERTY(QString errorMessage READ errorMessage CONSTANT) Q_PROPERTY(bool successful READ isSuccessful CONSTANT) public: AddOnionCommand(HiddenService *service); QByteArray build(); QString errorMessage() const { return m_errorMessage; } bool isSuccessful() const; signals: void succeeded(); void failed(int code); protected: HiddenService *m_service; QString m_errorMessage; virtual void onReply(int statusCode, const QByteArray &data); virtual void onFinished(int statusCode); }; } #endif // ADDONIONCOMMAND_H ricochet-1.1.4/src/tor/AuthenticateCommand.cpp000066400000000000000000000047451300720305500213360ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "AuthenticateCommand.h" using namespace Tor; AuthenticateCommand::AuthenticateCommand() { } QByteArray AuthenticateCommand::build(const QByteArray &data) { if (data.isNull()) return QByteArray("AUTHENTICATE\r\n"); return QByteArray("AUTHENTICATE ") + data.toHex() + "\r\n"; } void AuthenticateCommand::onReply(int statusCode, const QByteArray &data) { TorControlCommand::onReply(statusCode, data); m_statusMessage = QString::fromLatin1(data); } void AuthenticateCommand::onFinished(int statusCode) { if (statusCode == 515) { m_statusMessage = QStringLiteral("Authentication failed - incorrect password"); } else if (statusCode != 250) { if (m_statusMessage.isEmpty()) m_statusMessage = QStringLiteral("Authentication failed (error %1").arg(statusCode); } TorControlCommand::onFinished(statusCode); } ricochet-1.1.4/src/tor/AuthenticateCommand.h000066400000000000000000000043131300720305500207720ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef AUTHENTICATECOMMAND_H #define AUTHENTICATECOMMAND_H #include "TorControlCommand.h" namespace Tor { class AuthenticateCommand : public TorControlCommand { Q_OBJECT public: AuthenticateCommand(); QByteArray build(const QByteArray &data = QByteArray()); bool isSuccessful() const { return statusCode() == 250; } QString errorMessage() const { return m_statusMessage; } protected: virtual void onReply(int statusCode, const QByteArray &data); virtual void onFinished(int statusCode); private: QString m_statusMessage; }; } #endif // AUTHENTICATECOMMAND_H ricochet-1.1.4/src/tor/GetConfCommand.cpp000066400000000000000000000073351300720305500202430ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "GetConfCommand.h" #include "utils/StringUtil.h" #include using namespace Tor; GetConfCommand::GetConfCommand(Type t) : type(t) { } QByteArray GetConfCommand::build(const QByteArray &key) { return build(QList() << key); } QByteArray GetConfCommand::build(const QList &keys) { QByteArray out; if (type == GetConf) { out = "GETCONF"; } else if (type == GetInfo) { out = "GETINFO"; } else { Q_ASSERT(false); return out; } foreach (const QByteArray &key, keys) { out.append(' '); out.append(key); } out.append("\r\n"); return out; } void GetConfCommand::onReply(int statusCode, const QByteArray &data) { TorControlCommand::onReply(statusCode, data); if (statusCode != 250) return; int kep = data.indexOf('='); QString key = QString::fromLatin1(data.mid(0, kep)); QVariant value; if (kep >= 0) value = QString::fromLatin1(unquotedString(data.mid(kep + 1))); m_lastKey = key; QVariantMap::iterator it = m_results.find(key); if (it != m_results.end()) { // Make a list of values QVariantList results = it->toList(); if (results.isEmpty()) results.append(*it); results.append(value); *it = QVariant(results); } else { m_results.insert(key, value); } } void GetConfCommand::onDataLine(const QByteArray &data) { if (m_lastKey.isEmpty()) { qWarning() << "torctrl: Unexpected data line in GetConf command"; return; } QVariantMap::iterator it = m_results.find(m_lastKey); if (it != m_results.end()) { QVariantList results = it->toList(); if (results.isEmpty() && !it->toByteArray().isEmpty()) results.append(*it); results.append(data); *it = QVariant(results); } else { m_results.insert(m_lastKey, QVariantList() << data); } } void GetConfCommand::onDataFinished() { m_lastKey.clear(); } QVariant GetConfCommand::get(const QByteArray &key) const { return m_results.value(QString::fromLatin1(key)); } ricochet-1.1.4/src/tor/GetConfCommand.h000066400000000000000000000047451300720305500177120ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef GETCONFCOMMAND_H #define GETCONFCOMMAND_H #include "TorControlCommand.h" #include #include namespace Tor { class GetConfCommand : public TorControlCommand { Q_OBJECT Q_DISABLE_COPY(GetConfCommand) Q_PROPERTY(QVariantMap results READ results CONSTANT) public: enum Type { GetConf, GetInfo }; const Type type; GetConfCommand(Type type); QByteArray build(const QByteArray &key); QByteArray build(const QList &keys); const QVariantMap &results() const { return m_results; } QVariant get(const QByteArray &key) const; protected: virtual void onReply(int statusCode, const QByteArray &data); virtual void onDataLine(const QByteArray &data); virtual void onDataFinished(); private: QVariantMap m_results; QString m_lastKey; }; } #endif // GETCONFCOMMAND_H ricochet-1.1.4/src/tor/HiddenService.cpp000066400000000000000000000101311300720305500201170ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "HiddenService.h" #include "TorControl.h" #include "TorSocket.h" #include "utils/CryptoKey.h" #include "utils/Useful.h" #include #include #include #include using namespace Tor; HiddenService::HiddenService(QObject *parent) : QObject(parent), m_status(NotCreated) { } HiddenService::HiddenService(const QString &path, QObject *parent) : QObject(parent), m_dataPath(path), m_status(NotCreated) { /* Set the initial status and, if possible, load the hostname */ if (QDir(m_dataPath).exists(QLatin1String("private_key"))) { loadPrivateKey(); if (!m_hostname.isEmpty()) m_status = Offline; } } HiddenService::HiddenService(const CryptoKey &privateKey, const QString &path, QObject *parent) : QObject(parent), m_dataPath(path), m_status(NotCreated) { setPrivateKey(privateKey); m_status = Offline; } void HiddenService::setStatus(Status newStatus) { if (m_status == newStatus) return; Status old = m_status; m_status = newStatus; emit statusChanged(m_status, old); if (m_status == Online) emit serviceOnline(); } void HiddenService::addTarget(const Target &target) { m_targets.append(target); } void HiddenService::addTarget(quint16 servicePort, QHostAddress targetAddress, quint16 targetPort) { Target t = { targetAddress, servicePort, targetPort }; m_targets.append(t); } void HiddenService::setPrivateKey(const CryptoKey &key) { if (m_privateKey.isLoaded()) { BUG() << "Cannot change the private key on an existing HiddenService"; return; } if (!key.isPrivate()) { BUG() << "Cannot create a hidden service with a public key"; return; } m_privateKey = key; m_hostname = m_privateKey.torServiceID() + QStringLiteral(".onion"); emit privateKeyChanged(); } void HiddenService::loadPrivateKey() { if (m_privateKey.isLoaded() || m_dataPath.isEmpty()) return; bool ok = m_privateKey.loadFromFile(m_dataPath + QLatin1String("/private_key"), CryptoKey::PrivateKey); if (!ok) { qWarning() << "Failed to load hidden service key"; return; } m_hostname = m_privateKey.torServiceID(); emit privateKeyChanged(); } void HiddenService::servicePublished() { loadPrivateKey(); if (m_hostname.isEmpty()) { qDebug() << "Failed to read hidden service hostname"; return; } qDebug() << "Hidden service published successfully"; setStatus(Online); } ricochet-1.1.4/src/tor/HiddenService.h000066400000000000000000000064121300720305500175730ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef HIDDENSERVICE_H #define HIDDENSERVICE_H #include #include #include #include "utils/CryptoKey.h" namespace Tor { class TorSocket; class HiddenService : public QObject { Q_OBJECT Q_DISABLE_COPY(HiddenService) friend class TorControlPrivate; public: struct Target { QHostAddress targetAddress; quint16 servicePort, targetPort; }; enum Status { NotCreated = -1, /* Service has not been created yet */ Offline = 0, /* Data exists, but service is not published */ Online /* Published */ }; HiddenService(QObject *parent = 0); HiddenService(const QString &dataPath, QObject *parent = 0); HiddenService(const CryptoKey &privateKey, const QString &dataPath = QString(), QObject *parent = 0); Status status() const { return m_status; } const QString &hostname() const { return m_hostname; } const QString &dataPath() const { return m_dataPath; } CryptoKey privateKey() { return m_privateKey; } void setPrivateKey(const CryptoKey &privateKey); const QList &targets() const { return m_targets; } void addTarget(const Target &target); void addTarget(quint16 servicePort, QHostAddress targetAddress, quint16 targetPort); signals: void statusChanged(int newStatus, int oldStatus); void serviceOnline(); void privateKeyChanged(); private slots: void servicePublished(); private: QString m_dataPath; QList m_targets; QString m_hostname; Status m_status; CryptoKey m_privateKey; void loadPrivateKey(); void setStatus(Status newStatus); }; } #endif // HIDDENSERVICE_H ricochet-1.1.4/src/tor/ProtocolInfoCommand.cpp000066400000000000000000000062351300720305500213310ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "ProtocolInfoCommand.h" #include "TorControl.h" #include "utils/StringUtil.h" #include using namespace Tor; ProtocolInfoCommand::ProtocolInfoCommand(TorControl *m) : manager(m) { } QByteArray ProtocolInfoCommand::build() { return QByteArray("PROTOCOLINFO 1\r\n"); } void ProtocolInfoCommand::onReply(int statusCode, const QByteArray &data) { TorControlCommand::onReply(statusCode, data); if (statusCode != 250) return; if (data.startsWith("AUTH ")) { QList tokens = splitQuotedStrings(data.mid(5), ' '); foreach (QByteArray token, tokens) { if (token.startsWith("METHODS=")) { QList textMethods = unquotedString(token.mid(8)).split(','); for (QList::Iterator it = textMethods.begin(); it != textMethods.end(); ++it) { if (*it == "NULL") m_authMethods |= AuthNull; else if (*it == "HASHEDPASSWORD") m_authMethods |= AuthHashedPassword; else if (*it == "COOKIE") m_authMethods |= AuthCookie; } } else if (token.startsWith("COOKIEFILE=")) { m_cookieFile = QString::fromLatin1(unquotedString(token.mid(11))); } } } else if (data.startsWith("VERSION Tor=")) { m_torVersion = QString::fromLatin1(unquotedString(data.mid(12, data.indexOf(' ', 12)))); } } ricochet-1.1.4/src/tor/ProtocolInfoCommand.h000066400000000000000000000050251300720305500207720ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef PROTOCOLINFOCOMMAND_H #define PROTOCOLINFOCOMMAND_H #include "TorControlCommand.h" #include namespace Tor { class TorControl; class ProtocolInfoCommand : public TorControlCommand { Q_OBJECT Q_DISABLE_COPY(ProtocolInfoCommand) public: enum AuthMethod { AuthUnknown = 0, AuthNull = 0x1, AuthHashedPassword = 0x2, AuthCookie = 0x4 }; Q_DECLARE_FLAGS(AuthMethods, AuthMethod) ProtocolInfoCommand(TorControl *manager); QByteArray build(); AuthMethods authMethods() const { return m_authMethods; } QString torVersion() const { return m_torVersion; } QString cookieFile() const { return m_cookieFile; } protected: virtual void onReply(int statusCode, const QByteArray &data); private: TorControl *manager; AuthMethods m_authMethods; QString m_torVersion; QString m_cookieFile; }; } #endif // PROTOCOLINFOCOMMAND_H ricochet-1.1.4/src/tor/SetConfCommand.cpp000066400000000000000000000067431300720305500202610ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "SetConfCommand.h" #include "utils/StringUtil.h" using namespace Tor; SetConfCommand::SetConfCommand() : m_resetMode(false) { } void SetConfCommand::setResetMode(bool enabled) { m_resetMode = enabled; } bool SetConfCommand::isSuccessful() const { return statusCode() == 250; } QByteArray SetConfCommand::build(const QByteArray &key, const QByteArray &value) { return build(QList >() << qMakePair(key, value)); } QByteArray SetConfCommand::build(const QVariantMap &data) { QList > out; for (QVariantMap::ConstIterator it = data.begin(); it != data.end(); it++) { QByteArray key = it.key().toLatin1(); if (static_cast(it.value().type()) == QMetaType::QVariantList) { QVariantList values = it.value().value(); foreach (const QVariant &value, values) out.append(qMakePair(key, value.toString().toLatin1())); } else { out.append(qMakePair(key, it.value().toString().toLatin1())); } } return build(out); } QByteArray SetConfCommand::build(const QList > &data) { QByteArray out(m_resetMode ? "RESETCONF" : "SETCONF"); for (int i = 0; i < data.size(); i++) { out += " " + data[i].first; if (!data[i].second.isEmpty()) out += "=" + quotedString(data[i].second); } out.append("\r\n"); return out; } void SetConfCommand::onReply(int statusCode, const QByteArray &data) { TorControlCommand::onReply(statusCode, data); if (statusCode != 250) m_errorMessage = QString::fromLatin1(data); } void SetConfCommand::onFinished(int statusCode) { TorControlCommand::onFinished(statusCode); if (isSuccessful()) emit setConfSucceeded(); else emit setConfFailed(statusCode); } ricochet-1.1.4/src/tor/SetConfCommand.h000066400000000000000000000051451300720305500177210ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef SETCONFCOMMAND_H #define SETCONFCOMMAND_H #include "TorControlCommand.h" #include #include #include namespace Tor { class SetConfCommand : public TorControlCommand { Q_OBJECT Q_DISABLE_COPY(SetConfCommand) Q_PROPERTY(QString errorMessage READ errorMessage CONSTANT) Q_PROPERTY(bool successful READ isSuccessful CONSTANT) public: SetConfCommand(); void setResetMode(bool resetMode); QByteArray build(const QByteArray &key, const QByteArray &value); QByteArray build(const QVariantMap &data); QByteArray build(const QList > &data); QString errorMessage() const { return m_errorMessage; } bool isSuccessful() const; signals: void setConfSucceeded(); void setConfFailed(int code); protected: QString m_errorMessage; bool m_resetMode; virtual void onReply(int statusCode, const QByteArray &data); virtual void onFinished(int statusCode); }; } #endif // SETCONFCOMMAND_H ricochet-1.1.4/src/tor/TorControl.cpp000066400000000000000000000574771300720305500175400ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "TorControl.h" #include "TorControlSocket.h" #include "HiddenService.h" #include "ProtocolInfoCommand.h" #include "AuthenticateCommand.h" #include "SetConfCommand.h" #include "GetConfCommand.h" #include "AddOnionCommand.h" #include "utils/StringUtil.h" #include "utils/Settings.h" #include "utils/PendingOperation.h" #include #include #include #include #include #include #include #include Tor::TorControl *torControl = 0; using namespace Tor; namespace Tor { class TorControlPrivate : public QObject { Q_OBJECT public: TorControl *q; TorControlSocket *socket; QHostAddress torAddress; QString errorMessage; QString torVersion; QByteArray authPassword; QHostAddress socksAddress; QList services; quint16 controlPort, socksPort; TorControl::Status status; TorControl::TorStatus torStatus; QVariantMap bootstrapStatus; bool hasOwnership; TorControlPrivate(TorControl *parent); void setStatus(TorControl::Status status); void setTorStatus(TorControl::TorStatus status); void getTorInfo(); void publishServices(); public slots: void socketConnected(); void socketDisconnected(); void socketError(); void authenticateReply(); void protocolInfoReply(); void getTorInfoReply(); void setError(const QString &message); void statusEvent(int code, const QByteArray &data); void updateBootstrap(const QList &data); }; } TorControl::TorControl(QObject *parent) : QObject(parent), d(new TorControlPrivate(this)) { } TorControlPrivate::TorControlPrivate(TorControl *parent) : QObject(parent), q(parent), controlPort(0), socksPort(0), status(TorControl::NotConnected), torStatus(TorControl::TorUnknown), hasOwnership(false) { socket = new TorControlSocket(this); QObject::connect(socket, SIGNAL(connected()), this, SLOT(socketConnected())); QObject::connect(socket, SIGNAL(disconnected()), this, SLOT(socketDisconnected())); QObject::connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(socketError())); QObject::connect(socket, SIGNAL(error(QString)), this, SLOT(setError(QString))); } QNetworkProxy TorControl::connectionProxy() { return QNetworkProxy(QNetworkProxy::Socks5Proxy, d->socksAddress.toString(), d->socksPort); } void TorControlPrivate::setStatus(TorControl::Status n) { if (n == status) return; TorControl::Status old = status; status = n; if (old == TorControl::Error) errorMessage.clear(); emit q->statusChanged(status, old); if (status == TorControl::Connected && old < TorControl::Connected) emit q->connected(); else if (status < TorControl::Connected && old >= TorControl::Connected) emit q->disconnected(); } void TorControlPrivate::setTorStatus(TorControl::TorStatus n) { if (n == torStatus) return; TorControl::TorStatus old = torStatus; torStatus = n; emit q->torStatusChanged(torStatus, old); emit q->connectivityChanged(); if (torStatus == TorControl::TorReady && socksAddress.isNull()) { // Request info again to read the SOCKS port getTorInfo(); } } void TorControlPrivate::setError(const QString &message) { errorMessage = message; setStatus(TorControl::Error); qWarning() << "torctrl: Error:" << errorMessage; socket->abort(); QTimer::singleShot(15000, q, SLOT(reconnect())); } TorControl::Status TorControl::status() const { return d->status; } TorControl::TorStatus TorControl::torStatus() const { return d->torStatus; } QString TorControl::torVersion() const { return d->torVersion; } QString TorControl::errorMessage() const { return d->errorMessage; } bool TorControl::hasConnectivity() const { return torStatus() == TorReady && !d->socksAddress.isNull(); } QHostAddress TorControl::socksAddress() const { return d->socksAddress; } quint16 TorControl::socksPort() const { return d->socksPort; } QList TorControl::hiddenServices() const { return d->services; } QVariantMap TorControl::bootstrapStatus() const { return d->bootstrapStatus; } void TorControl::setAuthPassword(const QByteArray &password) { d->authPassword = password; } void TorControl::connect(const QHostAddress &address, quint16 port) { if (status() > Connecting) { qDebug() << "Ignoring TorControl::connect due to existing connection"; return; } d->torAddress = address; d->controlPort = port; d->setTorStatus(TorUnknown); bool b = d->socket->blockSignals(true); d->socket->abort(); d->socket->blockSignals(b); d->setStatus(Connecting); d->socket->connectToHost(address, port); } void TorControl::reconnect() { Q_ASSERT(!d->torAddress.isNull() && d->controlPort); if (d->torAddress.isNull() || !d->controlPort || status() >= Connecting) return; d->setStatus(Connecting); d->socket->connectToHost(d->torAddress, d->controlPort); } void TorControlPrivate::authenticateReply() { AuthenticateCommand *command = qobject_cast(sender()); Q_ASSERT(command); Q_ASSERT(status == TorControl::Authenticating); if (!command) return; if (!command->isSuccessful()) { setError(command->errorMessage()); return; } qDebug() << "torctrl: Authentication successful"; setStatus(TorControl::Connected); setTorStatus(TorControl::TorUnknown); TorControlCommand *clientEvents = new TorControlCommand; connect(clientEvents, &TorControlCommand::replyLine, this, &TorControlPrivate::statusEvent); socket->registerEvent("STATUS_CLIENT", clientEvents); getTorInfo(); publishServices(); // XXX Fix old configurations that would store unwanted options in torrc. // This can be removed some suitable amount of time after 1.0.4. if (hasOwnership) q->saveConfiguration(); } void TorControlPrivate::socketConnected() { Q_ASSERT(status == TorControl::Connecting); qDebug() << "torctrl: Connected socket; querying information"; setStatus(TorControl::Authenticating); ProtocolInfoCommand *command = new ProtocolInfoCommand(q); connect(command, &TorControlCommand::finished, this, &TorControlPrivate::protocolInfoReply); socket->sendCommand(command, command->build()); } void TorControlPrivate::socketDisconnected() { /* Clear some internal state */ torVersion.clear(); socksAddress.clear(); socksPort = 0; setTorStatus(TorControl::TorUnknown); /* This emits the disconnected() signal as well */ setStatus(TorControl::NotConnected); } void TorControlPrivate::socketError() { setError(QStringLiteral("Connection failed: %1").arg(socket->errorString())); } void TorControlPrivate::protocolInfoReply() { ProtocolInfoCommand *info = qobject_cast(sender()); if (!info) return; torVersion = info->torVersion(); if (status == TorControl::Authenticating) { AuthenticateCommand *auth = new AuthenticateCommand; connect(auth, &TorControlCommand::finished, this, &TorControlPrivate::authenticateReply); QByteArray data; ProtocolInfoCommand::AuthMethods methods = info->authMethods(); if (methods.testFlag(ProtocolInfoCommand::AuthNull)) { qDebug() << "torctrl: Using null authentication"; data = auth->build(); } else if (methods.testFlag(ProtocolInfoCommand::AuthCookie) && !info->cookieFile().isEmpty()) { QString cookieFile = info->cookieFile(); QString cookieError; qDebug() << "torctrl: Using cookie authentication with file" << cookieFile; QFile file(cookieFile); if (file.open(QIODevice::ReadOnly)) { QByteArray cookie = file.readAll(); file.close(); /* Simple test to avoid a vulnerability where any process listening on what we think is * the control port could trick us into sending the contents of an arbitrary file */ if (cookie.size() == 32) data = auth->build(cookie); else cookieError = QStringLiteral("Unexpected file size"); } else cookieError = file.errorString(); if (!cookieError.isNull() || data.isNull()) { /* If we know a password and password authentication is allowed, try using that instead. * This is a strange corner case that will likely never happen in a normal configuration, * but it has happened. */ if (methods.testFlag(ProtocolInfoCommand::AuthHashedPassword) && !authPassword.isEmpty()) { qDebug() << "torctrl: Unable to read authentication cookie file:" << cookieError; goto usePasswordAuth; } setError(QStringLiteral("Unable to read authentication cookie file: %1").arg(cookieError)); delete auth; return; } } else if (methods.testFlag(ProtocolInfoCommand::AuthHashedPassword) && !authPassword.isEmpty()) { usePasswordAuth: qDebug() << "torctrl: Using hashed password authentication"; data = auth->build(authPassword); } else { if (methods.testFlag(ProtocolInfoCommand::AuthHashedPassword)) setError(QStringLiteral("Tor requires a control password to connect, but no password is configured.")); else setError(QStringLiteral("Tor is not configured to accept any supported authentication methods.")); delete auth; return; } socket->sendCommand(auth, data); } } void TorControlPrivate::getTorInfo() { Q_ASSERT(q->isConnected()); GetConfCommand *command = new GetConfCommand(GetConfCommand::GetInfo); connect(command, &TorControlCommand::finished, this, &TorControlPrivate::getTorInfoReply); QList keys; keys << QByteArray("status/circuit-established") << QByteArray("status/bootstrap-phase"); /* If these are set in the config, they override the automatic behavior. */ SettingsObject settings(QStringLiteral("tor")); QHostAddress forceAddress(settings.read("socksAddress").toString()); quint16 port = (quint16)settings.read("socksPort").toInt(); if (!forceAddress.isNull() && port) { qDebug() << "torctrl: Using manually specified SOCKS connection settings"; socksAddress = forceAddress; socksPort = port; emit q->connectivityChanged(); } else keys << QByteArray("net/listeners/socks"); socket->sendCommand(command, command->build(keys)); } void TorControlPrivate::getTorInfoReply() { GetConfCommand *command = qobject_cast(sender()); if (!command || !q->isConnected()) return; QList listenAddresses = splitQuotedStrings(command->get(QByteArray("net/listeners/socks")).toString().toLatin1(), ' '); for (QList::Iterator it = listenAddresses.begin(); it != listenAddresses.end(); ++it) { QByteArray value = unquotedString(*it); int sepp = value.indexOf(':'); QHostAddress address(QString::fromLatin1(value.mid(0, sepp))); quint16 port = (quint16)value.mid(sepp+1).toUInt(); /* Use the first address that matches the one used for this control connection. If none do, * just use the first address and rely on the user to reconfigure if necessary (not a problem; * their setup is already very customized) */ if (socksAddress.isNull() || address == socket->peerAddress()) { socksAddress = address; socksPort = port; if (address == socket->peerAddress()) break; } } /* It is not immediately an error to have no SOCKS address; when DisableNetwork is set there won't be a * listener yet. To handle that situation, we'll try to read the socks address again when TorReady state * is reached. */ if (!socksAddress.isNull()) { qDebug().nospace() << "torctrl: SOCKS address is " << socksAddress.toString() << ":" << socksPort; emit q->connectivityChanged(); } if (command->get(QByteArray("status/circuit-established")).toInt() == 1) { qDebug() << "torctrl: Tor indicates that circuits have been established; state is TorReady"; setTorStatus(TorControl::TorReady); } else { setTorStatus(TorControl::TorOffline); } QByteArray bootstrap = command->get(QByteArray("status/bootstrap-phase")).toString().toLatin1(); if (!bootstrap.isEmpty()) updateBootstrap(splitQuotedStrings(bootstrap, ' ')); } void TorControl::addHiddenService(HiddenService *service) { if (d->services.contains(service)) return; d->services.append(service); } void TorControlPrivate::publishServices() { Q_ASSERT(q->isConnected()); if (services.isEmpty()) return; SettingsObject settings(QStringLiteral("tor")); if (settings.read("neverPublishServices").toBool()) { qDebug() << "torctrl: Skipping service publication because neverPublishService is enabled"; /* Call servicePublished under the assumption that they're published externally. */ for (QList::Iterator it = services.begin(); it != services.end(); ++it) (*it)->servicePublished(); return; } if (q->torVersionAsNewAs(QStringLiteral("0.2.7"))) { foreach (HiddenService *service, services) { if (service->hostname().isEmpty()) qDebug() << "torctrl: Creating a new hidden service"; else qDebug() << "torctrl: Publishing hidden service" << service->hostname(); AddOnionCommand *onionCommand = new AddOnionCommand(service); QObject::connect(onionCommand, &AddOnionCommand::succeeded, service, &HiddenService::servicePublished); socket->sendCommand(onionCommand, onionCommand->build()); } } else { qDebug() << "torctrl: Using legacy SETCONF hidden service configuration for tor" << torVersion; SetConfCommand *command = new SetConfCommand; QList > torConfig; foreach (HiddenService *service, services) { if (service->dataPath().isEmpty()) continue; if (service->privateKey().isLoaded() && !QFile::exists(service->dataPath() + QStringLiteral("/private_key"))) { // This case can happen if tor is downgraded after the profile is created qWarning() << "Cannot publish ephemeral hidden services with this version of tor; skipping"; continue; } qDebug() << "torctrl: Configuring hidden service at" << service->dataPath(); QDir dir(service->dataPath()); torConfig.append(qMakePair(QByteArray("HiddenServiceDir"), dir.absolutePath().toLocal8Bit())); const QList &targets = service->targets(); for (QList::ConstIterator tit = targets.begin(); tit != targets.end(); ++tit) { QString target = QString::fromLatin1("%1 %2:%3").arg(tit->servicePort) .arg(tit->targetAddress.toString()) .arg(tit->targetPort); torConfig.append(qMakePair(QByteArray("HiddenServicePort"), target.toLatin1())); } QObject::connect(command, &SetConfCommand::setConfSucceeded, service, &HiddenService::servicePublished); } if (!torConfig.isEmpty()) socket->sendCommand(command, command->build(torConfig)); } } void TorControl::shutdown() { if (!hasOwnership()) { qWarning() << "torctrl: Ignoring shutdown command for a tor instance I don't own"; return; } d->socket->sendCommand("SIGNAL SHUTDOWN\r\n"); } void TorControl::shutdownSync() { if (!hasOwnership()) { qWarning() << "torctrl: Ignoring shutdown command for a tor instance I don't own"; return; } shutdown(); while (d->socket->bytesToWrite()) { if (!d->socket->waitForBytesWritten(5000)) return; } } void TorControlPrivate::statusEvent(int code, const QByteArray &data) { Q_UNUSED(code); QList tokens = splitQuotedStrings(data.trimmed(), ' '); if (tokens.size() < 3) return; qDebug() << "torctrl: status event:" << data.trimmed(); if (tokens[2] == "CIRCUIT_ESTABLISHED") { setTorStatus(TorControl::TorReady); } else if (tokens[2] == "CIRCUIT_NOT_ESTABLISHED") { setTorStatus(TorControl::TorOffline); } else if (tokens[2] == "BOOTSTRAP") { tokens.takeFirst(); updateBootstrap(tokens); } } void TorControlPrivate::updateBootstrap(const QList &data) { bootstrapStatus.clear(); // WARN or NOTICE bootstrapStatus[QStringLiteral("severity")] = data.value(0); for (int i = 1; i < data.size(); i++) { int equals = data[i].indexOf('='); QString key = QString::fromLatin1(data[i].mid(0, equals)); QString value; if (equals >= 0) value = QString::fromLatin1(unquotedString(data[i].mid(equals + 1))); bootstrapStatus[key.toLower()] = value; } qDebug() << bootstrapStatus; emit q->bootstrapStatusChanged(); } QObject *TorControl::getConfiguration(const QString &options) { GetConfCommand *command = new GetConfCommand(GetConfCommand::GetConf); d->socket->sendCommand(command, command->build(options.toLatin1())); QQmlEngine::setObjectOwnership(command, QQmlEngine::CppOwnership); return command; } QObject *TorControl::setConfiguration(const QVariantMap &options) { SetConfCommand *command = new SetConfCommand; command->setResetMode(true); d->socket->sendCommand(command, command->build(options)); QQmlEngine::setObjectOwnership(command, QQmlEngine::CppOwnership); return command; } namespace Tor { class SaveConfigOperation : public PendingOperation { Q_OBJECT public: SaveConfigOperation(QObject *parent) : PendingOperation(parent), command(0) { } void start(TorControlSocket *socket) { Q_ASSERT(!command); command = new GetConfCommand(GetConfCommand::GetInfo); QObject::connect(command, &TorControlCommand::finished, this, &SaveConfigOperation::configTextReply); socket->sendCommand(command, command->build(QList() << "config-text" << "config-file")); } private slots: void configTextReply() { Q_ASSERT(command); if (!command) return; QString path = QFile::decodeName(command->get("config-file").toByteArray()); if (path.isEmpty()) { finishWithError(QStringLiteral("Cannot write torrc without knowing its path")); return; } // Out of paranoia, refuse to write any file not named 'torrc', or if the // file doesn't exist QFileInfo fileInfo(path); if (fileInfo.fileName() != QStringLiteral("torrc") || !fileInfo.exists()) { finishWithError(QStringLiteral("Refusing to write torrc to unacceptable path %1").arg(path)); return; } QSaveFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { finishWithError(QStringLiteral("Failed opening torrc file for writing: %1").arg(file.errorString())); return; } // Remove these keys when writing torrc; they are set at runtime and contain // absolute paths or port numbers static const char *bannedKeys[] = { "ControlPortWriteToFile", "DataDirectory", "HiddenServiceDir", "HiddenServicePort", 0 }; QVariantList configText = command->get("config-text").toList(); foreach (const QVariant &value, configText) { QByteArray line = value.toByteArray(); bool skip = false; for (const char **key = bannedKeys; *key; key++) { if (line.startsWith(*key)) { skip = true; break; } } if (skip) continue; file.write(line); file.write("\n"); } if (!file.commit()) { finishWithError(QStringLiteral("Failed writing torrc: %1").arg(file.errorString())); return; } qDebug() << "torctrl: Wrote torrc file"; finishWithSuccess(); } private: GetConfCommand *command; }; } PendingOperation *TorControl::saveConfiguration() { if (!hasOwnership()) { qWarning() << "torctrl: Ignoring save configuration command for a tor instance I don't own"; return 0; } SaveConfigOperation *operation = new SaveConfigOperation(this); QObject::connect(operation, &PendingOperation::finished, operation, &QObject::deleteLater); operation->start(d->socket); QQmlEngine::setObjectOwnership(operation, QQmlEngine::CppOwnership); return operation; } bool TorControl::hasOwnership() const { return d->hasOwnership; } void TorControl::takeOwnership() { d->hasOwnership = true; d->socket->sendCommand("TAKEOWNERSHIP\r\n"); // Reset PID-based polling QVariantMap options; options[QStringLiteral("__OwningControllerProcess")] = QVariant(); setConfiguration(options); } bool TorControl::torVersionAsNewAs(const QString &match) const { QRegularExpression r(QStringLiteral("[.-]")); QStringList split = torVersion().split(r); QStringList matchSplit = match.split(r); for (int i = 0; i < matchSplit.size(); i++) { if (i >= split.size()) return false; bool ok1 = false, ok2 = false; int currentVal = split[i].toInt(&ok1); int matchVal = matchSplit[i].toInt(&ok2); if (!ok1 || !ok2) return false; if (currentVal > matchVal) return true; if (currentVal < matchVal) return false; } // Versions are equal, up to the length of match return true; } #include "TorControl.moc" ricochet-1.1.4/src/tor/TorControl.h000066400000000000000000000110171300720305500171610ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef TORCONTROL_H #define TORCONTROL_H #include #include #include "utils/PendingOperation.h" class QNetworkProxy; namespace Tor { class HiddenService; class TorControlPrivate; class TorControl : public QObject { Q_OBJECT Q_ENUMS(Status TorStatus) // Status of the control connection Q_PROPERTY(Status status READ status NOTIFY statusChanged) // Status of Tor (and whether it believes it can connect) Q_PROPERTY(TorStatus torStatus READ torStatus NOTIFY torStatusChanged) // Whether it's possible to make a SOCKS connection and connect Q_PROPERTY(bool hasConnectivity READ hasConnectivity NOTIFY connectivityChanged) Q_PROPERTY(QString torVersion READ torVersion NOTIFY connected) Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY statusChanged) Q_PROPERTY(QVariantMap bootstrapStatus READ bootstrapStatus NOTIFY bootstrapStatusChanged) Q_PROPERTY(bool hasOwnership READ hasOwnership NOTIFY hasOwnershipChanged) public: enum Status { Error = -1, NotConnected, Connecting, Authenticating, Connected }; enum TorStatus { TorUnknown, TorOffline, TorReady }; explicit TorControl(QObject *parent = 0); /* Information */ Status status() const; TorStatus torStatus() const; QString torVersion() const; bool torVersionAsNewAs(const QString &version) const; QString errorMessage() const; bool hasConnectivity() const; QHostAddress socksAddress() const; quint16 socksPort() const; QNetworkProxy connectionProxy(); /* Authentication */ void setAuthPassword(const QByteArray &password); /* Connection */ bool isConnected() const { return status() == Connected; } void connect(const QHostAddress &address, quint16 port); /* Ownership means that tor is managed by this socket, and we * can shut it down, own its configuration, etc. */ bool hasOwnership() const; void takeOwnership(); /* Hidden Services */ QList hiddenServices() const; void addHiddenService(HiddenService *service); QVariantMap bootstrapStatus() const; Q_INVOKABLE QObject *getConfiguration(const QString &options); Q_INVOKABLE QObject *setConfiguration(const QVariantMap &options); Q_INVOKABLE PendingOperation *saveConfiguration(); signals: void statusChanged(int newStatus, int oldStatus); void torStatusChanged(int newStatus, int oldStatus); void connected(); void disconnected(); void connectivityChanged(); void bootstrapStatusChanged(); void hasOwnershipChanged(); public slots: /* Instruct Tor to shutdown */ void shutdown(); /* Call shutdown(), and wait synchronously for the command to be written */ void shutdownSync(); void reconnect(); private: TorControlPrivate *d; }; } extern Tor::TorControl *torControl; #endif // TORCONTROLMANAGER_H ricochet-1.1.4/src/tor/TorControlCommand.cpp000066400000000000000000000042421300720305500210150ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "TorControlCommand.h" #include using namespace Tor; TorControlCommand::TorControlCommand() : m_finalStatus(0) { } void TorControlCommand::onReply(int statusCode, const QByteArray &data) { emit replyLine(statusCode, data); } void TorControlCommand::onFinished(int statusCode) { m_finalStatus = statusCode; emit finished(); } void TorControlCommand::onDataLine(const QByteArray &data) { Q_UNUSED(data); } void TorControlCommand::onDataFinished() { qWarning() << "torctrl: Unexpected data response for command"; } ricochet-1.1.4/src/tor/TorControlCommand.h000066400000000000000000000044641300720305500204700ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef TORCONTROLCOMMAND_H #define TORCONTROLCOMMAND_H #include #include namespace Tor { class TorControlCommand : public QObject { Q_OBJECT Q_DISABLE_COPY(TorControlCommand) friend class TorControlSocket; public: TorControlCommand(); int statusCode() const { return m_finalStatus; } signals: void replyLine(int statusCode, const QByteArray &data); void finished(); protected: virtual void onReply(int statusCode, const QByteArray &data); virtual void onFinished(int statusCode); virtual void onDataLine(const QByteArray &data); virtual void onDataFinished(); private: int m_finalStatus; }; } #endif // TORCONTROLCOMMAND_H ricochet-1.1.4/src/tor/TorControlSocket.cpp000066400000000000000000000124061300720305500206700ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "TorControlSocket.h" #include "TorControlCommand.h" #include using namespace Tor; TorControlSocket::TorControlSocket(QObject *parent) : QTcpSocket(parent), currentCommand(0), inDataReply(false) { connect(this, SIGNAL(readyRead()), this, SLOT(process())); connect(this, SIGNAL(disconnected()), this, SLOT(clear())); } TorControlSocket::~TorControlSocket() { clear(); } void TorControlSocket::sendCommand(TorControlCommand *command, const QByteArray &data) { Q_ASSERT(data.endsWith("\r\n")); commandQueue.append(command); write(data); qDebug() << "torctrl: Sent" << data.trimmed(); } void TorControlSocket::registerEvent(const QByteArray &event, TorControlCommand *command) { eventCommands.insert(event, command); QByteArray data("SETEVENTS"); foreach (const QByteArray &key, eventCommands.keys()) { data += ' '; data += key; } data += "\r\n"; sendCommand(data); } void TorControlSocket::clear() { qDeleteAll(commandQueue); commandQueue.clear(); qDeleteAll(eventCommands); eventCommands.clear(); inDataReply = false; currentCommand = 0; } void TorControlSocket::setError(const QString &message) { m_errorMessage = message; emit error(message); abort(); } void TorControlSocket::process() { for (;;) { if (!canReadLine()) return; QByteArray line = readLine(5120); if (!line.endsWith("\r\n")) { setError(QStringLiteral("Invalid control message syntax")); return; } line.chop(2); if (inDataReply) { if (line == ".") { inDataReply = false; if (currentCommand) currentCommand->onDataFinished(); currentCommand = 0; } else { if (currentCommand) currentCommand->onDataLine(line); } continue; } if (line.size() < 4) { setError(QStringLiteral("Invalid control message syntax")); return; } int statusCode = line.left(3).toInt(); char type = line[3]; bool isFinalReply = (type == ' '); inDataReply = (type == '+'); // Trim down to just data line = line.mid(4); if (!isFinalReply && !inDataReply && type != '-') { setError(QStringLiteral("Invalid control message syntax")); return; } // 6xx replies are asynchronous responses if (statusCode >= 600 && statusCode < 700) { if (!currentCommand) { int space = line.indexOf(' '); if (space > 0) currentCommand = eventCommands.value(line.mid(0, space)); if (!currentCommand) { qWarning() << "torctrl: Ignoring unknown event"; continue; } } currentCommand->onReply(statusCode, line); if (isFinalReply) { currentCommand->onFinished(statusCode); currentCommand = 0; } continue; } if (commandQueue.isEmpty()) { qWarning() << "torctrl: Received unexpected data"; continue; } TorControlCommand *command = commandQueue.first(); if (command) command->onReply(statusCode, line); if (inDataReply) { currentCommand = command; } else if (isFinalReply) { commandQueue.takeFirst(); if (command) { command->onFinished(statusCode); command->deleteLater(); } } } } ricochet-1.1.4/src/tor/TorControlSocket.h000066400000000000000000000050551300720305500203370ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef TORCONTROLSOCKET_H #define TORCONTROLSOCKET_H #include #include namespace Tor { class TorControlCommand; class TorControlSocket : public QTcpSocket { Q_OBJECT public: explicit TorControlSocket(QObject *parent = 0); virtual ~TorControlSocket(); QString errorMessage() const { return m_errorMessage; } void registerEvent(const QByteArray &event, TorControlCommand *handler); void sendCommand(const QByteArray &data) { sendCommand(0, data); } void sendCommand(TorControlCommand *command, const QByteArray &data); signals: void error(const QString &message); private slots: void process(); void clear(); private: QQueue commandQueue; QHash eventCommands; QString m_errorMessage; TorControlCommand *currentCommand; bool inDataReply; void setError(const QString &message); }; } #endif // TORCONTROLSOCKET_H ricochet-1.1.4/src/tor/TorManager.cpp000066400000000000000000000226451300720305500174570ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "TorManager.h" #include "TorProcess.h" #include "TorControl.h" #include "GetConfCommand.h" #include "utils/Settings.h" #include #include #include using namespace Tor; namespace Tor { class TorManagerPrivate : public QObject { Q_OBJECT public: TorManager *q; TorProcess *process; TorControl *control; QString dataDir; QStringList logMessages; QString errorMessage; bool configNeeded; explicit TorManagerPrivate(TorManager *parent = 0); QString torExecutablePath() const; bool createDataDir(const QString &path); bool createDefaultTorrc(const QString &path); void setError(const QString &errorMessage); public slots: void processStateChanged(int state); void processErrorChanged(const QString &errorMessage); void processLogMessage(const QString &message); void controlStatusChanged(int status); void getConfFinished(); }; } TorManager::TorManager(QObject *parent) : QObject(parent), d(new TorManagerPrivate(this)) { } TorManagerPrivate::TorManagerPrivate(TorManager *parent) : QObject(parent) , q(parent) , process(0) , control(new TorControl(this)) , configNeeded(false) { connect(control, SIGNAL(statusChanged(int,int)), SLOT(controlStatusChanged(int))); } TorManager *TorManager::instance() { static TorManager *p = 0; if (!p) p = new TorManager(qApp); return p; } TorControl *TorManager::control() { return d->control; } TorProcess *TorManager::process() { return d->process; } QString TorManager::dataDirectory() const { return d->dataDir; } void TorManager::setDataDirectory(const QString &path) { d->dataDir = QDir::fromNativeSeparators(path); if (!d->dataDir.isEmpty() && !d->dataDir.endsWith(QLatin1Char('/'))) d->dataDir.append(QLatin1Char('/')); } bool TorManager::configurationNeeded() const { return d->configNeeded; } QStringList TorManager::logMessages() const { return d->logMessages; } bool TorManager::hasError() const { return !d->errorMessage.isEmpty(); } QString TorManager::errorMessage() const { return d->errorMessage; } void TorManager::start() { if (!d->errorMessage.isEmpty()) { d->errorMessage.clear(); emit errorChanged(); } SettingsObject settings(QStringLiteral("tor")); // If a control port is defined by config or environment, skip launching tor if (!settings.read("controlPort").isUndefined() || !qEnvironmentVariableIsEmpty("TOR_CONTROL_PORT")) { QHostAddress address(settings.read("controlAddress").toString()); quint16 port = (quint16)settings.read("controlPort").toInt(); QByteArray password = settings.read("controlPassword").toString().toLatin1(); if (!qEnvironmentVariableIsEmpty("TOR_CONTROL_HOST")) address = QHostAddress(QString::fromLatin1(qgetenv("TOR_CONTROL_HOST"))); if (!qEnvironmentVariableIsEmpty("TOR_CONTROL_PORT")) { bool ok = false; port = qgetenv("TOR_CONTROL_PORT").toUShort(&ok); if (!ok) port = 0; } if (!qEnvironmentVariableIsEmpty("TOR_CONTROL_PASSWD")) password = qgetenv("TOR_CONTROL_PASSWD"); if (!port) { d->setError(QStringLiteral("Invalid control port settings from environment or configuration")); return; } if (address.isNull()) address = QHostAddress::LocalHost; d->control->setAuthPassword(password); d->control->connect(address, port); } else { // Launch a bundled Tor instance QString executable = d->torExecutablePath(); if (executable.isEmpty()) { d->setError(QStringLiteral("Cannot find tor executable")); return; } if (!d->process) { d->process = new TorProcess(this); connect(d->process, SIGNAL(stateChanged(int)), d, SLOT(processStateChanged(int))); connect(d->process, SIGNAL(errorMessageChanged(QString)), d, SLOT(processErrorChanged(QString))); connect(d->process, SIGNAL(logMessage(QString)), d, SLOT(processLogMessage(QString))); } if (!QFile::exists(d->dataDir) && !d->createDataDir(d->dataDir)) { d->setError(QStringLiteral("Cannot write data location: %1").arg(d->dataDir)); return; } QString defaultTorrc = d->dataDir + QStringLiteral("default_torrc"); if (!QFile::exists(defaultTorrc) && !d->createDefaultTorrc(defaultTorrc)) { d->setError(QStringLiteral("Cannot write data files: %1").arg(defaultTorrc)); return; } QFile torrc(d->dataDir + QStringLiteral("torrc")); if (!torrc.exists() || torrc.size() == 0) { d->configNeeded = true; emit configurationNeededChanged(); } d->process->setExecutable(executable); d->process->setDataDir(d->dataDir); d->process->setDefaultTorrc(defaultTorrc); d->process->start(); } } void TorManagerPrivate::processStateChanged(int state) { qDebug() << Q_FUNC_INFO << state << TorProcess::Ready << process->controlPassword() << process->controlHost() << process->controlPort(); if (state == TorProcess::Ready) { control->setAuthPassword(process->controlPassword()); control->connect(process->controlHost(), process->controlPort()); } } void TorManagerPrivate::processErrorChanged(const QString &errorMessage) { qDebug() << "tor error:" << errorMessage; setError(errorMessage); } void TorManagerPrivate::processLogMessage(const QString &message) { qDebug() << "tor:" << message; if (logMessages.size() >= 50) logMessages.takeFirst(); logMessages.append(message); } void TorManagerPrivate::controlStatusChanged(int status) { if (status == TorControl::Connected) { if (!configNeeded) { // If DisableNetwork is 1, trigger configurationNeeded connect(control->getConfiguration(QStringLiteral("DisableNetwork")), SIGNAL(finished()), SLOT(getConfFinished())); } if (process) { // Take ownership via this control socket control->takeOwnership(); } } } void TorManagerPrivate::getConfFinished() { GetConfCommand *command = qobject_cast(sender()); if (!command) return; if (command->get("DisableNetwork").toInt() == 1 && !configNeeded) { configNeeded = true; emit q->configurationNeededChanged(); } } QString TorManagerPrivate::torExecutablePath() const { SettingsObject settings(QStringLiteral("tor")); QString path = settings.read("executablePath").toString(); if (!path.isEmpty()) return path; #ifdef Q_OS_WIN QString filename(QStringLiteral("/tor.exe")); #else QString filename(QStringLiteral("/tor")); #endif path = qApp->applicationDirPath(); if (QFile::exists(path + filename)) return path + filename; #ifdef BUNDLED_TOR_PATH path = QStringLiteral(BUNDLED_TOR_PATH); if (QFile::exists(path + filename)) return path + filename; #endif // Try $PATH return filename.mid(1); } bool TorManagerPrivate::createDataDir(const QString &path) { QDir dir(path); return dir.mkpath(QStringLiteral(".")); } bool TorManagerPrivate::createDefaultTorrc(const QString &path) { static const char defaultTorrcContent[] = "SocksPort auto\n" "AvoidDiskWrites 1\n" "DisableNetwork 1\n" "__ReloadTorrcOnSIGHUP 0\n"; QFile file(path); if (!file.open(QIODevice::WriteOnly)) return false; if (file.write(defaultTorrcContent) < 0) return false; return true; } void TorManagerPrivate::setError(const QString &message) { errorMessage = message; emit q->errorChanged(); } #include "TorManager.moc" ricochet-1.1.4/src/tor/TorManager.h000066400000000000000000000060501300720305500171140ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef TORMANAGER_H #define TORMANAGER_H #include #include namespace Tor { class TorProcess; class TorControl; class TorManagerPrivate; /* Run/connect to an instance of Tor according to configuration, and manage * UI interaction, first time configuration, etc. */ class TorManager : public QObject { Q_OBJECT Q_PROPERTY(bool configurationNeeded READ configurationNeeded NOTIFY configurationNeededChanged) Q_PROPERTY(QStringList logMessages READ logMessages CONSTANT) Q_PROPERTY(Tor::TorProcess* process READ process CONSTANT) Q_PROPERTY(Tor::TorControl* control READ control CONSTANT) Q_PROPERTY(bool hasError READ hasError NOTIFY errorChanged) Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorChanged) Q_PROPERTY(QString dataDirectory READ dataDirectory WRITE setDataDirectory) public: explicit TorManager(QObject *parent = 0); static TorManager *instance(); TorProcess *process(); TorControl *control(); QString dataDirectory() const; void setDataDirectory(const QString &path); // True on first run or when the Tor configuration wizard needs to be shown bool configurationNeeded() const; QStringList logMessages() const; bool hasError() const; QString errorMessage() const; public slots: void start(); signals: void configurationNeededChanged(); void errorChanged(); private: TorManagerPrivate *d; }; } #endif # ricochet-1.1.4/src/tor/TorProcess.cpp000066400000000000000000000221711300720305500175150ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "TorProcess_p.h" #include "utils/CryptoKey.h" #include "utils/SecureRNG.h" #include #include #include using namespace Tor; TorProcess::TorProcess(QObject *parent) : QObject(parent), d(new TorProcessPrivate(this)) { } TorProcess::~TorProcess() { if (state() > NotStarted) stop(); } TorProcessPrivate::TorProcessPrivate(TorProcess *q) : QObject(q), q(q), state(TorProcess::NotStarted), controlPort(0), controlPortAttempts(0) { connect(&process, &QProcess::started, this, &TorProcessPrivate::processStarted); connect(&process, (void (QProcess::*)(int, QProcess::ExitStatus))&QProcess::finished, this, &TorProcessPrivate::processFinished); connect(&process, (void (QProcess::*)(QProcess::ProcessError))&QProcess::error, this, &TorProcessPrivate::processError); connect(&process, &QProcess::readyRead, this, &TorProcessPrivate::processReadable); controlPortTimer.setInterval(500); connect(&controlPortTimer, &QTimer::timeout, this, &TorProcessPrivate::tryReadControlPort); } QString TorProcess::executable() const { return d->executable; } void TorProcess::setExecutable(const QString &path) { d->executable = path; } QString TorProcess::dataDir() const { return d->dataDir; } void TorProcess::setDataDir(const QString &path) { d->dataDir = path; } QString TorProcess::defaultTorrc() const { return d->defaultTorrc; } void TorProcess::setDefaultTorrc(const QString &path) { d->defaultTorrc = path; } QStringList TorProcess::extraSettings() const { return d->extraSettings; } void TorProcess::setExtraSettings(const QStringList &settings) { d->extraSettings = settings; } TorProcess::State TorProcess::state() const { return d->state; } QString TorProcess::errorMessage() const { return d->errorMessage; } void TorProcess::start() { if (state() > NotStarted) return; d->errorMessage.clear(); if (d->executable.isEmpty() || d->dataDir.isEmpty()) { d->errorMessage = QStringLiteral("Tor executable and data directory not specified"); d->state = Failed; emit errorMessageChanged(d->errorMessage); emit stateChanged(d->state); return; } if (!d->ensureFilesExist()) { d->state = Failed; emit errorMessageChanged(d->errorMessage); emit stateChanged(d->state); return; } QByteArray password = controlPassword(); QByteArray hashedPassword = torControlHashedPassword(password); if (password.isEmpty() || hashedPassword.isEmpty()) { d->errorMessage = QStringLiteral("Random password generation failed"); d->state = Failed; emit errorMessageChanged(d->errorMessage); emit stateChanged(d->state); } QStringList args; if (!d->defaultTorrc.isEmpty()) args << QStringLiteral("--defaults-torrc") << d->defaultTorrc; args << QStringLiteral("-f") << d->torrcPath(); args << QStringLiteral("DataDirectory") << d->dataDir; args << QStringLiteral("HashedControlPassword") << QString::fromLatin1(hashedPassword); args << QStringLiteral("ControlPort") << QStringLiteral("auto"); args << QStringLiteral("ControlPortWriteToFile") << d->controlPortFilePath(); args << QStringLiteral("__OwningControllerProcess") << QString::number(qApp->applicationPid()); args << d->extraSettings; d->state = Starting; emit stateChanged(d->state); if (QFile::exists(d->controlPortFilePath())) QFile::remove(d->controlPortFilePath()); d->controlPort = 0; d->controlHost.clear(); d->process.setProcessChannelMode(QProcess::MergedChannels); d->process.start(d->executable, args, QIODevice::ReadOnly); } void TorProcess::stop() { if (state() < Starting) return; d->controlPortTimer.stop(); if (d->process.state() == QProcess::Starting) d->process.waitForStarted(2000); d->state = NotStarted; // Windows can't terminate the process well, but Tor will clean itself up #ifndef Q_OS_WIN if (d->process.state() == QProcess::Running) { d->process.terminate(); if (!d->process.waitForFinished(5000)) { qWarning() << "Tor process" << d->process.pid() << "did not respond to terminate, killing..."; d->process.kill(); if (!d->process.waitForFinished(2000)) { qCritical() << "Tor process" << d->process.pid() << "did not respond to kill!"; } } } #endif emit stateChanged(d->state); } QByteArray TorProcess::controlPassword() { if (d->controlPassword.isEmpty()) d->controlPassword = SecureRNG::randomPrintable(16); return d->controlPassword; } QHostAddress TorProcess::controlHost() { return d->controlHost; } quint16 TorProcess::controlPort() { return d->controlPort; } bool TorProcessPrivate::ensureFilesExist() { QFile torrc(torrcPath()); if (!torrc.exists()) { QDir dir(dataDir); if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) { errorMessage = QStringLiteral("Cannot create Tor data directory: %1").arg(dataDir); return false; } if (!torrc.open(QIODevice::ReadWrite)) { errorMessage = QStringLiteral("Cannot create Tor configuration file: %1").arg(torrcPath()); return false; } } return true; } QString TorProcessPrivate::torrcPath() const { return QDir::toNativeSeparators(dataDir) + QDir::separator() + QStringLiteral("torrc"); } QString TorProcessPrivate::controlPortFilePath() const { return QDir::toNativeSeparators(dataDir) + QDir::separator() + QStringLiteral("control-port"); } void TorProcessPrivate::processStarted() { state = TorProcess::Connecting; emit q->stateChanged(state); controlPortAttempts = 0; controlPortTimer.start(); } void TorProcessPrivate::processFinished() { if (state < TorProcess::Starting) return; controlPortTimer.stop(); errorMessage = process.errorString(); if (errorMessage.isEmpty()) errorMessage = QStringLiteral("Process exited unexpectedly (code %1)").arg(process.exitCode()); state = TorProcess::Failed; emit q->errorMessageChanged(errorMessage); emit q->stateChanged(state); } void TorProcessPrivate::processError(QProcess::ProcessError error) { if (error == QProcess::FailedToStart || error == QProcess::Crashed) processFinished(); } void TorProcessPrivate::processReadable() { while (process.bytesAvailable() > 0) { QByteArray line = process.readLine(2048).trimmed(); if (!line.isEmpty()) emit q->logMessage(QString::fromLatin1(line)); } } void TorProcessPrivate::tryReadControlPort() { QFile file(controlPortFilePath()); if (file.open(QIODevice::ReadOnly)) { QByteArray data = file.readLine().trimmed(); int p; if (data.startsWith("PORT=") && (p = data.lastIndexOf(':')) > 0) { controlHost = QHostAddress(QString::fromLatin1(data.mid(5, p - 5))); controlPort = data.mid(p+1).toUShort(); if (!controlHost.isNull() && controlPort > 0) { controlPortTimer.stop(); state = TorProcess::Ready; emit q->stateChanged(state); return; } } } if (++controlPortAttempts * controlPortTimer.interval() > 10000) { errorMessage = QStringLiteral("No control port available after launching process"); state = TorProcess::Failed; emit q->errorMessageChanged(errorMessage); emit q->stateChanged(state); } } ricochet-1.1.4/src/tor/TorProcess.h000066400000000000000000000057371300720305500171730ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef TORPROCESS_H #define TORPROCESS_H #include #include namespace Tor { class TorProcessPrivate; /* Launches and controls a Tor instance with behavior suitable for bundling * an instance with the application. */ class TorProcess : public QObject { Q_OBJECT Q_ENUMS(State) Q_PROPERTY(State state READ state NOTIFY stateChanged) Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorMessageChanged) public: enum State { Failed = -1, NotStarted, Starting, Connecting, Ready }; explicit TorProcess(QObject *parent = 0); virtual ~TorProcess(); QString executable() const; void setExecutable(const QString &path); QString dataDir() const; void setDataDir(const QString &path); QString defaultTorrc() const; void setDefaultTorrc(const QString &path); QStringList extraSettings() const; void setExtraSettings(const QStringList &settings); State state() const; QString errorMessage() const; QHostAddress controlHost(); quint16 controlPort(); QByteArray controlPassword(); public slots: void start(); void stop(); signals: void stateChanged(int newState); void errorMessageChanged(const QString &errorMessage); void logMessage(const QString &message); private: TorProcessPrivate *d; }; } #endif ricochet-1.1.4/src/tor/TorProcess_p.h000066400000000000000000000047341300720305500175060ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef TORPROCESS_P_H #define TORPROCESS_P_H #include "TorProcess.h" #include #include namespace Tor { class TorProcessPrivate : public QObject { Q_OBJECT public: TorProcess *q; QProcess process; QString executable; QString dataDir; QString defaultTorrc; QStringList extraSettings; TorProcess::State state; QString errorMessage; QHostAddress controlHost; quint16 controlPort; QByteArray controlPassword; QTimer controlPortTimer; int controlPortAttempts; TorProcessPrivate(TorProcess *q); QString torrcPath() const; QString controlPortFilePath() const; bool ensureFilesExist(); public slots: void processStarted(); void processFinished(); void processError(QProcess::ProcessError error); void processReadable(); void tryReadControlPort(); }; } #endif ricochet-1.1.4/src/tor/TorSocket.cpp000066400000000000000000000111661300720305500173310ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "TorSocket.h" #include "TorControl.h" #include using namespace Tor; TorSocket::TorSocket(QObject *parent) : QTcpSocket(parent) , m_port(0) , m_reconnectEnabled(true) , m_maxInterval(900) , m_connectAttempts(0) { connect(torControl, SIGNAL(connectivityChanged()), SLOT(connectivityChanged())); connect(&m_connectTimer, SIGNAL(timeout()), SLOT(reconnect())); connect(this, SIGNAL(disconnected()), SLOT(onFailed())); connect(this, SIGNAL(error(QAbstractSocket::SocketError)), SLOT(onFailed())); m_connectTimer.setSingleShot(true); connectivityChanged(); } TorSocket::~TorSocket() { } void TorSocket::setReconnectEnabled(bool enabled) { if (enabled == m_reconnectEnabled) return; m_reconnectEnabled = enabled; if (m_reconnectEnabled) { m_connectAttempts = 0; reconnect(); } else { m_connectTimer.stop(); } } void TorSocket::setMaxAttemptInterval(int interval) { m_maxInterval = interval; } void TorSocket::resetAttempts() { m_connectAttempts = 0; if (m_connectTimer.isActive()) { m_connectTimer.stop(); m_connectTimer.start(reconnectInterval() * 1000); } } int TorSocket::reconnectInterval() { int delay = 0; if (m_connectAttempts <= 4) delay = 30; else if (m_connectAttempts <= 6) delay = 120; else delay = m_maxInterval; return qMin(delay, m_maxInterval); } void TorSocket::reconnect() { if (!torControl->hasConnectivity() || !reconnectEnabled()) return; m_connectTimer.stop(); if (!m_host.isEmpty() && m_port) { qDebug() << "Attempting reconnection of socket to" << m_host << m_port; connectToHost(m_host, m_port); } } void TorSocket::connectivityChanged() { if (torControl->hasConnectivity()) { setProxy(torControl->connectionProxy()); if (state() == QAbstractSocket::UnconnectedState) reconnect(); } else { m_connectTimer.stop(); m_connectAttempts = 0; } } void TorSocket::connectToHost(const QString &hostName, quint16 port, OpenMode openMode, NetworkLayerProtocol protocol) { m_host = hostName; m_port = port; if (!torControl->hasConnectivity()) return; if (proxy() != torControl->connectionProxy()) setProxy(torControl->connectionProxy()); QAbstractSocket::connectToHost(hostName, port, openMode, protocol); } void TorSocket::connectToHost(const QHostAddress &address, quint16 port, OpenMode openMode) { TorSocket::connectToHost(address.toString(), port, openMode); } void TorSocket::onFailed() { // Make sure the internal connection to the SOCKS proxy is closed // Otherwise reconnect attempts will fail (#295) close(); if (reconnectEnabled() && !m_connectTimer.isActive()) { m_connectAttempts++; m_connectTimer.start(reconnectInterval() * 1000); qDebug() << "Reconnecting socket to" << m_host << m_port << "in" << m_connectTimer.interval() / 1000 << "seconds"; } } ricochet-1.1.4/src/tor/TorSocket.h000066400000000000000000000066131300720305500167770ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef TORSOCKET_H #define TORSOCKET_H #include #include namespace Tor { /* Specialized QTcpSocket which makes connections over the SOCKS proxy * from a TorControl instance, automatically attempts reconnections, and * reacts to Tor's connectivity state. * * Use normal QTcpSocket/QAbstractSocket API. When a connection fails, it * will be retried automatically after the correct interval and when * connectivity is available. * * To fully disconnect, destroy the object, or call * setReconnectEnabled(false) and disconnect the socket with * disconnectFromHost or abort. * * The caller is responsible for resetting the attempt counter if a * connection was successful and reconnection will be used again. */ class TorSocket : public QTcpSocket { Q_OBJECT public: explicit TorSocket(QObject *parent = 0); virtual ~TorSocket(); bool reconnectEnabled() const { return m_reconnectEnabled; } void setReconnectEnabled(bool enabled); int maxAttemptInterval() { return m_maxInterval; } void setMaxAttemptInterval(int interval); void resetAttempts(); virtual void connectToHost(const QString &hostName, quint16 port, OpenMode openMode = ReadWrite, NetworkLayerProtocol protocol = AnyIPProtocol); virtual void connectToHost(const QHostAddress &address, quint16 port, OpenMode openMode = ReadWrite); QString hostName() const { return m_host; } quint16 port() const { return m_port; } protected: virtual int reconnectInterval(); private slots: void reconnect(); void connectivityChanged(); void onFailed(); private: QString m_host; quint16 m_port; QTimer m_connectTimer; bool m_reconnectEnabled; int m_maxInterval; int m_connectAttempts; using QAbstractSocket::connectToHost; }; } #endif ricochet-1.1.4/src/ui/000077500000000000000000000000001300720305500145145ustar00rootroot00000000000000ricochet-1.1.4/src/ui/ContactsModel.cpp000066400000000000000000000127251300720305500177660ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "ContactsModel.h" #include "core/IdentityManager.h" #include "core/ContactsManager.h" #include inline bool contactSort(const ContactUser *c1, const ContactUser *c2) { if (c1->status() != c2->status()) return c1->status() < c2->status(); return c1->nickname().localeAwareCompare(c2->nickname()) < 0; } ContactsModel::ContactsModel(QObject *parent) : QAbstractListModel(parent), m_identity(0) { } void ContactsModel::setIdentity(UserIdentity *identity) { if (identity == m_identity) return; beginResetModel(); foreach (ContactUser *user, contacts) user->disconnect(this); contacts.clear(); if (m_identity) { disconnect(m_identity, 0, this, 0); disconnect(&m_identity->contacts, 0, this, 0); } m_identity = identity; if (m_identity) { connect(&identity->contacts, SIGNAL(contactAdded(ContactUser*)), SLOT(contactAdded(ContactUser*))); contacts = identity->contacts.contacts(); std::sort(contacts.begin(), contacts.end(), contactSort); foreach (ContactUser *user, contacts) connectSignals(user); } endResetModel(); emit identityChanged(); } QModelIndex ContactsModel::indexOfContact(ContactUser *user) const { int row = contacts.indexOf(user); if (row < 0) return QModelIndex(); return index(row, 0); } ContactUser *ContactsModel::contact(int row) const { return contacts.value(row); } void ContactsModel::updateUser(ContactUser *user) { if (!user) { user = qobject_cast(sender()); if (!user) return; } int row = contacts.indexOf(user); if (row < 0) { user->disconnect(this); return; } QList sorted = contacts; std::sort(sorted.begin(), sorted.end(), contactSort); int newRow = sorted.indexOf(user); if (row != newRow) { beginMoveRows(QModelIndex(), row, row, QModelIndex(), (newRow > row) ? (newRow+1) : newRow); contacts = sorted; endMoveRows(); } emit dataChanged(index(newRow, 0), index(newRow, 0)); } void ContactsModel::connectSignals(ContactUser *user) { connect(user, SIGNAL(statusChanged()), SLOT(updateUser())); connect(user, SIGNAL(nicknameChanged()), SLOT(updateUser())); connect(user, SIGNAL(contactDeleted(ContactUser*)), SLOT(contactRemoved(ContactUser*))); } void ContactsModel::contactAdded(ContactUser *user) { Q_ASSERT(!indexOfContact(user).isValid()); connectSignals(user); QList::Iterator lp = qLowerBound(contacts.begin(), contacts.end(), user, contactSort); int row = lp - contacts.begin(); beginInsertRows(QModelIndex(), row, row); contacts.insert(lp, user); endInsertRows(); } void ContactsModel::contactRemoved(ContactUser *user) { if (!user && !(user = qobject_cast(sender()))) return; int row = contacts.indexOf(user); beginRemoveRows(QModelIndex(), row, row); contacts.removeAt(row); endRemoveRows(); disconnect(user, 0, this, 0); } QHash ContactsModel::roleNames() const { QHash roles; roles[Qt::DisplayRole] = "name"; roles[PointerRole] = "contact"; roles[StatusRole] = "status"; return roles; } int ContactsModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) return 0; return contacts.size(); } QVariant ContactsModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() >= contacts.size()) return QVariant(); ContactUser *user = contacts[index.row()]; switch (role) { case Qt::DisplayRole: case Qt::EditRole: return user->nickname(); case PointerRole: return QVariant::fromValue(user); case StatusRole: return user->status(); } return QVariant(); } ricochet-1.1.4/src/ui/ContactsModel.h000066400000000000000000000057131300720305500174320ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef CONTACTSMODEL_H #define CONTACTSMODEL_H #include #include class UserIdentity; class ContactUser; class ContactsModel : public QAbstractListModel { Q_OBJECT Q_DISABLE_COPY(ContactsModel) Q_PROPERTY(UserIdentity* identity READ identity WRITE setIdentity NOTIFY identityChanged) public: enum { PointerRole = Qt::UserRole, StatusRole, AlertRole /* bool */ }; explicit ContactsModel(QObject *parent = 0); UserIdentity *identity() const { return m_identity; } void setIdentity(UserIdentity *identity); Q_INVOKABLE QModelIndex indexOfContact(ContactUser *user) const; Q_INVOKABLE int rowOfContact(ContactUser *user) const { return indexOfContact(user).row(); } Q_INVOKABLE ContactUser *contact(int row) const; virtual int rowCount(const QModelIndex &parent = QModelIndex()) const; virtual QHash roleNames() const; virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; signals: void identityChanged(); private slots: void updateUser(ContactUser *user = 0); void contactAdded(ContactUser *user); void contactRemoved(ContactUser *user); private: UserIdentity *m_identity; QList contacts; void connectSignals(ContactUser *user); }; #endif // CONTACTSMODEL_H ricochet-1.1.4/src/ui/LanguagesModel.cpp000066400000000000000000000066061300720305500201170ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "LanguagesModel.h" #include #include #include LanguagesModel::LanguagesModel(QObject* parent) : QAbstractListModel(parent) { // create the list of languages based on present translation files in ":/lang" folder QDir languagesFolder(QStringLiteral(":/lang")); languages.append(LanguageEntry(tr("System default"), QString())); foreach (const QString& translationFile, languagesFolder.entryList()) { QString localeID = translationFile; localeID.remove(QLatin1String("ricochet_")).remove(QLatin1String(".qm")); QString nativeName = QLocale(localeID).nativeLanguageName(); languages.append(LanguageEntry(nativeName, localeID)); } } int LanguagesModel::rowCount(const QModelIndex &) const { return languages.length(); } QVariant LanguagesModel::data(const QModelIndex &index, int role) const { if (index.row() >= 0 && index.row() < languages.length()) { switch( role ) { case NameRole: return languages[index.row()].nativeName; case LocaleIDRole: return languages[index.row()].localeID; default: return QVariant(); } } else { return QVariant(); } } QHash LanguagesModel::roleNames() const { QHash roles; roles[NameRole] = "nativeName"; roles[LocaleIDRole] = "localeID"; return roles; } QString LanguagesModel::localeID(int row) { if (row >= 0 && row < languages.length()) return languages[row].localeID; return QString(); } int LanguagesModel::rowForLocaleID(const QString &localeID) { for (int i = 0; i < languages.length(); i++) { if (languages[i].localeID == localeID) return i; } return -1; } ricochet-1.1.4/src/ui/LanguagesModel.h000066400000000000000000000047051300720305500175620ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef LANGUAGESMODEL_H #define LANGUAGESMODEL_H #include class LanguagesModel : public QAbstractListModel { Q_OBJECT public: enum { NameRole = Qt::UserRole, LocaleIDRole }; LanguagesModel(QObject* parent = 0); virtual int rowCount(const QModelIndex &parent) const; virtual QVariant data(const QModelIndex &index, int role) const; virtual QHash roleNames() const; Q_INVOKABLE QString localeID(int index); Q_INVOKABLE int rowForLocaleID(const QString &localeID); private: struct LanguageEntry { QString nativeName; QString localeID; LanguageEntry(const QString& name, const QString& localeID) : nativeName(name), localeID(localeID) {} }; QList languages; }; #endif // LANGUAGESMODEL_H ricochet-1.1.4/src/ui/LinkedText.cpp000066400000000000000000000067021300720305500173000ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "LinkedText.h" #include #include #include #include #include LinkedText::LinkedText(QObject *parent) : QObject(parent) { // Select things that look like URLs of some kind and allow QUrl::fromUserInput to validate them linkRegex = QRegularExpression(QStringLiteral("([a-z]{3,9}:|www\\.)([^\\s,.);!>]|[,.);!>](?!\\s|$))+"), QRegularExpression::CaseInsensitiveOption); allowedSchemes << QStringLiteral("http") << QStringLiteral("https") << QStringLiteral("torsion") << QStringLiteral("ricochet"); } QString LinkedText::parsed(const QString &input) { QString re; int p = 0; QRegularExpressionMatchIterator it = linkRegex.globalMatch(input); while (it.hasNext()) { QRegularExpressionMatch match = it.next(); int start = match.capturedStart(); QUrl url = QUrl::fromUserInput(match.capturedRef().toString()); if (!allowedSchemes.contains(url.scheme().toLower())) continue; if (start > p) re.append(input.mid(p, start - p).toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("
"))); re.append(QStringLiteral("
%2").arg(QString::fromLatin1(url.toEncoded()).toHtmlEscaped(), match.capturedRef().toString().toHtmlEscaped())); p = match.capturedEnd(); } if (p < input.size()) re.append(input.mid(p).toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("
"))); return re; } void LinkedText::copyToClipboard(const QString &text) { QClipboard *clipboard = qApp->clipboard(); clipboard->setText(text); // For X11, also copy to the selection clipboard (middle-click) if (clipboard->supportsSelection()) clipboard->setText(text, QClipboard::Selection); } ricochet-1.1.4/src/ui/LinkedText.h000066400000000000000000000040401300720305500167360ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef LINKEDTEXT_H #define LINKEDTEXT_H #include #include class LinkedText : public QObject { Q_OBJECT Q_DISABLE_COPY(LinkedText) public: explicit LinkedText(QObject *parent = 0); Q_INVOKABLE QString parsed(const QString &input); Q_INVOKABLE void copyToClipboard(const QString &text); private: QRegularExpression linkRegex; QStringList allowedSchemes; }; #endif ricochet-1.1.4/src/ui/MainWindow.cpp000066400000000000000000000175301300720305500173020ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "MainWindow.h" #include "core/UserIdentity.h" #include "core/IncomingRequestManager.h" #include "core/OutgoingContactRequest.h" #include "core/IdentityManager.h" #include "core/ContactIDValidator.h" #include "core/ConversationModel.h" #include "tor/TorControl.h" #include "tor/TorManager.h" #include "tor/TorProcess.h" #include "ContactsModel.h" #include "ui/LinkedText.h" #include "utils/Settings.h" #include "utils/PendingOperation.h" #include "utils/Useful.h" #include "ui/LanguagesModel.h" #include #include #include #include #include #include #include #include #include #include MainWindow *uiMain = 0; static QObject *linkedtext_singleton(QQmlEngine *, QJSEngine *) { return new LinkedText; } /* Through the QQmlNetworkAccessManagerFactory below, all network requests * created via QML will be passed to this object; including, for example, * tags parsed in rich Text items. * * Ricochet's UI does not directly cause network requests for any reason. These * are always a potentially deanonymizing bug. This object will block them, * and assert if appropriate. */ class BlockedNetworkAccessManager : public QNetworkAccessManager { public: BlockedNetworkAccessManager(QObject *parent) : QNetworkAccessManager(parent) { /* Either of these is sufficient to cause any network request to fail. * Both of them should be redundant, because createRequest below also * blackholes every request (and crashes for assert builds). */ setNetworkAccessible(QNetworkAccessManager::NotAccessible); setProxy(QNetworkProxy(QNetworkProxy::Socks5Proxy, QLatin1String("0.0.0.0"), 0)); } protected: virtual QNetworkReply *createRequest(Operation op, const QNetworkRequest &req, QIODevice *outgoingData = 0) { BUG() << "QML attempted to load a network resource from" << req.url() << " - this is potentially an input sanitization flaw."; return QNetworkAccessManager::createRequest(op, QNetworkRequest(), outgoingData); } }; class NetworkAccessBlockingFactory : public QQmlNetworkAccessManagerFactory { public: virtual QNetworkAccessManager *create(QObject *parent) { return new BlockedNetworkAccessManager(parent); } }; MainWindow::MainWindow(QObject *parent) : QObject(parent) { Q_ASSERT(!uiMain); uiMain = this; qml = new QQmlApplicationEngine(this); qml->setNetworkAccessManagerFactory(new NetworkAccessBlockingFactory); qmlRegisterUncreatableType("im.ricochet", 1, 0, "ContactUser", QString()); qmlRegisterUncreatableType("im.ricochet", 1, 0, "UserIdentity", QString()); qmlRegisterUncreatableType("im.ricochet", 1, 0, "ContactsManager", QString()); qmlRegisterUncreatableType("im.ricochet", 1, 0, "IncomingRequestManager", QString()); qmlRegisterUncreatableType("im.ricochet", 1, 0, "IncomingContactRequest", QString()); qmlRegisterUncreatableType("im.ricochet", 1, 0, "OutgoingContactRequest", QString()); qmlRegisterUncreatableType("im.ricochet", 1, 0, "TorControl", QString()); qmlRegisterUncreatableType("im.ricochet", 1, 0, "TorProcess", QString()); qmlRegisterType("im.ricochet", 1, 0, "ConversationModel"); qmlRegisterType("im.ricochet", 1, 0, "ContactsModel"); qmlRegisterType("im.ricochet", 1, 0, "ContactIDValidator"); qmlRegisterType("im.ricochet", 1, 0, "Settings"); qmlRegisterSingletonType("im.ricochet", 1, 0, "LinkedText", linkedtext_singleton); qmlRegisterType("im.ricochet", 1, 0, "LanguagesModel"); qRegisterMetaType(); } MainWindow::~MainWindow() { } bool MainWindow::showUI() { Q_ASSERT(!identityManager->identities().isEmpty()); qml->rootContext()->setContextProperty(QLatin1String("userIdentity"), identityManager->identities()[0]); qml->rootContext()->setContextProperty(QLatin1String("torControl"), torControl); qml->rootContext()->setContextProperty(QLatin1String("torInstance"), Tor::TorManager::instance()); qml->rootContext()->setContextProperty(QLatin1String("uiMain"), this); qml->load(QUrl(QLatin1String("qrc:/ui/main.qml"))); if (qml->rootObjects().isEmpty()) { // Assume this is only applicable to technical users; not worth translating or simplifying. QMessageBox::critical(0, QStringLiteral("Ricochet"), QStringLiteral("An error occurred while loading the Ricochet UI.\n\n" "You might be missing plugins or dependency packages.")); qCritical() << "Failed to load UI. Exiting."; return false; } return true; } QString MainWindow::version() const { return qApp->applicationVersion(); } QString MainWindow::aboutText() const { QFile file(QStringLiteral(":/text/LICENSE")); file.open(QIODevice::ReadOnly); QString text = QString::fromUtf8(file.readAll()); return text; } QVariantMap MainWindow::screens() const { QVariantMap mapScreenSizes; foreach (QScreen *screen, QGuiApplication::screens()) { QVariantMap screenObj; screenObj.insert(QString::fromUtf8("width"), screen->availableSize().width()); screenObj.insert(QString::fromUtf8("height"), screen->availableSize().height()); screenObj.insert(QString::fromUtf8("left"), screen->geometry().left()); screenObj.insert(QString::fromUtf8("top"), screen->geometry().top()); mapScreenSizes.insert(screen->name(), screenObj); } return mapScreenSizes; } /* QMessageBox implementation for Qt <5.2 */ bool MainWindow::showRemoveContactDialog(ContactUser *user) { if (!user) return false; QMessageBox::StandardButton btn = QMessageBox::question(0, tr("Remove %1").arg(user->nickname()), tr("Do you want to permanently remove %1?").arg(user->nickname())); return btn == QMessageBox::Yes; } QQuickWindow *MainWindow::findParentWindow(QQuickItem *item) { Q_ASSERT(item); return item ? item->window() : 0; } ricochet-1.1.4/src/ui/MainWindow.h000066400000000000000000000051301300720305500167400ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef MAINWINDOW_H #define MAINWINDOW_H #include #include class ContactUser; class UserIdentity; class IncomingContactRequest; class OutgoingContactRequest; class QQmlApplicationEngine; class QQuickItem; class QQuickWindow; class MainWindow : public QObject { Q_OBJECT Q_DISABLE_COPY(MainWindow) Q_PROPERTY(QString version READ version CONSTANT) Q_PROPERTY(QString aboutText READ aboutText CONSTANT) Q_PROPERTY(QVariantMap screens READ screens CONSTANT) public: explicit MainWindow(QObject *parent = 0); ~MainWindow(); bool showUI(); QString aboutText() const; QString version() const; QVariantMap screens() const; Q_INVOKABLE bool showRemoveContactDialog(ContactUser *user); // Find parent window of a QQuickItem; exposed as property after Qt 5.4 Q_INVOKABLE QQuickWindow *findParentWindow(QQuickItem *item); private: QQmlApplicationEngine *qml; }; extern MainWindow *uiMain; #endif // MAINWINDOW_H ricochet-1.1.4/src/ui/qml/000077500000000000000000000000001300720305500153055ustar00rootroot00000000000000ricochet-1.1.4/src/ui/qml/AboutPreferences.qml000066400000000000000000000013761300720305500212630ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 ColumnLayout { anchors { fill: parent margins: 8 } Label { Layout.fillWidth: true //: %1 version, e.g. 1.0.0 text: qsTr("Ricochet %1").arg(uiMain.version) horizontalAlignment: Qt.AlignHCenter } Label { Layout.fillWidth: true text: "ricochet.im" horizontalAlignment: Qt.AlignHCenter MouseArea { anchors.fill: parent onClicked: Qt.openUrlExternally("https://ricochet.im/") } } TextArea { Layout.fillHeight: true Layout.fillWidth: true readOnly: true text: uiMain.aboutText } } ricochet-1.1.4/src/ui/qml/AddContactDialog.qml000066400000000000000000000056261300720305500211550ustar00rootroot00000000000000import QtQuick 2.2 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 ApplicationWindow { id: addContactWindow width: 400 height: 300 minimumWidth: width maximumWidth: width minimumHeight: height maximumHeight: height flags: styleHelper.dialogWindowFlags modality: Qt.WindowModal title: mainWindow.title signal closed onVisibleChanged: if (!visible) closed() property string staticContactId: fields.contactId.text function close() { visible = false } function accept() { if (!fields.hasValidRequest) return userIdentity.contacts.createContactRequest(fields.contactId.text, fields.name.text, "", fields.message.text) close() } ColumnLayout { id: infoArea z: 2 anchors { left: parent.left right: parent.right top: parent.top topMargin: 8 leftMargin: 16 rightMargin: 16 } Label { Layout.columnSpan: 2 Layout.fillWidth: true horizontalAlignment: Qt.AlignHCenter wrapMode: Text.Wrap text: qsTr("Share your Ricochet ID to allow connection requests") } ContactIDField { id: localId Layout.fillWidth: true readOnly: true text: userIdentity.contactID horizontalAlignment: Qt.AlignHCenter } Item { height: 1 } Rectangle { color: palette.mid height: 1 Layout.fillWidth: true Layout.columnSpan: 2 } Item { height: 1 } } ContactRequestFields { id: fields anchors { left: parent.left right: parent.right top: infoArea.bottom bottom: buttonRow.top margins: 8 leftMargin: 16 rightMargin: 16 } Component.onCompleted: { if (staticContactId.length > 0) { fields.contactId.text = staticContactId fields.contactId.readOnly = true fields.name.focus = true } else { fields.contactId.focus = true } } } RowLayout { id: buttonRow anchors { right: parent.right bottom: parent.bottom rightMargin: 16 bottomMargin: 8 } Button { text: qsTr("Cancel") onClicked: addContactWindow.close() } Button { text: qsTr("Add") isDefault: true enabled: fields.hasValidRequest onClicked: addContactWindow.accept() } } Action { shortcut: StandardKey.Close onTriggered: addContactWindow.close() } Action { shortcut: "Escape" onTriggered: addContactWindow.close() } } ricochet-1.1.4/src/ui/qml/AudioNotifications.qml000066400000000000000000000006551300720305500216210ustar00rootroot00000000000000import QtQuick 2.0 import QtMultimedia 5.0 QtObject { id: audioNotifications property real volume: uiSettings.data.notificationVolume property SoundEffect message: SoundEffect { source: "qrc:/sounds/message.wav" volume: audioNotifications.volume } property SoundEffect contactOnline: SoundEffect { source: "qrc:/sounds/online.wav" volume: audioNotifications.volume } } ricochet-1.1.4/src/ui/qml/Bubble.qml000066400000000000000000000026471300720305500172240ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 Rectangle { id: bubble x: { switch (horizontalAlignment) { case Qt.AlignHCenter: return -width + (target.width / 2) + 15 case Qt.AlignLeft: return 20 case Qt.AlignRight: return (target.width - width - 20) } } y: parent.height + 14 width: label.contentWidth + 12 height: label.contentHeight + 12 color: "#c4e7ff" border.color: Qt.darker(color, 1.2) border.width: 1 opacity: displayed ? 1 : 0 visible: opacity property alias textFormat: label.textFormat property Item target property int maximumWidth: target ? target.width : 100 property int horizontalAlignment: Qt.AlignHCenter property bool displayed: text.length property alias text: label.text Behavior on opacity { NumberAnimation { duration: 200 } } Rectangle { id: arrow rotation: 45 width: 10 height: 10 x: (horizontalAlignment == Qt.AlignLeft) ? 20 : parent.width - 20 y: -5 color: parent.color border.color: parent.border.color border.width: 1 } Rectangle { x: arrow.x - 1 width: arrow.width + 2 height: 10 color: parent.color } Label { id: label wrapMode: Text.Wrap width: maximumWidth - 16 textFormat: Text.PlainText x: 6 y: 6 } } ricochet-1.1.4/src/ui/qml/ChatMessageArea.qml000066400000000000000000000032101300720305500207710ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 Rectangle { id: scroll clip: true color: palette.base property alias model: messageView.model /* As of Qt 5.5.0, ScrollView is too buggy to use. It often fails to keep the * view scrolled to the bottom, and moves erratically on wheel events. */ Rectangle { id: scrollBar width: 5 height: messageView.visibleArea.heightRatio * (messageView.height - 10) y: 5 + messageView.visibleArea.yPosition * (messageView.height - 10) x: parent.width - width - 3 z: 1000 visible: messageView.visibleArea.heightRatio < 1 color: "#bbbbbb" radius: 14 } ListView { id: messageView spacing: 12 pixelAligned: true boundsBehavior: Flickable.StopAtBounds anchors.fill: parent MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton onWheel: { wheel.accepted = true if (wheel.pixelDelta.y !== 0) { messageView.contentY = Math.max(messageView.originY, Math.min(messageView.originY + messageView.contentHeight - messageView.height, messageView.contentY - wheel.pixelDelta.y)) } else if (wheel.angleDelta.y !== 0) { messageView.flick(0, wheel.angleDelta.y * 5) } } } header: Item { width: 1; height: messageView.spacing } footer: Item { width: 1; height: messageView.spacing } delegate: MessageDelegate { } verticalLayoutDirection: ListView.BottomToTop } } ricochet-1.1.4/src/ui/qml/ChatPage.qml000066400000000000000000000112041300720305500174720ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 import im.ricochet 1.0 FocusScope { id: chatPage property var contact property TextArea textField: textInput property var conversationModel: (contact !== null) ? contact.conversation : null function forceActiveFocus() { textField.forceActiveFocus() } onVisibleChanged: if (visible) forceActiveFocus() property bool active: visible && activeFocusItem !== null onActiveChanged: { if (active) conversationModel.resetUnreadCount() } Connections { target: conversationModel onUnreadCountChanged: if (active) conversationModel.resetUnreadCount() } RowLayout { id: infoBar anchors { top: parent.top left: parent.left leftMargin: 4 right: parent.right rightMargin: 4 } height: implicitHeight + 8 spacing: 8 PresenceIcon { status: contact.status } Label { text: contact.nickname textFormat: Text.PlainText font.pointSize: styleHelper.pointSize } Item { Layout.fillWidth: true height: 1 } } Rectangle { anchors { left: parent.left right: parent.right top: infoBar.top bottom: infoBar.bottom } color: palette.base z: -1 Column { anchors { top: parent.bottom left: parent.left right: parent.right } Rectangle { width: parent.width; height: 1; color: palette.midlight; } Rectangle { width: parent.width; height: 1; color: palette.window; } } } ChatMessageArea { anchors { top: infoBar.bottom topMargin: 2 left: parent.left right: parent.right bottom: statusBar.top } model: conversationModel } StatusBar { id: statusBar anchors { left: parent.left right: parent.right bottom: parent.bottom } height: statusLayout.height + 8 RowLayout { id: statusLayout width: statusBar.width - 8 y: 2 TextArea { id: textInput Layout.fillWidth: true y: 2 // This ridiculous incantation enables an automatically sized TextArea Layout.preferredHeight: mapFromItem(flickableItem, 0, 0).y * 2 + Math.max(styleHelper.textHeight + 2*edit.textMargin, flickableItem.contentHeight) Layout.maximumHeight: (styleHelper.textHeight * 4) + (2 * edit.textMargin) textMargin: 3 wrapMode: TextEdit.Wrap textFormat: TextEdit.PlainText font.pointSize: styleHelper.pointSize focus: true property TextEdit edit Component.onCompleted: { var objects = contentItem.contentItem.children for (var i = 0; i < objects.length; i++) { if (objects[i].hasOwnProperty('textDocument')) { edit = objects[i] break } } edit.Keys.pressed.connect(keyHandler) } function keyHandler(event) { switch (event.key) { case Qt.Key_Enter: case Qt.Key_Return: if (event.modifiers & Qt.ShiftModifier || event.modifiers & Qt.AltModifier) { textInput.insert(textInput.cursorPosition, "\n") } else { send() } event.accepted = true break default: event.accepted = false } } function send() { if (textInput.length > 2000) textInput.remove(2000, textInput.length) conversationModel.sendMessage(textInput.text) textInput.remove(0, textInput.length) } onLengthChanged: { if (textInput.length > 2000) textInput.remove(2000, textInput.length) } } } } } ricochet-1.1.4/src/ui/qml/ChatWindow.qml000066400000000000000000000022141300720305500200660ustar00rootroot00000000000000import QtQuick 2.2 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 import im.ricochet 1.0 ApplicationWindow { id: chatWindow width: 500 height: 400 title: contact !== null ? contact.nickname : "" property alias contact: chatPage.contact signal closed onVisibleChanged: { if (!visible) closed() } onClosed: { // If not also in combined window mode, clear chat history when closing if (!uiSettings.data.combinedChatWindow) chatPage.conversationModel.clear() } property bool inactive: true onActiveFocusItemChanged: { // Focus text input when window regains focus if (activeFocusItem !== null && inactive) { inactive = false retakeFocus.start() } else if (activeFocusItem === null) { inactive = true } } Timer { id: retakeFocus onTriggered: chatPage.forceActiveFocus() interval: 1 } ChatPage { id: chatPage anchors.fill: parent } Action { shortcut: StandardKey.Close onTriggered: chatWindow.close() } } ricochet-1.1.4/src/ui/qml/ContactActions.qml000066400000000000000000000026341300720305500207410ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Dialogs 1.1 import "ContactWindow.js" as ContactWindow Item { id: contactMenu property QtObject contact function openWindow() { var window = ContactWindow.getWindow(contact) window.raise() window.requestActivate() } function removeContact() { removeContactDialog.active = true if (removeContactDialog.item !== null) { removeContactDialog.item.open() } else if (uiMain.showRemoveContactDialog(contact)) { contact.deleteContact() } } function openContextMenu() { contextMenu.popup() } function openPreferences() { root.openPreferences("ContactPreferences.qml", { 'selectedContact': contact }) } signal renameTriggered Menu { id: contextMenu MenuItem { text: qsTr("Open Window") onTriggered: openWindow() } MenuItem { text: qsTr("Details...") onTriggered: openPreferences() } MenuItem { text: qsTr("Rename") onTriggered: renameTriggered() } MenuSeparator { } MenuItem { text: qsTr("Remove") onTriggered: removeContact() } } Loader { id: removeContactDialog source: "MessageDialogWrapper.qml" active: false } } ricochet-1.1.4/src/ui/qml/ContactIDField.qml000066400000000000000000000054351300720305500206030ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Controls.Styles 1.0 import QtQuick.Layouts 1.0 import im.ricochet 1.0 import "utils.js" as Utils FocusScope { id: contactId z: 4 height: layout.height Layout.fillWidth: true property alias text: field.text property alias readOnly: field.readOnly property alias horizontalAlignment: field.horizontalAlignment property alias acceptableInput: field.acceptableInput property bool showCopyButton: true RowLayout { id: layout width: parent.width TextField { id: field Layout.fillWidth: true font.family: "Courier" validator: readOnly ? null : idValidator placeholderText: "ricochet:" focus: true onTextChanged: errorBubble.clear() ContactIDValidator { id: idValidator notContactOfIdentity: userIdentity onFailed: { var contact if ((contact = matchingContact(field.text))) errorBubble.show(qsTr("%1 is already your contact").arg(Utils.htmlEscaped(contact.nickname))) else if (matchesIdentity(field.text)) errorBubble.show(qsTr("You can't add yourself as a contact")) else errorBubble.show(qsTr("Enter an ID starting with ricochet:")) } } Bubble { id: errorBubble target: field horizontalAlignment: Qt.AlignLeft textFormat: Text.RichText function show(value) { text = value opacity = 1 } function clear() { opacity = 0 } } function copyLoudly() { // The LinkedText helper also copies to the X11 selection clipboard LinkedText.copyToClipboard(field.text) copyBubble.displayed = true bubbleResetTimer.start() } MouseArea { anchors.fill: parent enabled: field.readOnly onClicked: field.copyLoudly() } Bubble { id: copyBubble target: field text: qsTr("Copied to clipboard") displayed: false } Timer { id: bubbleResetTimer interval: 1000 onTriggered: copyBubble.displayed = false } } Button { text: qsTr("Copy") visible: contactId.showCopyButton onClicked: field.copyLoudly() } } } ricochet-1.1.4/src/ui/qml/ContactList.qml000066400000000000000000000057651300720305500202640ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 import im.ricochet 1.0 ScrollView { id: scroll data: [ Rectangle { anchors.fill: scroll z: -1 color: palette.base }, ContactsModel { id: contactsModel identity: userIdentity } ] property QtObject selectedContact property ListView view: contactListView // Emitted for double click on a contact signal contactActivated(ContactUser contact, Item actions) onSelectedContactChanged: { if (selectedContact !== contactsModel.contact(contactListView.currentIndex)) { contactListView.currentIndex = contactsModel.rowOfContact(selectedContact) } } ListView { id: contactListView model: contactsModel currentIndex: -1 signal contactActivated(ContactUser contact, Item actions) onContactActivated: scroll.contactActivated(contact, actions) onCurrentIndexChanged: { // Not using a binding to allow writes to selectedContact scroll.selectedContact = contactsModel.contact(contactListView.currentIndex) } data: [ MouseArea { anchors.fill: parent z: -100 onClicked: contactListView.currentIndex = -1 } ] section.property: "status" section.delegate: Row { width: parent.width - x height: label.height + 4 x: 8 spacing: 6 Label { id: label y: 2 font.pointSize: styleHelper.pointSize font.bold: true font.capitalization: Font.SmallCaps textFormat: Text.PlainText color: "#3f454a" text: { // Translation strings are uppercase for legacy reasons, and because they // should correctly be capitalized. We go lowercase only because it looks // nicer when using SmallCaps, and that's a display detail. switch (parseInt(section)) { case ContactUser.Online: return qsTr("Online").toLowerCase() case ContactUser.Offline: return qsTr("Offline").toLowerCase() case ContactUser.RequestPending: return qsTr("Requests").toLowerCase() case ContactUser.RequestRejected: return qsTr("Rejected").toLowerCase() case ContactUser.Outdated: return qsTr("Outdated").toLowerCase() } } } Rectangle { height: 1 width: parent.width - x anchors { top: label.verticalCenter topMargin: 1 } color: "black" opacity: 0.1 } } delegate: ContactListDelegate { } } } ricochet-1.1.4/src/ui/qml/ContactListDelegate.qml000066400000000000000000000055001300720305500217020ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import im.ricochet 1.0 Rectangle { id: delegate color: highlighted ? "#c4e7ff" : "white" width: parent.width height: nameLabel.height + 8 property bool highlighted: ListView.isCurrentItem onHighlightedChanged: { if (renameMode) renameMode = false } PresenceIcon { id: presenceIcon anchors { left: parent.left leftMargin: 20 verticalCenter: nameLabel.verticalCenter } status: model.status } Label { id: nameLabel anchors { left: presenceIcon.right leftMargin: 6 right: unreadBadge.left rightMargin: 8 verticalCenter: parent.verticalCenter } text: model.name textFormat: Text.PlainText elide: Text.ElideRight font.pointSize: styleHelper.pointSize color: "black" opacity: model.status === ContactUser.Online ? 1 : 0.8 } UnreadCountBadge { id: unreadBadge anchors { verticalCenter: parent.verticalCenter right: parent.right rightMargin: 8 } value: model.contact.conversation.unreadCount } ContactActions { id: contextMenu contact: model.contact onRenameTriggered: renameMode = true } MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton onPressed: { if (!delegate.ListView.isCurrentItem) contactListView.currentIndex = model.index } onClicked: { if (mouse.button === Qt.RightButton) { contextMenu.openContextMenu() } } onDoubleClicked: { if (mouse.button === Qt.LeftButton) { contactListView.contactActivated(model.contact, contextMenu) } } } property bool renameMode property Item renameItem onRenameModeChanged: { if (renameMode && renameItem === null) { renameItem = renameComponent.createObject(delegate) renameItem.forceActiveFocus() renameItem.selectAll() } else if (!renameMode && renameItem !== null) { renameItem.visible = false renameItem.destroy() renameItem = null } } Component { id: renameComponent TextField { id: nameField anchors { left: nameLabel.left right: nameLabel.right verticalCenter: nameLabel.verticalCenter } text: model.contact.nickname onAccepted: { model.contact.nickname = text delegate.renameMode = false } } } } ricochet-1.1.4/src/ui/qml/ContactPreferences.qml000066400000000000000000000150531300720305500216010ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 import im.ricochet 1.0 Item { property alias selectedContact: contacts.selectedContact RowLayout { anchors { fill: parent margins: 8 } ContactList { id: contacts Layout.preferredWidth: 200 Layout.minimumWidth: 150 Layout.fillHeight: true frameVisible: true } data: [ ContactActions { id: contactActions contact: contacts.selectedContact } ] ColumnLayout { id: contactInfo visible: contact !== null Layout.fillHeight: true Layout.fillWidth: true property QtObject contact: contacts.selectedContact property QtObject request: (contact !== null) ? contact.contactRequest : null Item { height: 1; width: 1 } Label { id: nickname Layout.fillWidth: true text: visible ? contactInfo.contact.nickname : "" textFormat: Text.PlainText horizontalAlignment: Qt.AlignHCenter font.pointSize: styleHelper.pointSize + 1 property bool renameMode property Item renameItem onRenameModeChanged: { if (renameMode && renameItem === null) { renameItem = renameComponent.createObject(nickname) renameItem.forceActiveFocus() renameItem.selectAll() } else if (!renameMode && renameItem !== null) { renameItem.focus = false renameItem.visible = false renameItem.destroy() renameItem = null } } MouseArea { anchors.fill: parent; onDoubleClicked: nickname.renameMode = true } Component { id: renameComponent TextField { id: nameField anchors { left: parent.left right: parent.right verticalCenter: parent.verticalCenter } text: contactInfo.contact.nickname horizontalAlignment: nickname.horizontalAlignment font.pointSize: nickname.font.pointSize onEditingFinished: { contactInfo.contact.nickname = text nickname.renameMode = false } } } } Item { height: 1; width: 1 } ContactIDField { Layout.fillWidth: true Layout.minimumWidth: 100 readOnly: true text: visible ? contactInfo.contact.contactID : "" } GridLayout { Layout.fillWidth: true columns: 2 Label { text: qsTr("Date added:"); Layout.alignment: Qt.AlignRight } Label { Layout.fillWidth: true elide: Text.ElideRight text: visible ? Qt.formatDate(contactInfo.contact.settings.read("whenCreated"), Qt.DefaultLocaleLongDate) : "" textFormat: Text.PlainText } Label { text: qsTr("Last seen:"); visible: lastSeen.visible; Layout.alignment: Qt.AlignRight } Label { id: lastSeen Layout.fillWidth: true elide: Text.ElideRight visible: contactInfo.request === null text: visible ? Qt.formatDate(contactInfo.contact.settings.read("lastConnected"), Qt.DefaultLocaleLongDate) : "" textFormat: Text.PlainText } Label { text: qsTr("Request:"); visible: requestStatus.visible; Layout.alignment: Qt.AlignRight } Label { id: requestStatus visible: contactInfo.request !== null textFormat: Text.PlainText text: { var re = "" if (contactInfo.request === null) return re switch (contactInfo.request.status) { case OutgoingContactRequest.Pending: re = qsTr("Pending connection"); break case OutgoingContactRequest.Acknowledged: re = qsTr("Delivered"); break case OutgoingContactRequest.Accepted: re = qsTr("Accepted"); break case OutgoingContactRequest.Error: re = qsTr("Error"); break case OutgoingContactRequest.Rejected: re = qsTr("Rejected"); break } if (contactInfo.request.isConnected) { //: %1 status, e.g. "Accepted" re = qsTr("%1 (Connected)").arg(re) } return re } } Label { text: qsTr("Response:"); visible: rejectMessage.visible; Layout.alignment: Qt.AlignRight } Label { id: rejectMessage Layout.fillWidth: true elide: Text.ElideRight text: visible ? contactInfo.request.rejectMessage : "" textFormat: Text.PlainText visible: (contactInfo.request !== null) && (contactInfo.request.rejectMessage !== "") } } Item { height: 1; width: 1 } Rectangle { color: palette.mid height: 1 Layout.fillWidth: true } Item { height: 1; width: 1 } RowLayout { Layout.fillWidth: true Button { text: qsTr("Rename") onClicked: nickname.renameMode = !nickname.renameMode } Item { Layout.fillWidth: true; height: 1 } Button { text: qsTr("Remove") onClicked: contactActions.removeContact() } } Item { Layout.fillHeight: true width: 1 } } } } ricochet-1.1.4/src/ui/qml/ContactRequestDialog.qml000066400000000000000000000052171300720305500221110ustar00rootroot00000000000000import QtQuick 2.2 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 ApplicationWindow { id: contactRequestDialog width: 350 height: 200 minimumWidth: width maximumWidth: width minimumHeight: height maximumHeight: height flags: styleHelper.dialogWindowFlags modality: Qt.WindowModal title: mainWindow.title signal closed onVisibleChanged: if (!visible) closed() property QtObject request property bool hasValidContact: request.hostname != "" && fields.name.text.length function close() { visible = false } function accept() { request.nickname = fields.name.text request.accept() close() } function reject() { request.reject() close() } GridLayout { id: infoArea anchors { left: parent.left right: parent.right top: parent.top topMargin: 8 leftMargin: 16 rightMargin: 16 } columns: 2 Label { Layout.columnSpan: 2 Layout.fillWidth: true horizontalAlignment: Qt.AlignHCenter wrapMode: Text.Wrap text: qsTr("Someone new is asking to connect to you") } Item { height: 1 } Rectangle { color: palette.mid height: 1 Layout.fillWidth: true Layout.columnSpan: 2 } Item { height: 1 } } ContactRequestFields { id: fields anchors { left: parent.left right: parent.right top: infoArea.bottom bottom: buttonRow.top margins: 8 leftMargin: 16 rightMargin: 16 } readOnly: true Component.onCompleted: { contactId.text = request.contactId name.text = request.nickname name.readOnly = false name.focus = true message.text = request.message } } RowLayout { id: buttonRow anchors { right: parent.right bottom: parent.bottom rightMargin: 16 bottomMargin: 8 } Button { text: qsTr("Reject") onClicked: contactRequestDialog.reject() } Button { text: qsTr("Accept") enabled: hasValidContact onClicked: contactRequestDialog.accept() } } Action { shortcut: StandardKey.Close onTriggered: contactRequestDialog.close() } Action { shortcut: "Escape" onTriggered: contactRequestDialog.close() } } ricochet-1.1.4/src/ui/qml/ContactRequestFields.qml000066400000000000000000000022361300720305500221160ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 import im.ricochet 1.0 GridLayout { id: contactFields columns: 2 property bool readOnly property ContactIDField contactId: contactIdField property TextField name: nameField property TextArea message: messageField property bool hasValidRequest: contactIdField.acceptableInput && nameField.text.length Label { text: qsTr("ID:") Layout.alignment: Qt.AlignVCenter | Qt.AlignRight } ContactIDField { id: contactIdField Layout.fillWidth: true readOnly: contactFields.readOnly showCopyButton: false } Label { text: qsTr("Name:") Layout.alignment: Qt.AlignVCenter | Qt.AlignRight } TextField { id: nameField Layout.fillWidth: true readOnly: contactFields.readOnly } Label { text: qsTr("Message:") Layout.alignment: Qt.AlignTop | Qt.AlignRight } TextArea { id: messageField Layout.fillWidth: true Layout.fillHeight: true textFormat: TextEdit.PlainText readOnly: contactFields.readOnly } } ricochet-1.1.4/src/ui/qml/ContactWindow.js000066400000000000000000000010011300720305500204160ustar00rootroot00000000000000.pragma library var windows = { } var createWindow = function() { console.log("BUG!") } function getWindow(user) { var id = user.uniqueID var window = windows[user.uniqueID] if (window === undefined || window === null) { window = createWindow(user) window.closed.connect(function() { windows[id] = undefined }) windows[id] = window } return window } function windowExists(user) { return windows[user.uniqueID] !== undefined && windows[user.uniqueID] !== null } ricochet-1.1.4/src/ui/qml/GeneralPreferences.qml000066400000000000000000000046741300720305500215720ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 import im.ricochet 1.0 ColumnLayout { anchors { fill: parent margins: 8 } CheckBox { text: qsTr("Use a single window for conversations") checked: uiSettings.data.combinedChatWindow || false onCheckedChanged: { uiSettings.write("combinedChatWindow", checked) } } CheckBox { text: qsTr("Open links in default browser without prompting") checked: uiSettings.data.alwaysOpenBrowser || false onCheckedChanged: { uiSettings.write("alwaysOpenBrowser", checked) } } CheckBox { text: qsTr("Play audio notifications") checked: uiSettings.data.playAudioNotification || false onCheckedChanged: { uiSettings.write("playAudioNotification", checked) } } RowLayout { Item { width: 16 } Label { text: qsTr("Volume") } Slider { maximumValue: 1.0 updateValueWhileDragging: false enabled: uiSettings.data.playAudioNotification || false value: uiSettings.read("notificationVolume", 0.75) onValueChanged: { uiSettings.write("notificationVolume", value) } } } RowLayout { z: 2 Label { text: qsTr("Language") } ComboBox { id: languageBox model: languageModel textRole: "nativeName" currentIndex: languageModel.rowForLocaleID(uiSettings.data.language) Layout.minimumWidth: 200 LanguagesModel { id: languageModel } onActivated: { var localeID = languageModel.localeID(index) uiSettings.write("language", localeID) restartBubble.displayed = true bubbleResetTimer.start() } Bubble { id: restartBubble target: languageBox text: qsTr("Restart Ricochet to apply changes") displayed: false horizontalAlignment: Qt.AlignRight Timer { id: bubbleResetTimer interval: 3000 onTriggered: restartBubble.displayed = false } } } } Item { Layout.fillHeight: true Layout.fillWidth: true } } ricochet-1.1.4/src/ui/qml/MainToolBar.qml000066400000000000000000000043421300720305500201720ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 import QtQuick.Controls.Styles 1.0 import im.ricochet 1.0 ToolBar { Layout.minimumWidth: 200 Layout.fillWidth: true // Necessary to avoid oversized toolbars, e.g. OS X with Qt 5.4.1 implicitHeight: toolBarLayout.height + __style.padding.top + __style.padding.bottom property Action addContact: addContactAction property Action preferences: preferencesAction data: [ Action { id: addContactAction text: qsTr("Add Contact") onTriggered: { var object = createDialog("AddContactDialog.qml", { }, window) object.visible = true } }, Action { id: preferencesAction text: qsTr("Preferences") onTriggered: root.openPreferences() } ] Component { id: iconButtonStyle ButtonStyle { background: Item { } label: Text { text: control.text font.family: iconFont.name font.pixelSize: height horizontalAlignment: Qt.AlignHCenter renderType: Text.QtRendering color: "black" } } } RowLayout { id: toolBarLayout width: parent.width TorStateWidget { anchors.verticalCenter: parent.verticalCenter } Item { Layout.fillWidth: true height: 1 } ToolButton { id: addContactButton implicitHeight: 24 action: addContactAction style: iconButtonStyle text: "\ue810" // iconFont plus symbol Loader { id: emptyState active: contactList.view.count == 0 sourceComponent: Bubble { target: addContactButton maximumWidth: toolBarLayout.width text: qsTr("Click to add contacts") } } } ToolButton { action: preferencesAction implicitHeight: 24 style: iconButtonStyle text: "\ue803" // iconFont gear } } } ricochet-1.1.4/src/ui/qml/MainWindow.qml000066400000000000000000000107701300720305500201010ustar00rootroot00000000000000import QtQuick 2.2 import QtQuick.Window 2.0 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 import im.ricochet 1.0 import "ContactWindow.js" as ContactWindow ApplicationWindow { id: window title: "Ricochet" visibility: Window.AutomaticVisibility width: 250 height: 400 minimumHeight: 400 minimumWidth: uiSettings.data.combinedChatWindow ? 650 : 250 maximumWidth: uiSettings.data.combinedChatWindow ? (1 << 24) - 1 : 250 onMinimumWidthChanged: width = Math.max(width, minimumWidth) onMaximumWidthChanged: width = Math.min(width, maximumWidth) // OS X Menu Loader { active: Qt.platform.os == 'osx' sourceComponent: MenuBar { Menu { title: "Ricochet" MenuItem { text: qsTranslate("QCocoaMenuItem", "Preference") onTriggered: toolBar.preferences.trigger() } } } } Connections { target: userIdentity.contacts onUnreadCountChanged: { if (unreadCount > 0) { if (audioNotifications !== null) audioNotifications.message.play() var w = window if (!uiSettings.data.combinedChatWindow || ContactWindow.windowExists(user)) w = ContactWindow.getWindow(user) // On OS X, avoid bouncing the dock icon forever w.alert(Qt.platform.os == "osx" ? 1000 : 0) } } onContactStatusChanged: { if (status === ContactUser.Online && audioNotifications !== null) { audioNotifications.contactOnline.play() } } } RowLayout { anchors.fill: parent spacing: 0 ColumnLayout { spacing: 0 Layout.preferredWidth: combinedChatView.visible ? 220 : 0 Layout.fillWidth: !combinedChatView.visible MainToolBar { id: toolBar // Needed to allow bubble to appear over contact list z: 3 } Item { Layout.fillHeight: true Layout.fillWidth: true ContactList { id: contactList anchors.fill: parent opacity: offlineLoader.item !== null ? (1 - offlineLoader.item.opacity) : 1 onContactActivated: { if (contact.status === ContactUser.RequestPending || contact.status === ContactUser.RequestRejected) { actions.openPreferences() } else if (!uiSettings.data.combinedChatWindow) { actions.openWindow() } } } Loader { id: offlineLoader active: torControl.torStatus !== TorControl.TorReady || (item !== null && item.visible) anchors.fill: parent source: Qt.resolvedUrl("OfflineStateItem.qml") } } } Rectangle { visible: combinedChatView.visible width: 1 Layout.fillHeight: true color: Qt.darker(palette.window, 1.5) } PageView { id: combinedChatView visible: uiSettings.data.combinedChatWindow || false Layout.fillWidth: true Layout.fillHeight: true property QtObject currentContact: (visible && width > 0) ? contactList.selectedContact : null onCurrentContactChanged: { if (currentContact !== null) { show(currentContact.uniqueID, Qt.resolvedUrl("ChatPage.qml"), { 'contact': currentContact }); } else { currentKey = "" } } } } property bool inactive: true onActiveFocusItemChanged: { // Focus current page when window regains focus if (activeFocusItem !== null && inactive) { inactive = false retakeFocus.start() } else if (activeFocusItem === null) { inactive = true } } Timer { id: retakeFocus interval: 1 onTriggered: { if (combinedChatView.currentPage !== null) combinedChatView.currentPage.forceActiveFocus() } } Action { shortcut: StandardKey.Close onTriggered: window.close() } } ricochet-1.1.4/src/ui/qml/MessageDelegate.qml000066400000000000000000000143521300720305500210440ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import im.ricochet 1.0 Column { id: delegate width: parent.width Loader { active: { if (model.section === "offline") return true // either this is the first message, or the message was a long time ago.. if ((model.timespan === -1 || model.timespan > 3600 /* one hour */)) return true return false } sourceComponent: Label { //: %1 nickname text: { if (model.section === "offline") return qsTr("%1 is offline").arg(contact !== null ? contact.nickname : "") else return Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleShortDate) } textFormat: Text.PlainText width: background.parent.width elide: Text.ElideRight horizontalAlignment: Qt.AlignHCenter color: palette.mid Rectangle { id: line width: (parent.width - parent.contentWidth) / 2 - 4 height: 1 y: (parent.height - 1) / 2 color: Qt.lighter(palette.mid, 1.4) } Rectangle { width: line.width height: 1 y: line.y x: parent.width - width color: line.color } } } Rectangle { id: background width: Math.max(30, textField.width + 12) height: textField.height + 12 x: model.isOutgoing ? parent.width - width - 11 : 10 property int __maxWidth: parent.width * 0.8 color: (model.status === ConversationModel.Error) ? "#ffdcc4" : ( model.isOutgoing ? "#eaeced" : "#c4e7ff" ) Behavior on color { ColorAnimation { } } Rectangle { rotation: 45 width: 10 height: 10 x: model.isOutgoing ? parent.width - 20 : 10 y: model.isOutgoing ? parent.height - 5 : -5 color: parent.color } Rectangle { anchors.fill: parent anchors.margins: 1 opacity: (model.status === ConversationModel.Sending || model.status === ConversationModel.Queued || model.status === ConversationModel.Error) ? 1 : 0 visible: opacity > 0 color: Qt.lighter(parent.color, 1.15) Behavior on opacity { NumberAnimation { } } } TextEdit { id: textField width: Math.min(implicitWidth, background.__maxWidth) height: contentHeight x: Math.round((parent.width - width) / 2) y: 6 renderType: Text.NativeRendering textFormat: TextEdit.RichText selectionColor: palette.highlight selectedTextColor: palette.highlightedText font.pointSize: styleHelper.pointSize wrapMode: TextEdit.Wrap readOnly: true selectByMouse: true text: LinkedText.parsed(model.text) onLinkActivated: { textField.deselect() delegate.showContextMenu(link) } // Workaround an incomplete fix for QTBUG-31646 Component.onCompleted: { if (textField.hasOwnProperty('linkHovered')) textField.linkHovered.connect(function() { }) } MouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton onClicked: delegate.showContextMenu(parent.hoveredLink) } } } function showContextMenu(link) { var object = contextMenu.createObject(delegate, (link !== undefined) ? { 'hoveredLink': link } : { }) // XXX QtQuickControls private API. The only other option is 'visible', and it is not reliable. See PR#183 object.popupVisibleChanged.connect(function() { if (!object.__popupVisible) object.destroy(1000) }) object.popup() } Component { id: contextMenu Menu { property string hoveredLink: textField.hasOwnProperty('hoveredLink') ? textField.hoveredLink : "" MenuItem { text: linkAddContact.visible ? qsTr("Copy ID") : qsTr("Copy Link") visible: hoveredLink.length > 0 onTriggered: LinkedText.copyToClipboard(hoveredLink) } MenuItem { text: qsTr("Open with Browser") visible: hoveredLink.length > 0 && hoveredLink.substr(0,4).toLowerCase() == "http" onTriggered: { if (uiSettings.data.alwaysOpenBrowser || contact.settings.data.alwaysOpenBrowser) { Qt.openUrlExternally(hoveredLink) } else { var window = uiMain.findParentWindow(delegate) var object = createDialog("OpenBrowserDialog.qml", { 'link': hoveredLink, 'contact': contact }, window) object.visible = true } } } MenuItem { id: linkAddContact text: qsTr("Add as Contact") visible: hoveredLink.length > 0 && (hoveredLink.substr(0,9).toLowerCase() == "ricochet:" || hoveredLink.substr(0,8).toLowerCase() == "torsion:") onTriggered: { var object = createDialog("AddContactDialog.qml", { 'staticContactId': hoveredLink }, chatWindow) object.visible = true } } MenuSeparator { visible: hoveredLink.length > 0 } MenuItem { text: qsTr("Copy Message") visible: textField.selectedText.length == 0 onTriggered: { LinkedText.copyToClipboard(textField.getText(0, textField.length)) } } MenuItem { text: qsTr("Copy Selection") visible: textField.selectedText.length > 0 shortcut: "Ctrl+C" onTriggered: textField.copy() } } } } ricochet-1.1.4/src/ui/qml/MessageDialogWrapper.qml000066400000000000000000000010761300720305500220710ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Dialogs 1.1 import "utils.js" as Utils MessageDialog { id: removeContactDialog title: qsTr("Remove %1").arg(Utils.htmlEscaped(contact.nickname)) //: %1 nickname text: qsTr("Do you want to permanently remove %1?").arg(Utils.htmlEscaped(contact.nickname)) informativeText: qsTr("This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request.") standardButtons: StandardButton.Yes | StandardButton.No onYes: contact.deleteContact() } ricochet-1.1.4/src/ui/qml/NetworkSetupWizard.qml000066400000000000000000000067571300720305500216720ustar00rootroot00000000000000import QtQuick 2.2 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 ApplicationWindow { id: window width: minimumWidth height: minimumHeight minimumWidth: 400 maximumWidth: minimumWidth minimumHeight: visibleItem.height + 16 maximumHeight: minimumHeight title: "Ricochet" signal networkReady signal closed onVisibleChanged: if (!visible) closed() property Item visibleItem: configPage.visible ? configPage : pageLoader.item function back() { if (pageLoader.visible) { pageLoader.visible = false configPage.visible = true } else { openBeginning() } } function openBeginning() { configPage.visible = false configPage.reset() pageLoader.sourceComponent = firstPage pageLoader.visible = true } function openConfig() { pageLoader.visible = false configPage.visible = true } function openBootstrap() { configPage.visible = false pageLoader.source = Qt.resolvedUrl("TorBootstrapStatus.qml") pageLoader.visible = true } Loader { id: pageLoader anchors { top: parent.top left: parent.left right: parent.right margins: 8 } sourceComponent: firstPage } TorConfigurationPage { id: configPage anchors { top: parent.top left: parent.left right: parent.right margins: 8 } visible: false } StartupStatusPage { id: statusPage anchors { top: parent.top left: parent.left right: parent.right margins: 8 } visible: false onHasErrorChanged: { if (hasError) { if (visibleItem) visibleItem.visible = false pageLoader.visible = false statusPage.visible = true visibleItem = statusPage } } } Component { id: firstPage Column { spacing: 8 Label { width: parent.width text: qsTr("This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network.") wrapMode: Text.Wrap horizontalAlignment: Qt.AlignHCenter } Button { anchors.horizontalCenter: parent.horizontalCenter text: qsTr("Connect") isDefault: true onClicked: { // Reset to defaults and proceed to bootstrap page configPage.reset() configPage.save() } } Rectangle { height: 1 width: parent.width color: palette.mid } Label { width: parent.width text: qsTr("This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings.") wrapMode: Text.Wrap horizontalAlignment: Qt.AlignHCenter } Button { anchors.horizontalCenter: parent.horizontalCenter text: qsTr("Configure") onClicked: window.openConfig() } } } Action { shortcut: StandardKey.Close onTriggered: window.close() } } ricochet-1.1.4/src/ui/qml/OfflineStateItem.qml000066400000000000000000000134031300720305500212230ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 import im.ricochet 1.0 MouseArea { id: offlineState acceptedButtons: Qt.LeftButton | Qt.RightButton visible: opacity > 0 enabled: visible opacity: 0 clip: true Behavior on opacity { NumberAnimation { duration: 500 } } Rectangle { anchors.fill: parent color: palette.base } Label { id: label anchors { horizontalCenter: parent.horizontalCenter verticalCenter: parent.verticalCenter verticalCenterOffset: parent.height / -3 } font.pointSize: 14 } Rectangle { id: indicator width: label.width anchors { top: label.bottom topMargin: 2 } height: 2 x: label.x onWidthChanged: if (indicatorAnimation.running) indicatorAnimation.restart() property alias running: indicatorAnimation.running SequentialAnimation { id: indicatorAnimation function restart() { stop() animation1.to = offlineState.width animation2.from = -indicator.width animation2.to = offlineState.width start() } NumberAnimation { id: animation1 target: indicator property: "x" to: offlineState.width duration: 500 easing.type: Easing.InQuad } NumberAnimation { id: animation2 loops: Animation.Infinite target: indicator property: "x" from: -indicator.width to: offlineState.width duration: 1500 easing.type: Easing.OutInQuad } } } onWidthChanged: if (indicatorAnimation.running) indicatorAnimation.restart() Label { id: detailedLabel anchors { left: parent.left right: parent.right top: indicator.bottom margins: 16 } wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter color: Qt.lighter(palette.text, 1.2) font.pointSize: 11 text: torControl.errorMessage } GridLayout { id: buttonRow visible: false anchors { left: parent.left right: parent.right top: detailedLabel.bottom margins: 16 topMargin: 32 } Button { Layout.alignment: Qt.AlignRight | Qt.AlignVCenter text: qsTr("Configure") onClicked: { var object = createDialog("NetworkSetupWizard.qml", { }, window) object.visible = true } } Button { Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter text: qsTr("Details") onClicked: { openPreferences("TorPreferences.qml") } } } states: [ State { name: "connected" when: torControl.torStatus === TorControl.TorReady PropertyChanges { target: offlineState opacity: 0 } }, State { name: "failed" when: torControl.status === TorControl.Error PropertyChanges { target: offlineState opacity: 1 } PropertyChanges { target: label text: qsTr("Connection failed") } PropertyChanges { target: indicator color: "#ffdcc4" running: false } PropertyChanges { target: buttonRow visible: true } }, State { name: "connecting" when: torControl.torStatus !== TorControl.TorReady PropertyChanges { target: offlineState opacity: 1 } PropertyChanges { target: label //: \u2026 is ellipsis text: qsTr("Connecting\u2026") } PropertyChanges { target: indicator color: "#c4e7ff" running: true x: label.x } } ] transitions: [ Transition { to: "connecting" SequentialAnimation { PropertyAction { target: label property: "text" } PropertyAction { target: indicator property: "running" } ColorAnimation { target: indicator property: "color" duration: 1000 } } }, Transition { to: "failed" SequentialAnimation { PropertyAction { target: indicator property: "running" } PropertyAction { target: label property: "text" } ParallelAnimation { NumberAnimation { target: indicator property: "x" duration: 1000 easing.type: Easing.OutQuad } ColorAnimation { target: indicator property: "color" duration: 1000 } } } } ] } ricochet-1.1.4/src/ui/qml/OpenBrowserDialog.qml000066400000000000000000000051721300720305500214120ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 import im.ricochet 1.0 import "utils.js" as Utils ApplicationWindow { id: dialog width: 400 height: layout.height + 32 minimumWidth: width maximumWidth: width minimumHeight: height maximumHeight: height flags: styleHelper.dialogWindowFlags modality: Qt.WindowModal title: mainWindow.title signal closed onVisibleChanged: if (!visible) closed() function close() { visible = false } property string link property QtObject contact ColumnLayout { id: layout focus: true spacing: 8 anchors { left: parent.left right: parent.right top: parent.top margins: 16 } Label { Layout.fillWidth: true text: qsTr("Warning! Opening links with your default browser will harm your security and anonymity.

You can copy to the clipboard instead.") wrapMode: Text.Wrap horizontalAlignment: Qt.AlignHCenter onLinkActivated: { LinkedText.copyToClipboard(dialog.link) dialog.close() } } Item { width: 1; height: 1 } Rectangle { height: 1 Layout.fillWidth: true color: Qt.darker(palette.window, 1.5) } CheckBox { id: alwaysOpenContact text: qsTr("Don't ask again for links from %1").arg(contact ? Utils.htmlEscaped(contact.nickname) : "???") checked: contact.settings.data.alwaysOpenBrowser || false } CheckBox { id: alwaysOpenAll text: qsTr("Don't ask again for any links (not recommended!)") checked: uiSettings.data.alwaysOpenBrowser || false } RowLayout { width: parent.width Button { text: qsTr("Open Browser") onClicked: { if (alwaysOpenContact.checked) contact.settings.write("alwaysOpenBrowser", true) if (alwaysOpenAll.checked) uiSettings.write("alwaysOpenBrowser", true) Qt.openUrlExternally(link) dialog.close() } } Item { Layout.fillWidth: true; height: 1 } Button { text: qsTr("Cancel") isDefault: true onClicked: dialog.close() } } Keys.onEscapePressed: dialog.close() Keys.onReturnPressed: dialog.close() } } ricochet-1.1.4/src/ui/qml/PageView.qml000066400000000000000000000032641300720305500175340ustar00rootroot00000000000000import QtQuick 2.0 /* Simple QML view that associates a string key with a page, and * displays one page at a time. */ FocusScope { property Item currentPage property string currentKey property var _items: { '': null } function add(key, source, properties) { if (key === "") return if (_items[key] !== null) remove(key) var component = Qt.createComponent(source, content) if (component.status !== Component.Ready) { console.log("PageView:", source, component.errorString()) return } if (properties === undefined) properties = [ ] properties['visible'] = false properties['anchors.fill'] = content var item = component.createObject(content, properties) _items[key] = item } function show(key, source, properties) { if (_items[key] === undefined) add(key, source, properties) currentKey = key } function remove(key) { var item = _items[key] if (item !== undefined) { if (item === currentPage) currentKey = null _items[key] = undefined item.destroy() } } onCurrentKeyChanged: { var item = _items[currentKey] if (item === currentPage) return if (currentPage !== null) { currentPage.visible = false currentPage.focus = false } currentPage = item || null if (currentPage !== null) { currentPage.visible = true currentPage.focus = true } } Item { id: content anchors.fill: parent } } ricochet-1.1.4/src/ui/qml/PreferencesDialog.qml000066400000000000000000000032221300720305500214000ustar00rootroot00000000000000import QtQuick 2.2 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 import im.ricochet 1.0 ApplicationWindow { id: preferencesWindow width: 550 minimumWidth: 550 height: 400 minimumHeight: 400 title: qsTr("Ricochet Preferences") signal closed onVisibleChanged: if (!visible) closed() property string initialPage property var initialPageProperties: { } Component.onCompleted: { if (initialPage != "") { initialPage = Qt.resolvedUrl(initialPage) for (var i = 0; i < tabs.count; i++) { if (tabs.getTab(i).source == initialPage) { tabs.currentIndex = i var item = tabs.getTab(i).item for (var key in initialPageProperties) { item[key] = initialPageProperties[key] } } } } } TabView { id: tabs anchors.fill: parent anchors.margins: 8 Tab { title: qsTr("General") source: Qt.resolvedUrl("GeneralPreferences.qml") } Tab { title: qsTr("Contacts") source: Qt.resolvedUrl("ContactPreferences.qml") } Tab { title: qsTr("Tor") source: Qt.resolvedUrl("TorPreferences.qml") } Tab { title: qsTr("About") source: Qt.resolvedUrl("AboutPreferences.qml") } } Action { shortcut: StandardKey.Close onTriggered: preferencesWindow.close() } Action { shortcut: "Escape" onTriggered: preferencesWindow.close() } } ricochet-1.1.4/src/ui/qml/PresenceIcon.qml000066400000000000000000000005451300720305500204010ustar00rootroot00000000000000import QtQuick 2.0 import im.ricochet 1.0 Rectangle { id: presenceIcon width: 10 height: 10 radius: 360 property int status: -1 onStatusChanged: { if (status === ContactUser.Online) color = "#3EBB4F" else color = "#999999" } Behavior on color { ColorAnimation { } } } ricochet-1.1.4/src/ui/qml/StartupStatusPage.qml000066400000000000000000000020641300720305500214650ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 Column { id: statusPage spacing: 8 property bool hasError: torInstance.hasError Label { anchors { left: parent.left right: parent.right margins: 8 } text: qsTr("The Tor process was not started successfully. This is most likely an installation or system error.") font.bold: true wrapMode: Text.Wrap } Label { anchors { left: parent.left right: parent.right margins: 8 } text: torInstance.errorMessage wrapMode: Text.Wrap } TorLogDisplay { id: logDisplay width: parent.width height: text.length > 0 ? 300 : 0 } RowLayout { anchors { left: parent.left right: parent.right margins: 8 } Item { height: 1; Layout.fillWidth: true } Button { text: qsTr("Quit") onClicked: Qt.quit() } } } ricochet-1.1.4/src/ui/qml/TorBootstrapStatus.qml000066400000000000000000000033661300720305500216760ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 Column { id: page spacing: 8 property var bootstrap: torControl.bootstrapStatus onBootstrapChanged: { if (bootstrap['tag'] === "done") window.networkReady() } Label { //: \u2026 is ellipsis text: qsTr("Connecting to the Tor network\u2026") font.bold: true } ProgressBar { width: parent.width maximumValue: 100 indeterminate: bootstrap.progress === undefined value: bootstrap.progress === undefined ? 0 : bootstrap.progress } Label { text: (bootstrap['warning'] !== undefined ) ? bootstrap['warning'] : bootstrap['summary'] textFormat: Text.PlainText } TorLogDisplay { id: logDisplay width: parent.width height: 0 visible: height > 0 Behavior on height { SmoothedAnimation { easing.type: Easing.InOutQuad velocity: 1500 } } } RowLayout { width: parent.width Button { text: qsTr("Back") onClicked: window.back() } Item { height: 1; Layout.fillWidth: true } Button { text: logDisplay.height ? qsTr("Hide details") : qsTr("Show details") onClicked: { if (logDisplay.height) logDisplay.height = 0 else logDisplay.height = 300 } } Item { height: 1; Layout.fillWidth: true } Button { text: qsTr("Done") isDefault: true enabled: bootstrap.tag === "done" onClicked: window.visible = false } } } ricochet-1.1.4/src/ui/qml/TorConfigurationPage.qml000066400000000000000000000155131300720305500221160ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 Column { id: setup spacing: 8 property alias proxyType: proxyTypeField.selectedType property alias proxyAddress: proxyAddressField.text property alias proxyPort: proxyPortField.text property alias proxyUsername: proxyUsernameField.text property alias proxyPassword: proxyPasswordField.text property alias allowedPorts: allowedPortsField.text property alias bridges: bridgesField.text function reset() { proxyTypeField.currentIndex = 0 proxyAddress = '' proxyPort = '' proxyUsername = '' proxyPassword = '' allowedPorts = '' bridges = '' } function save() { // null value is reset var conf = { 'Socks4Proxy': null, 'Socks5Proxy': null, 'Socks5ProxyUsername': null, 'Socks5ProxyPassword': null, 'HTTPSProxy': null, 'HTTPSProxyAuthenticator': null, 'FirewallPorts': null, 'FascistFirewall': null, 'Bridge': null, 'UseBridges': null, 'DisableNetwork': '0' } if (proxyType === "socks4") { conf['Socks4Proxy'] = proxyAddress + ":" + proxyPort } else if (proxyType === "socks5") { conf['Socks5Proxy'] = proxyAddress + ":" + proxyPort if (proxyUsername.length > 0) conf['Socks5ProxyUsername'] = proxyUsername if (proxyPassword.length > 0) conf['Socks5ProxyPassword'] = proxyPassword } else if (proxyType === "https") { conf['HTTPSProxy'] = proxyAddress + ":" + proxyPort if (proxyUsername.length > 0 || proxyPassword.length > 0) conf['HTTPSProxyAuthenticator'] = proxyUsername + ":" + proxyPassword } if (allowedPorts.length > 0) { conf['FirewallPorts'] = allowedPorts conf['FascistFirewall'] = "1" } if (bridges.length > 0) { conf['Bridge'] = bridges.split('\n') conf['UseBridges'] = "1" } var command = torControl.setConfiguration(conf) command.finished.connect(function() { if (command.successful) { if (torControl.hasOwnership) torControl.saveConfiguration() window.openBootstrap() } else console.log("SETCONF error:", command.errorMessage) }) } Label { width: parent.width text: qsTr("Does this computer need a proxy to access the internet?") wrapMode: Text.Wrap } GroupBox { width: setup.width GridLayout { anchors.fill: parent columns: 2 Label { text: qsTr("Proxy type:") color: proxyPalette.text } ComboBox { id: proxyTypeField property string none: qsTr("None") model: [ { "text": qsTr("None"), "type": "" }, { "text": "SOCKS 4", "type": "socks4" }, { "text": "SOCKS 5", "type": "socks5" }, { "text": "HTTPS", "type": "https" }, ] textRole: "text" property string selectedType: currentIndex >= 0 ? model[currentIndex].type : "" SystemPalette { id: proxyPalette colorGroup: setup.proxyType == "" ? SystemPalette.Disabled : SystemPalette.Active } } Label { text: qsTr("Address:") color: proxyPalette.text } RowLayout { Layout.fillWidth: true TextField { id: proxyAddressField Layout.fillWidth: true enabled: setup.proxyType placeholderText: qsTr("IP address or hostname") } Label { text: qsTr("Port:") color: proxyPalette.text } TextField { id: proxyPortField Layout.preferredWidth: 50 enabled: setup.proxyType } } Label { text: qsTr("Username:") color: proxyPalette.text } RowLayout { Layout.fillWidth: true TextField { id: proxyUsernameField Layout.fillWidth: true enabled: setup.proxyType placeholderText: qsTr("Optional") } Label { text: qsTr("Password:") color: proxyPalette.text } TextField { id: proxyPasswordField Layout.fillWidth: true enabled: setup.proxyType placeholderText: qsTr("Optional") } } } } Item { height: 4; width: 1 } Label { width: parent.width text: qsTr("Does this computer's Internet connection go through a firewall that only allows connections to certain ports?") wrapMode: Text.Wrap } GroupBox { width: parent.width // Workaround OS X visual bug height: Math.max(implicitHeight, 40) RowLayout { anchors.fill: parent Label { text: qsTr("Allowed ports:") } TextField { id: allowedPortsField Layout.fillWidth: true } Label { text: qsTr("Example: 80,443") SystemPalette { id: disabledPalette; colorGroup: SystemPalette.Disabled } color: disabledPalette.text } } } Item { height: 4; width: 1 } Label { width: parent.width text: qsTr("If this computer's Internet connection is censored, you will need to obtain and use bridge relays.") wrapMode: Text.Wrap } GroupBox { width: parent.width ColumnLayout { anchors.fill: parent Label { text: qsTr("Enter one or more bridge relays (one per line):") } TextArea { id: bridgesField Layout.fillWidth: true Layout.preferredHeight: allowedPortsField.height * 2 tabChangesFocus: true } } } RowLayout { width: parent.width Button { text: qsTr("Back") onClicked: window.back() } Item { height: 1; Layout.fillWidth: true } Button { text: qsTr("Connect") isDefault: true onClicked: { setup.save() } } } } ricochet-1.1.4/src/ui/qml/TorLogDisplay.qml000066400000000000000000000005601300720305500205550ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 TextArea { id: logDisplay readOnly: true text: torInstance.logMessages.join('\n') textFormat: TextEdit.PlainText wrapMode: TextEdit.Wrap Connections { target: torInstance.process onLogMessage: { logDisplay.append(message) } } } ricochet-1.1.4/src/ui/qml/TorPreferences.qml000066400000000000000000000061211300720305500207460ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 import im.ricochet 1.0 import "utils.js" as Utils Item { anchors.fill: parent property var bootstrap: torInstance.control.bootstrapStatus Column { id: info anchors { left: parent.left right: parent.right top: parent.top margins: 8 } spacing: 6 GridLayout { columns: 4 width: parent.width Label { text: qsTr("Running:") } Label { font.bold: true; Layout.fillWidth: true; text: (torInstance.process ? (torInstance.process.state == TorProcess.Ready ? qsTr("Yes") : qsTr("No")) : qsTr("External")) } Label { text: qsTr("Control connected:") } Label { font.bold: true; Layout.fillWidth: true; text: ((torInstance.control.status == TorControl.Connected) ? qsTr("Yes") : qsTr("No")) } Label { text: qsTr("Circuits established:") } Label { font.bold: true; text: ((torInstance.control.torStatus == TorControl.TorReady) ? qsTr("Yes") : qsTr("No")) } Label { text: qsTr("Hidden service:") } Label { font.bold: true; text: (userIdentity.isOnline ? qsTr("Online") : qsTr("Offline")) } Label { text: qsTr("Version:") } Label { font.bold: true; text: torControl.torVersion; textFormat: Text.PlainText } //Label { text: "Recommended:" } //Label { font.bold: true; text: "Unknown" } } Rectangle { width: parent.width height: 1 color: palette.mid } Label { text: bootstrap.summary visible: bootstrap.tag !== 'done' } ProgressBar { width: parent.width maximumValue: 100 indeterminate: bootstrap.progress === undefined value: bootstrap.progress === undefined ? 0 : bootstrap.progress visible: bootstrap.tag !== 'done' } Label { //: %1 is error message text: qsTr("Error: %1").arg(Utils.htmlEscaped(errorMessage)) visible: errorMessage != "" property string errorMessage: { if (torInstance.hasError) return torInstance.errorMessage else if (torInstance.control.errorMessage != "") return torInstance.control.errorMessage else if (bootstrap.warning !== undefined) return bootstrap.warning else return "" } } Button { text: qsTr("Configure") visible: torControl.hasOwnership onClicked: { var object = createDialog("NetworkSetupWizard.qml") object.visible = true } } } TorLogDisplay { anchors { left: parent.left right: parent.right top: info.bottom bottom: parent.bottom margins: 8 } visible: torInstance.process !== null } } ricochet-1.1.4/src/ui/qml/TorStateWidget.qml000066400000000000000000000023311300720305500207300ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 import im.ricochet 1.0 Label { text: { if (torControl.status === TorControl.Error) return qsTr("Connection failed") if (torControl.status < TorControl.Connected) { //: \u2026 is ellipsis return qsTr("Connecting\u2026") } if (torControl.torStatus === TorControl.TorUnknown || torControl.torStatus === TorControl.TorOffline) { var bootstrap = torControl.bootstrapStatus if (bootstrap['recommendation'] === 'warn') return qsTr("Connection failed") else if (bootstrap['progress'] === undefined) return qsTr("Connecting\u2026") else { //: %1 is progress percentage, e.g. 100 return qsTr("Connecting\u2026 (%1%)").arg(bootstrap['progress']) } } if (torControl.torStatus === TorControl.TorReady) { // Indicates whether we've verified that the hidden services is connectable if (userIdentity.isOnline) return qsTr("Online") else return qsTr("Connected") } } } ricochet-1.1.4/src/ui/qml/UnreadCountBadge.qml000066400000000000000000000007171300720305500211770ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 Rectangle { id: badge radius: 4 width: number.width ? Math.max(height, number.width + 4) : 0 height: number.height ? number.height + 4 : 0 color: "#d80000" property int value Label { id: number anchors.centerIn: parent font.pointSize: styleHelper.pointSize - 2 color: "white" font.bold: true text: value > 0 ? (value + "") : "" } } ricochet-1.1.4/src/ui/qml/dummy.qml000066400000000000000000000003201300720305500171460ustar00rootroot00000000000000// Unused dummy QML file to specify import dependencies // This isn't included in the build, but is read by qmlimportscanner for static builds. import QtQuick 2.0 import QtQuick.PrivateWidgets 1.0 Item { } ricochet-1.1.4/src/ui/qml/main.qml000066400000000000000000000116421300720305500167500ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 import QtQuick.Window 2.0 import im.ricochet 1.0 import "ContactWindow.js" as ContactWindow // Root non-graphical object providing window management and other logic. QtObject { id: root property MainWindow mainWindow: MainWindow { onVisibleChanged: if (!visible) Qt.quit() } function createDialog(component, properties, parent) { if (typeof(component) === "string") component = Qt.createComponent(component) if (component.status !== Component.Ready) console.log("openDialog:", component.errorString()) var object = component.createObject(parent ? parent : null, (properties !== undefined) ? properties : { }) if (!object) console.log("openDialog:", component.errorString()) object.closed.connect(function() { object.destroy() }) return object } property QtObject preferencesDialog function openPreferences(page, properties) { if (preferencesDialog == null) { preferencesDialog = createDialog("PreferencesDialog.qml", { 'initialPage': page, 'initialPageProperties': properties } ) preferencesDialog.closed.connect(function() { preferencesDialog = null }) } preferencesDialog.visible = true preferencesDialog.raise() preferencesDialog.requestActivate() } property QtObject audioNotifications: audioNotificationLoader.item Component.onCompleted: { ContactWindow.createWindow = function(user) { var re = createDialog("ChatWindow.qml", { 'contact': user }) re.x = mainWindow.x + mainWindow.width + 10 re.y = mainWindow.y + (mainWindow.height / 2) - (re.height / 2) var screens = uiMain.screens if ((mainWindow.Screen !== undefined) && (mainWindow.Screen.name in screens)) { var currentScreen = screens[mainWindow.Screen.name] var offsetX = currentScreen.left var offsetY = currentScreen.top re.x = re.x - offsetX + re.width <= currentScreen.width ? re.x : mainWindow.x - re.width - 10 re.y = re.y - offsetY + re.height <= currentScreen.height ? re.y : currentScreen.height + offsetY - re.height - 10 } re.visible = true return re } if (torInstance.configurationNeeded) { var object = createDialog("NetworkSetupWizard.qml") object.networkReady.connect(function() { mainWindow.visible = true object.visible = false }) object.visible = true } else { mainWindow.visible = true } } property list data: [ Connections { target: userIdentity.contacts.incomingRequests onRequestAdded: { var object = createDialog("ContactRequestDialog.qml", { 'request': request }) object.visible = true } }, Connections { target: torInstance onConfigurationNeededChanged: { if (torInstance.configurationNeeded) { var object = createDialog("NetworkSetupWizard.qml", { 'modality': Qt.ApplicationModal }, mainWindow) object.networkReady.connect(function() { object.visible = false }) object.visible = true } } }, Settings { id: uiSettings path: "ui" }, SystemPalette { id: palette }, FontLoader { id: iconFont source: "qrc:/icons/ricochet_icons.ttf" }, Item { id: styleHelper visible: false Label { id: fakeLabel } Label { id: fakeLabelSized; font.pointSize: styleHelper.pointSize > 0 ? styleHelper.pointSize : 1 } property int pointSize: (Qt.platform.os === "windows") ? 10 : fakeLabel.font.pointSize property int textHeight: fakeLabelSized.height property int dialogWindowFlags: Qt.Dialog | Qt.WindowSystemMenuHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint }, Timer { interval: 2000 running: true repeat: false onTriggered: { var pendingRequests = userIdentity.contacts.incomingRequests.requests for (var i = 0; i < pendingRequests.length; i++) { var object = createDialog("ContactRequestDialog.qml", { 'request': pendingRequests[i] }) object.visible = true } } }, Loader { id: audioNotificationLoader active: uiSettings.data.playAudioNotification || false source: "AudioNotifications.qml" } ] } ricochet-1.1.4/src/ui/qml/qml.qrc000066400000000000000000000030221300720305500166020ustar00rootroot00000000000000 main.qml AddContactDialog.qml ChatMessageArea.qml ChatWindow.qml ContactActions.qml ContactList.qml ContactListDelegate.qml MessageDelegate.qml PreferencesDialog.qml PresenceIcon.qml ContactRequestDialog.qml ContactRequestFields.qml TorStateWidget.qml ContactWindow.js NetworkSetupWizard.qml TorConfigurationPage.qml TorBootstrapStatus.qml TorLogDisplay.qml TorPreferences.qml ContactPreferences.qml ContactIDField.qml AboutPreferences.qml MessageDialogWrapper.qml StartupStatusPage.qml OfflineStateItem.qml MainWindow.qml OpenBrowserDialog.qml Bubble.qml PageView.qml ChatPage.qml MainToolBar.qml UnreadCountBadge.qml GeneralPreferences.qml AudioNotifications.qml utils.js ../../../LICENSE ricochet-1.1.4/src/ui/qml/utils.js000066400000000000000000000003751300720305500170100ustar00rootroot00000000000000.pragma library function htmlEscaped(str) { str = str.replace(/&/g, "&"); str = str.replace(//g, ">"); str = str.replace(/"/g, """); str = str.replace(/'/g, "'"); return str } ricochet-1.1.4/src/utils/000077500000000000000000000000001300720305500152375ustar00rootroot00000000000000ricochet-1.1.4/src/utils/CryptoKey.cpp000066400000000000000000000337211300720305500177020ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "CryptoKey.h" #include "SecureRNG.h" #include "Useful.h" #include #include #include #include #include #if OPENSSL_VERSION_NUMBER < 0x10100000L void RSA_get0_factors(const RSA *r, const BIGNUM **p, const BIGNUM **q) { *p = r->p; *q = r->q; } #define RSA_bits(o) (BN_num_bits((o)->n)) #endif void base32_encode(char *dest, unsigned destlen, const char *src, unsigned srclen); bool base32_decode(char *dest, unsigned destlen, const char *src, unsigned srclen); CryptoKey::CryptoKey() { } CryptoKey::~CryptoKey() { clear(); } CryptoKey::Data::~Data() { if (key) { RSA_free(key); key = 0; } } void CryptoKey::clear() { d = 0; } bool CryptoKey::loadFromData(const QByteArray &data, KeyType type, KeyFormat format) { RSA *key = NULL; clear(); if (data.isEmpty()) return false; if (format == PEM) { BIO *b = BIO_new_mem_buf((void*)data.constData(), -1); if (type == PrivateKey) key = PEM_read_bio_RSAPrivateKey(b, NULL, NULL, NULL); else key = PEM_read_bio_RSAPublicKey(b, NULL, NULL, NULL); BIO_free(b); } else if (format == DER) { const uchar *dp = reinterpret_cast(data.constData()); if (type == PrivateKey) key = d2i_RSAPrivateKey(NULL, &dp, data.size()); else key = d2i_RSAPublicKey(NULL, &dp, data.size()); } else { Q_UNREACHABLE(); } if (!key) { qWarning() << "Failed to parse" << (type == PrivateKey ? "private" : "public") << "key from data"; return false; } d = new Data(key); return true; } bool CryptoKey::loadFromFile(const QString &path, KeyType type, KeyFormat format) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open" << (type == PrivateKey ? "private" : "public") << "key from" << path << "-" << file.errorString(); return false; } QByteArray data = file.readAll(); file.close(); return loadFromData(data, type, format); } bool CryptoKey::isPrivate() const { if (!isLoaded()) { return false; } else { const BIGNUM *p, *q; RSA_get0_factors(d->key, &p, &q); return (p != 0); } } int CryptoKey::bits() const { return isLoaded() ? RSA_bits(d->key) : 0; } QByteArray CryptoKey::publicKeyDigest() const { if (!isLoaded()) return QByteArray(); QByteArray buf = encodedPublicKey(DER); QByteArray re(20, 0); bool ok = SHA1(reinterpret_cast(buf.constData()), buf.size(), reinterpret_cast(re.data())) != NULL; if (!ok) { qWarning() << "Failed to hash public key data for digest"; return QByteArray(); } return re; } QByteArray CryptoKey::encodedPublicKey(KeyFormat format) const { if (!isLoaded()) return QByteArray(); if (format == PEM) { BIO *b = BIO_new(BIO_s_mem()); if (!PEM_write_bio_RSAPublicKey(b, d->key)) { BUG() << "Failed to encode public key in PEM format"; BIO_free(b); return QByteArray(); } BUF_MEM *buf; BIO_get_mem_ptr(b, &buf); /* Close BIO, but don't free buf. */ (void)BIO_set_close(b, BIO_NOCLOSE); BIO_free(b); QByteArray re((const char *)buf->data, (int)buf->length); BUF_MEM_free(buf); return re; } else if (format == DER) { uchar *buf = NULL; int len = i2d_RSAPublicKey(d->key, &buf); if (len <= 0 || !buf) { BUG() << "Failed to encode public key in DER format"; return QByteArray(); } QByteArray re((const char*)buf, len); OPENSSL_free(buf); return re; } else { Q_UNREACHABLE(); } return QByteArray(); } QByteArray CryptoKey::encodedPrivateKey(KeyFormat format) const { if (!isLoaded() || !isPrivate()) return QByteArray(); if (format == PEM) { BIO *b = BIO_new(BIO_s_mem()); if (!PEM_write_bio_RSAPrivateKey(b, d->key, NULL, NULL, 0, NULL, NULL)) { BUG() << "Failed to encode private key in PEM format"; BIO_free(b); return QByteArray(); } BUF_MEM *buf; BIO_get_mem_ptr(b, &buf); /* Close BIO, but don't free buf. */ (void)BIO_set_close(b, BIO_NOCLOSE); BIO_free(b); QByteArray re((const char *)buf->data, (int)buf->length); BUF_MEM_free(buf); return re; } else if (format == DER) { uchar *buf = NULL; int len = i2d_RSAPrivateKey(d->key, &buf); if (len <= 0 || !buf) { BUG() << "Failed to encode private key in DER format"; return QByteArray(); } QByteArray re((const char*)buf, len); OPENSSL_free(buf); return re; } else { Q_UNREACHABLE(); } return QByteArray(); } QString CryptoKey::torServiceID() const { if (!isLoaded()) return QString(); QByteArray digest = publicKeyDigest(); if (digest.isNull()) return QString(); static const int hostnameDigestSize = 10; static const int hostnameEncodedSize = 16; QByteArray re(hostnameEncodedSize+1, 0); base32_encode(re.data(), re.size(), digest.constData(), hostnameDigestSize); // Chop extra null byte re.chop(1); return QString::fromLatin1(re); } QByteArray CryptoKey::signData(const QByteArray &data) const { QByteArray digest(32, 0); bool ok = SHA256(reinterpret_cast(data.constData()), data.size(), reinterpret_cast(digest.data())) != NULL; if (!ok) { qWarning() << "Digest for RSA signature failed"; return QByteArray(); } return signSHA256(digest); } QByteArray CryptoKey::signSHA256(const QByteArray &digest) const { if (!isPrivate()) return QByteArray(); QByteArray re(RSA_size(d->key), 0); unsigned sigsize = 0; int r = RSA_sign(NID_sha256, reinterpret_cast(digest.constData()), digest.size(), reinterpret_cast(re.data()), &sigsize, d->key); if (r != 1) { qWarning() << "RSA encryption failed when generating signature"; return QByteArray(); } re.truncate(sigsize); return re; } bool CryptoKey::verifyData(const QByteArray &data, QByteArray signature) const { QByteArray digest(32, 0); bool ok = SHA256(reinterpret_cast(data.constData()), data.size(), reinterpret_cast(digest.data())) != NULL; if (!ok) { qWarning() << "Digest for RSA verify failed"; return false; } return verifySHA256(digest, signature); } bool CryptoKey::verifySHA256(const QByteArray &digest, QByteArray signature) const { if (!isLoaded()) return false; int r = RSA_verify(NID_sha256, reinterpret_cast(digest.constData()), digest.size(), reinterpret_cast(signature.data()), signature.size(), d->key); if (r != 1) return false; return true; } /* Cryptographic hash of a password as expected by Tor's HashedControlPassword */ QByteArray torControlHashedPassword(const QByteArray &password) { QByteArray salt = SecureRNG::random(8); if (salt.isNull()) return QByteArray(); int count = ((quint32)16 + (96 & 15)) << ((96 >> 4) + 6); SHA_CTX hash; SHA1_Init(&hash); QByteArray tmp = salt + password; while (count) { int c = qMin(count, tmp.size()); SHA1_Update(&hash, reinterpret_cast(tmp.constData()), c); count -= c; } unsigned char md[20]; SHA1_Final(md, &hash); /* 60 is the hex-encoded value of 96, which is a constant used by Tor's algorithm. */ return QByteArray("16:") + salt.toHex().toUpper() + QByteArray("60") + QByteArray::fromRawData(reinterpret_cast(md), 20).toHex().toUpper(); } /* Copyright (c) 2001-2004, Roger Dingledine * Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson * Copyright (c) 2007-2010, The Tor Project, Inc. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #define BASE32_CHARS "abcdefghijklmnopqrstuvwxyz234567" /* Implements base32 encoding as in rfc3548. Requires that srclen*8 is a multiple of 5. */ void base32_encode(char *dest, unsigned destlen, const char *src, unsigned srclen) { unsigned i, bit, v, u; unsigned nbits = srclen * 8; /* We need an even multiple of 5 bits, and enough space */ if ((nbits%5) != 0 || destlen > (nbits/5)+1) { Q_ASSERT(false); memset(dest, 0, destlen); return; } for (i = 0, bit = 0; bit < nbits; ++i, bit += 5) { /* set v to the 16-bit value starting at src[bits/8], 0-padded. */ v = ((quint8) src[bit / 8]) << 8; if (bit + 5 < nbits) v += (quint8) src[(bit/8)+1]; /* set u to the 5-bit value at the bit'th bit of src. */ u = (v >> (11 - (bit % 8))) & 0x1F; dest[i] = BASE32_CHARS[u]; } dest[i] = '\0'; } /* Implements base32 decoding as in rfc3548. Requires that srclen*5 is a multiple of 8. */ bool base32_decode(char *dest, unsigned destlen, const char *src, unsigned srclen) { unsigned int i, j, bit; unsigned nbits = srclen * 5; /* We need an even multiple of 8 bits, and enough space */ if ((nbits%8) != 0 || (nbits/8)+1 > destlen) { Q_ASSERT(false); return false; } char *tmp = new char[srclen]; /* Convert base32 encoded chars to the 5-bit values that they represent. */ for (j = 0; j < srclen; ++j) { if (src[j] > 0x60 && src[j] < 0x7B) tmp[j] = src[j] - 0x61; else if (src[j] > 0x31 && src[j] < 0x38) tmp[j] = src[j] - 0x18; else if (src[j] > 0x40 && src[j] < 0x5B) tmp[j] = src[j] - 0x41; else { delete[] tmp; return false; } } /* Assemble result byte-wise by applying five possible cases. */ for (i = 0, bit = 0; bit < nbits; ++i, bit += 8) { switch (bit % 40) { case 0: dest[i] = (((quint8)tmp[(bit/5)]) << 3) + (((quint8)tmp[(bit/5)+1]) >> 2); break; case 8: dest[i] = (((quint8)tmp[(bit/5)]) << 6) + (((quint8)tmp[(bit/5)+1]) << 1) + (((quint8)tmp[(bit/5)+2]) >> 4); break; case 16: dest[i] = (((quint8)tmp[(bit/5)]) << 4) + (((quint8)tmp[(bit/5)+1]) >> 1); break; case 24: dest[i] = (((quint8)tmp[(bit/5)]) << 7) + (((quint8)tmp[(bit/5)+1]) << 2) + (((quint8)tmp[(bit/5)+2]) >> 3); break; case 32: dest[i] = (((quint8)tmp[(bit/5)]) << 5) + ((quint8)tmp[(bit/5)+1]); break; } } delete[] tmp; return true; } ricochet-1.1.4/src/utils/CryptoKey.h000066400000000000000000000063701300720305500173470ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef CRYPTOKEY_H #define CRYPTOKEY_H #include #include #include class CryptoKey { public: enum KeyType { PrivateKey, PublicKey }; enum KeyFormat { PEM, DER }; CryptoKey(); CryptoKey(const CryptoKey &other) : d(other.d) { } ~CryptoKey(); bool loadFromData(const QByteArray &data, KeyType type, KeyFormat format = PEM); bool loadFromFile(const QString &path, KeyType type, KeyFormat format = PEM); void clear(); bool isLoaded() const { return d.data() && d->key != 0; } bool isPrivate() const; QByteArray publicKeyDigest() const; QByteArray encodedPublicKey(KeyFormat format) const; QByteArray encodedPrivateKey(KeyFormat format) const; QString torServiceID() const; int bits() const; // Calculate and sign SHA-256 digest of data using this key and PKCS #1 v2.0 padding QByteArray signData(const QByteArray &data) const; // Verify a signature as per signData bool verifyData(const QByteArray &data, QByteArray signature) const; // Sign the input SHA-256 digest using this key and PKCS #1 v2.0 padding QByteArray signSHA256(const QByteArray &digest) const; // Verify a signature as per signSHA256 bool verifySHA256(const QByteArray &digest, QByteArray signature) const; private: struct Data : public QSharedData { typedef struct rsa_st RSA; RSA *key; Data(RSA *k = 0) : key(k) { } ~Data(); }; QExplicitlySharedDataPointer d; }; QByteArray torControlHashedPassword(const QByteArray &password); #endif // CRYPTOKEY_H ricochet-1.1.4/src/utils/PendingOperation.cpp000066400000000000000000000051221300720305500212100ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "PendingOperation.h" PendingOperation::PendingOperation(QObject *parent) : QObject(parent), m_finished(false) { } bool PendingOperation::isFinished() const { return m_finished; } bool PendingOperation::isSuccess() const { return m_finished && m_errorMessage.isNull(); } bool PendingOperation::isError() const { return m_finished && !m_errorMessage.isNull(); } QString PendingOperation::errorMessage() const { return m_errorMessage; } void PendingOperation::finishWithError(const QString &message) { if (message.isEmpty()) m_errorMessage = QStringLiteral("Unknown Error"); m_errorMessage = message; if (!m_finished) { m_finished = true; emit finished(); emit error(m_errorMessage); } } void PendingOperation::finishWithSuccess() { Q_ASSERT(m_errorMessage.isNull()); if (!m_finished) { m_finished = true; emit finished(); if (isSuccess()) emit success(); } } ricochet-1.1.4/src/utils/PendingOperation.h000066400000000000000000000061051300720305500206570ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef PENDINGOPERATION_H #define PENDINGOPERATION_H #include /* Represents an asynchronous operation for reporting status * * This class is used for asynchronous operations that report a * status and errors when finished, particularly for exposing them * to QML. * * Subclass PendingOperation to implement your operation's logic. * You also need to handle the object's lifetime, for example by * calling deleteLater() when finished() is emitted. * * PendingOperation will emit finished() and one of success() or * error() when completed. */ class PendingOperation : public QObject { Q_OBJECT Q_PROPERTY(bool isFinished READ isFinished NOTIFY finished FINAL) Q_PROPERTY(bool isSuccess READ isSuccess NOTIFY success FINAL) Q_PROPERTY(bool isError READ isError NOTIFY error FINAL) Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY finished FINAL) public: PendingOperation(QObject *parent = 0); bool isFinished() const; bool isSuccess() const; bool isError() const; QString errorMessage() const; signals: // Always emitted once when finished, regardless of status void finished(); // One of error() or success() is emitted once void error(const QString &errorMessage); void success(); protected slots: void finishWithError(const QString &errorMessage); void finishWithSuccess(); private: bool m_finished; QString m_errorMessage; }; Q_DECLARE_METATYPE(PendingOperation*) #endif ricochet-1.1.4/src/utils/SecureRNG.cpp000066400000000000000000000104551300720305500175450ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "SecureRNG.h" #include #include #include #include #ifdef Q_OS_WIN #include #endif #if QT_VERSION >= 0x040700 #include #endif bool SecureRNG::seed() { #if QT_VERSION >= 0x040700 QElapsedTimer timer; timer.start(); #endif #ifdef Q_OS_WIN /* RAND_poll is very unreliable on windows; with older versions of OpenSSL, * it can take up to several minutes to run and has been known to crash. * Even newer versions seem to take around 400ms, which is far too long for * interactive startup. Random data from the windows CSP is used as a seed * instead, as it should be very high quality random and fast. */ HCRYPTPROV provider = 0; if (!CryptAcquireContext(&provider, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) { qWarning() << "Failed to acquire CSP context for RNG seed:" << hex << GetLastError(); return false; } /* Same amount of entropy OpenSSL uses, apparently. */ char buf[32]; if (!CryptGenRandom(provider, sizeof(buf), reinterpret_cast(buf))) { qWarning() << "Failed to get entropy from CSP for RNG seed: " << hex << GetLastError(); CryptReleaseContext(provider, 0); return false; } CryptReleaseContext(provider, 0); RAND_seed(buf, sizeof(buf)); memset(buf, 0, sizeof(buf)); #else if (!RAND_poll()) { qWarning() << "OpenSSL RNG seed failed:" << ERR_get_error(); return false; } #endif #if QT_VERSION >= 0x040700 qDebug() << "RNG seed took" << timer.elapsed() << "ms"; #endif return true; } void SecureRNG::random(char *buf, int size) { int r = RAND_bytes(reinterpret_cast(buf), size); if (r <= 0) qFatal("RNG failed: %lu", ERR_get_error()); } QByteArray SecureRNG::random(int size) { QByteArray re(size, 0); random(re.data(), size); return re; } QByteArray SecureRNG::randomPrintable(int length) { QByteArray re(length, 0); for (int i = 0; i < re.size(); i++) re[i] = randomInt(95) + 32; return re; } unsigned SecureRNG::randomInt(unsigned max) { unsigned cutoff = UINT_MAX - (UINT_MAX % max); unsigned value = 0; for (;;) { random(reinterpret_cast(&value), sizeof(value)); if (value < cutoff) return value % max; } } #ifndef UINT64_MAX #define UINT64_MAX ((quint64)-1) #endif quint64 SecureRNG::randomInt64(quint64 max) { quint64 cutoff = UINT64_MAX - (UINT64_MAX % max); quint64 value = 0; for (;;) { random(reinterpret_cast(value), sizeof(value)); if (value < cutoff) return value % max; } } ricochet-1.1.4/src/utils/SecureRNG.h000066400000000000000000000037421300720305500172130ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef SECURERNG_H #define SECURERNG_H #include class SecureRNG { public: static bool seed(); static void random(char *buf, int size); static QByteArray random(int size); static QByteArray randomPrintable(int length); static unsigned randomInt(unsigned max); static quint64 randomInt64(quint64 max); }; #endif // SECURERNG_H ricochet-1.1.4/src/utils/Settings.cpp000066400000000000000000000350601300720305500175470ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "Settings.h" #include #include #include #include #include #include #include #include #include #include class SettingsFilePrivate : public QObject { Q_OBJECT public: SettingsFile *q; QString filePath; QString errorMessage; QTimer syncTimer; QJsonObject jsonRoot; SettingsObject *rootObject; SettingsFilePrivate(SettingsFile *qp); virtual ~SettingsFilePrivate(); void reset(); void setError(const QString &message); bool checkDirPermissions(const QString &path); bool readFile(); bool writeFile(); static QStringList splitPath(const QString &input, bool &ok); QJsonValue read(const QJsonObject &base, const QStringList &path); bool write(const QStringList &path, const QJsonValue &value); signals: void modified(const QStringList &path, const QJsonValue &value); private slots: void sync(); }; SettingsFile::SettingsFile(QObject *parent) : QObject(parent), d(new SettingsFilePrivate(this)) { d->rootObject = new SettingsObject(this, QString()); } SettingsFile::~SettingsFile() { } SettingsFilePrivate::SettingsFilePrivate(SettingsFile *qp) : QObject(qp) , q(qp) , rootObject(0) { syncTimer.setInterval(0); syncTimer.setSingleShot(true); connect(&syncTimer, &QTimer::timeout, this, &SettingsFilePrivate::sync); } SettingsFilePrivate::~SettingsFilePrivate() { if (syncTimer.isActive()) sync(); delete rootObject; } void SettingsFilePrivate::reset() { filePath.clear(); errorMessage.clear(); jsonRoot = QJsonObject(); emit modified(QStringList(), jsonRoot); } QString SettingsFile::filePath() const { return d->filePath; } bool SettingsFile::setFilePath(const QString &filePath) { if (d->filePath == filePath) return hasError(); d->reset(); d->filePath = filePath; QFileInfo fileInfo(filePath); QDir dir(fileInfo.path()); if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) { d->setError(QStringLiteral("Cannot create directory: %1").arg(dir.path())); return false; } d->checkDirPermissions(fileInfo.path()); if (!d->readFile()) return false; return true; } QString SettingsFile::errorMessage() const { return d->errorMessage; } bool SettingsFile::hasError() const { return !d->errorMessage.isEmpty(); } void SettingsFilePrivate::setError(const QString &message) { errorMessage = message; emit q->error(); } bool SettingsFilePrivate::checkDirPermissions(const QString &path) { static QFile::Permissions desired = QFileDevice::ReadUser | QFileDevice::WriteUser | QFileDevice::ExeUser; static QFile::Permissions ignored = QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner; QFile file(path); if ((file.permissions() & ~ignored) != desired) { qDebug() << "Correcting permissions on configuration directory"; if (!file.setPermissions(desired)) { qWarning() << "Correcting permissions on configuration directory failed"; return false; } } return true; } SettingsObject *SettingsFile::root() { return d->rootObject; } const SettingsObject *SettingsFile::root() const { return d->rootObject; } void SettingsFilePrivate::sync() { if (filePath.isEmpty()) return; syncTimer.stop(); writeFile(); } bool SettingsFilePrivate::readFile() { QFile file(filePath); if (!file.open(QIODevice::ReadWrite)) { setError(file.errorString()); return false; } QByteArray data = file.readAll(); if (data.isEmpty() && (file.error() != QFileDevice::NoError || file.size() > 0)) { setError(file.errorString()); return false; } if (data.isEmpty()) { jsonRoot = QJsonObject(); return true; } QJsonParseError parseError; QJsonDocument document = QJsonDocument::fromJson(data, &parseError); if (document.isNull()) { setError(parseError.errorString()); return false; } if (!document.isObject()) { setError(QStringLiteral("Invalid configuration file (expected object)")); return false; } jsonRoot = document.object(); emit modified(QStringList(), jsonRoot); return true; } bool SettingsFilePrivate::writeFile() { QSaveFile file(filePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { setError(file.errorString()); return false; } QJsonDocument document(jsonRoot); QByteArray data = document.toJson(); if (data.isEmpty() && !document.isEmpty()) { setError(QStringLiteral("Encoding failure")); return false; } if (file.write(data) < data.size() || !file.commit()) { setError(file.errorString()); return false; } return true; } QStringList SettingsFilePrivate::splitPath(const QString &input, bool &ok) { QStringList components = input.split(QLatin1Char('.')); // Allow a leading '.' to simplify concatenation if (!components.isEmpty() && components.first().isEmpty()) components.takeFirst(); // No other empty components, including a trailing . foreach (const QString &word, components) { if (word.isEmpty()) { ok = false; return QStringList(); } } ok = true; return components; } QJsonValue SettingsFilePrivate::read(const QJsonObject &base, const QStringList &path) { QJsonValue current = base; foreach (const QString &key, path) { QJsonObject object = current.toObject(); if (object.isEmpty() || (current = object.value(key)).isUndefined()) return QJsonValue::Undefined; } return current; } // Compare two QJsonValue to find keys that have changed, // recursing into objects and building paths as necessary. typedef QList > ModifiedList; static void findModifiedRecursive(ModifiedList &modified, const QStringList &path, const QJsonValue &oldValue, const QJsonValue &newValue) { if (oldValue.isObject() || newValue.isObject()) { // If either is a non-object type, this returns an empty object QJsonObject oldObject = oldValue.toObject(); QJsonObject newObject = newValue.toObject(); // Iterate keys of the original object and compare to new for (QJsonObject::iterator it = oldObject.begin(); it != oldObject.end(); it++) { QJsonValue newSubValue = newObject.value(it.key()); if (*it == newSubValue) continue; if ((*it).isObject() || newSubValue.isObject()) findModifiedRecursive(modified, QStringList() << path << it.key(), *it, newSubValue); else modified.append(qMakePair(QStringList() << path << it.key(), newSubValue)); } // Iterate keys of the new object that may not be in original for (QJsonObject::iterator it = newObject.begin(); it != newObject.end(); it++) { if (oldObject.contains(it.key())) continue; if ((*it).isObject()) findModifiedRecursive(modified, QStringList() << path << it.key(), QJsonValue::Undefined, it.value()); else modified.append(qMakePair(QStringList() << path << it.key(), it.value())); } } else modified.append(qMakePair(path, newValue)); } bool SettingsFilePrivate::write(const QStringList &path, const QJsonValue &value) { typedef QVarLengthArray > ObjectStack; ObjectStack stack; QJsonValue current = jsonRoot; QJsonValue originalValue; QString currentKey; foreach (const QString &key, path) { const QJsonObject &parent = current.toObject(); stack.append(qMakePair(currentKey, parent)); current = parent.value(key); currentKey = key; } // Stack now contains parent objects starting with the root, and current // is the old value. Write back changes in reverse. if (current == value) return false; originalValue = current; current = value; ObjectStack::const_iterator it = stack.end(), begin = stack.begin(); while (it != begin) { --it; QJsonObject update = it->second; update.insert(currentKey, current); current = update; currentKey = it->first; } // current is now the updated jsonRoot jsonRoot = current.toObject(); syncTimer.start(); ModifiedList modified; findModifiedRecursive(modified, path, originalValue, value); for (ModifiedList::iterator it = modified.begin(); it != modified.end(); it++) emit this->modified(it->first, it->second); return true; } class SettingsObjectPrivate : public QObject { Q_OBJECT public: explicit SettingsObjectPrivate(SettingsObject *q); SettingsObject *q; SettingsFile *file; QStringList path; QJsonObject object; bool invalid; void setFile(SettingsFile *file); public slots: void modified(const QStringList &absolutePath, const QJsonValue &value); }; SettingsObject::SettingsObject(QObject *parent) : QObject(parent) , d(new SettingsObjectPrivate(this)) { d->setFile(defaultFile()); if (d->file) setPath(QString()); } SettingsObject::SettingsObject(const QString &path, QObject *parent) : QObject(parent) , d(new SettingsObjectPrivate(this)) { d->setFile(defaultFile()); setPath(path); } SettingsObject::SettingsObject(SettingsFile *file, const QString &path, QObject *parent) : QObject(parent) , d(new SettingsObjectPrivate(this)) { d->setFile(file); setPath(path); } SettingsObject::SettingsObject(SettingsObject *base, const QString &path, QObject *parent) : QObject(parent) , d(new SettingsObjectPrivate(this)) { d->setFile(base->d->file); setPath(base->path() + QLatin1Char('.') + path); } SettingsObjectPrivate::SettingsObjectPrivate(SettingsObject *qp) : QObject(qp) , q(qp) , file(0) , invalid(true) { } void SettingsObjectPrivate::setFile(SettingsFile *value) { if (file == value) return; if (file) disconnect(file, 0, this, 0); file = value; if (file) connect(file->d, &SettingsFilePrivate::modified, this, &SettingsObjectPrivate::modified); } // Emit SettingsObject::modified with a relative path if path is matched void SettingsObjectPrivate::modified(const QStringList &key, const QJsonValue &value) { if (key.size() < path.size()) return; for (int i = 0; i < path.size(); i++) { if (path[i] != key[i]) return; } object = file->d->read(file->d->jsonRoot, path).toObject(); emit q->modified(QStringList(key.mid(path.size())).join(QLatin1Char('.')), value); emit q->dataChanged(); } static QPointer defaultObjectFile; SettingsFile *SettingsObject::defaultFile() { return defaultObjectFile; } void SettingsObject::setDefaultFile(SettingsFile *file) { defaultObjectFile = file; } QString SettingsObject::path() const { return d->path.join(QLatin1Char('.')); } void SettingsObject::setPath(const QString &input) { bool ok = false; QStringList newPath = SettingsFilePrivate::splitPath(input, ok); if (!ok) { d->invalid = true; d->path.clear(); d->object = QJsonObject(); emit pathChanged(); emit dataChanged(); return; } if (!d->invalid && d->path == newPath) return; d->path = newPath; if (d->file) { d->invalid = false; d->object = d->file->d->read(d->file->d->jsonRoot, d->path).toObject(); emit dataChanged(); } emit pathChanged(); } QJsonObject SettingsObject::data() const { return d->object; } void SettingsObject::setData(const QJsonObject &input) { if (d->invalid || d->object == input) return; d->object = input; d->file->d->write(d->path, d->object); } QJsonValue SettingsObject::read(const QString &key, const QJsonValue &defaultValue) const { bool ok = false; QStringList splitKey = SettingsFilePrivate::splitPath(key, ok); if (d->invalid || !ok || splitKey.isEmpty()) { qDebug() << "Invalid settings read of path" << key; return defaultValue; } QJsonValue ret = d->file->d->read(d->object, splitKey); if (ret.isUndefined()) ret = defaultValue; return ret; } void SettingsObject::write(const QString &key, const QJsonValue &value) { bool ok = false; QStringList splitKey = SettingsFilePrivate::splitPath(key, ok); if (d->invalid || !ok || splitKey.isEmpty()) { qDebug() << "Invalid settings write of path" << key; return; } splitKey = d->path + splitKey; d->file->d->write(splitKey, value); } void SettingsObject::unset(const QString &key) { write(key, QJsonValue()); } void SettingsObject::undefine() { if (d->invalid) return; d->object = QJsonObject(); d->file->d->write(d->path, QJsonValue::Undefined); } #include "Settings.moc" ricochet-1.1.4/src/utils/Settings.h000066400000000000000000000201731300720305500172130ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef SETTINGS_H #define SETTINGS_H #include #include #include #include #include #include class SettingsObject; class SettingsFilePrivate; class SettingsObjectPrivate; /* SettingsFile represents a JSON-encoded configuration file. * * SettingsFile is an API for reading, writing, and change notification * on JSON-encoded settings files. * * Data is accessed via SettingsObject, either using the root property * or by creating a SettingsObject, optionally using a base path. */ class SettingsFile : public QObject { Q_OBJECT Q_DISABLE_COPY(SettingsFile) Q_PROPERTY(SettingsObject *root READ root CONSTANT) Q_PROPERTY(QString filePath READ filePath WRITE setFilePath NOTIFY filePathChanged) Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY error) Q_PROPERTY(bool hasError READ hasError NOTIFY error) public: explicit SettingsFile(QObject *parent = 0); virtual ~SettingsFile(); QString filePath() const; bool setFilePath(const QString &filePath); QString errorMessage() const; bool hasError() const; SettingsObject *root(); const SettingsObject *root() const; signals: void filePathChanged(); void error(); private: SettingsFilePrivate *d; friend class SettingsObject; friend class SettingsObjectPrivate; }; /* SettingsObject reads and writes data within a SettingsFile * * A SettingsObject is associated with a SettingsFile and represents an object * tree within that file. It refers to the JSON object tree using a path * notation with keys separated by '.'. For example: * * { * "one": { * "two": { * "three": "value" * } * } * } * * With this data, a SettingsObject with an empty path can read with the path * "one.two.three", and a SettingsObject with a path of "one.two" can simply * read or write on "three". * * Multiple SettingsObjects may be created for the same path, and will be kept * synchronized with changes. The modified signal is emitted for all changes * affecting keys within a path, including writes of object trees and from other * instances. */ class SettingsObject : public QObject { Q_OBJECT Q_DISABLE_COPY(SettingsObject) Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) Q_PROPERTY(QJsonObject data READ data WRITE setData NOTIFY dataChanged) public: explicit SettingsObject(QObject *parent = 0); explicit SettingsObject(const QString &path, QObject *parent = 0); explicit SettingsObject(SettingsFile *file, const QString &path, QObject *parent = 0); explicit SettingsObject(SettingsObject *base, const QString &path, QObject *parent = 0); /* Specify a SettingsFile to use by default on SettingsObject instances. * * After calling setDefaultFile, a SettingsObject created without any file, e.g.: * * SettingsObject settings; * SettingsObject animals(QStringLiteral("animals")); * * Will use the specified SettingsFile instance by default. This is a convenience * over passing around instances of SettingsFile in application use cases, and is * particularly useful for QML. */ static SettingsFile *defaultFile(); static void setDefaultFile(SettingsFile *file); QString path() const; void setPath(const QString &path); QJsonObject data() const; void setData(const QJsonObject &data); Q_INVOKABLE QJsonValue read(const QString &key, const QJsonValue &defaultValue = QJsonValue::Undefined) const; template T read(const QString &key) const; Q_INVOKABLE void write(const QString &key, const QJsonValue &value); template void write(const QString &key, const T &value); Q_INVOKABLE void unset(const QString &key); // const char* key overloads QJsonValue read(const char *key, const QJsonValue &defaultValue = QJsonValue::Undefined) const { return read(QString::fromLatin1(key), defaultValue); } template T read(const char *key) const { return read(QString::fromLatin1(key)); } void write(const char *key, const QJsonValue &value) { write(QString::fromLatin1(key), value); } template void write(const char *key, const T &value) { write(QString::fromLatin1(key), value); } void unset(const char *key) { unset(QString::fromLatin1(key)); } Q_INVOKABLE void undefine(); signals: void pathChanged(); void dataChanged(); void modified(const QString &path, const QJsonValue &value); private: SettingsObjectPrivate *d; }; template inline void SettingsObject::write(const QString &key, const T &value) { write(key, QJsonValue(value)); } template<> inline QString SettingsObject::read(const QString &key) const { return read(key).toString(); } template<> inline QJsonArray SettingsObject::read(const QString &key) const { return read(key).toArray(); } template<> inline QJsonObject SettingsObject::read(const QString &key) const { return read(key).toObject(); } template<> inline double SettingsObject::read(const QString &key) const { return read(key).toDouble(); } template<> inline int SettingsObject::read(const QString &key) const { return read(key).toInt(); } template<> inline bool SettingsObject::read(const QString &key) const { return read(key).toBool(); } template<> inline QDateTime SettingsObject::read(const QString &key) const { QString value = read(key).toString(); if (value.isEmpty()) return QDateTime(); return QDateTime::fromString(value, Qt::ISODate).toLocalTime(); } template<> inline void SettingsObject::write(const QString &key, const QDateTime &value) { write(key, QJsonValue(value.toUTC().toString(Qt::ISODate))); } // Explicitly store value encoded as base64. Decodes and casts implicitly to QByteArray for reads. class Base64Encode { public: explicit Base64Encode(const QByteArray &value) : d(value) { } operator QByteArray() { return d; } QByteArray encoded() const { return d.toBase64(); } private: QByteArray d; }; template<> inline Base64Encode SettingsObject::read(const QString &key) const { return Base64Encode(QByteArray::fromBase64(read(key).toString().toLatin1())); } template<> inline void SettingsObject::write(const QString &key, const Base64Encode &value) { write(key, QJsonValue(QString::fromLatin1(value.encoded()))); } #endif ricochet-1.1.4/src/utils/StringUtil.cpp000066400000000000000000000063621300720305500200560ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "StringUtil.h" QByteArray quotedString(const QByteArray &string) { QByteArray out; out.reserve(string.size() * 2); out.append('"'); for (int i = 0; i < string.size(); ++i) { switch (string[i]) { case '"': out.append("\\\""); break; case '\\': out.append("\\\\"); break; default: out.append(string[i]); break; } } out.append('"'); return out; } QByteArray unquotedString(const QByteArray &string) { if (string.size() < 2 || string[0] != '"') return string; QByteArray out; out.reserve(string.size() - 2); for (int i = 1; i < string.size(); ++i) { switch (string[i]) { case '\\': if (++i < string.size()) out.append(string[i]); break; case '"': return out; default: out.append(string[i]); } } return out; } QList splitQuotedStrings(const QByteArray &input, char separator) { QList out; bool inquote = false; int start = 0; for (int i = 0; i < input.size(); ++i) { switch (input[i]) { case '"': inquote = !inquote; break; case '\\': if (inquote) ++i; break; } if (!inquote && input[i] == separator) { out.append(input.mid(start, i - start)); start = i+1; } } if (start < input.size()) out.append(input.mid(start)); return out; } ricochet-1.1.4/src/utils/StringUtil.h000066400000000000000000000040051300720305500175130ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef STRINGUTIL_H #define STRINGUTIL_H #include #include QByteArray quotedString(const QByteArray &string); /* Return the unquoted contents of a string, either until an end quote or an unescaped separator character. */ QByteArray unquotedString(const QByteArray &string); QList splitQuotedStrings(const QByteArray &input, char separator); #endif // STRINGUTIL_H ricochet-1.1.4/src/utils/Useful.h000066400000000000000000000050521300720305500166550ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef UTILS_USEFUL_H #define UTILS_USEFUL_H #include #include /* Print a warning for bug conditions, and assert on a debug build. * * This should be used in place of Q_ASSERT for bug conditions, along * with a proper error case for release-mode builds. For example: * * if (!connection || !user) { * BUG() << "Request" << request << "should have a connection and user"; * return false; * } * * Do not confuse bugs with actual error cases; BUG() should never be * triggered unless the code or logic is wrong. */ #if !defined(QT_NO_DEBUG) || defined(QT_FORCE_ASSERTS) # define BUG() Explode(__FILE__,__LINE__), qWarning() << "BUG:" namespace { class Explode { public: const char *file; int line; Explode(const char *file, int line) : file(file), line(line) { } ~Explode() { qt_assert("something broke!", file, line); } }; } #else # define BUG() qWarning() << "BUG:" #endif #endif ricochet-1.1.4/tests/000077500000000000000000000000001300720305500144525ustar00rootroot00000000000000ricochet-1.1.4/tests/tests.pri000066400000000000000000000001571300720305500163330ustar00rootroot00000000000000TEMPLATE = app QT += testlib CONFIG -= app_bundle CONFIG += testcase SRC = ../../src/ INCLUDEPATH += $${SRC} ricochet-1.1.4/tests/tests.pro000066400000000000000000000000541300720305500163350ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS += tst_cryptokey ricochet-1.1.4/tests/tst_cryptokey/000077500000000000000000000000001300720305500173755ustar00rootroot00000000000000ricochet-1.1.4/tests/tst_cryptokey/tst_cryptokey.cpp000066400000000000000000000207771300720305500230410ustar00rootroot00000000000000/* Ricochet - https://ricochet.im/ * Copyright (C) 2014, John Brooks * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the names of the copyright owners nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include "utils/CryptoKey.h" class TestCryptoKey : public QObject { Q_OBJECT private slots: void load(); void publicKeyDigest(); void encodedPublicKey(); void encodedPrivateKey(); void torServiceID(); void sign(); }; const char *alice = "-----BEGIN RSA PRIVATE KEY-----\n" "MIICXQIBAAKBgQDAS9nLWyK0jWZ8yduqVEhSyZRplTaeUpGWYRi14n1C4sjO6nqm\n" "ES31UCGDH4nIor2R/XMJCJkJwK+t2XrtiH+jUEHwUGhnMkm3hW5NHt5g39s9YK7l\n" "xD39O8N2tHUycVq8guhrb1WBQ2/bmZ85nOIuBDZxIuVQZA1U1L6rWGvm+wIDAQAB\n" "AoGAewYL6JX9thVgpCVga7BQNObSFFpp/xBEJDkqXfLwwIHmhrpsjSIgjPke94yN\n" "0daMAYJsvjLJ9ftYaZjhlGXngbBJiAU95gcZoTAsn2hNJP22ndGuhi6WEKhYwRxK\n" "U5d+3Khzy/ysuoay7DSVtpSmpiacWPSiiptEkxNbcbGba8ECQQDeEGoPASmxZoh4\n" "I2JNQkqSwMKsOZpp/SJhnmLCPoA1oDwlGtu4HF7t9hBXeyIXgLvbfJudFEa+LqR7\n" "wrKQPn0fAkEA3a7cR7eSRNu1ak7gVfQfnP4tFl3+7UC2hUqVHLA5ks4pLl7/ITa+\n" "3P04SOs3WpvZJHYJ+hi/anqEPYrD/3B+pQJBAKmjnnHh8IjODDjCxyjAGJntWYoZ\n" "4yVOtEIgrc830delley+jNUkDzz3+dnqfcu4k0oD8hjYUYaduRe2T5Szt/8CQQDC\n" "EVt8WUNujp0R9P1FohKu4IFeLGmJD/b5V2KUm927HEpG8xkM3Z1XX0KP64MpCnid\n" "B80SKeog8CKmsb2F+NiVAkBT1CEAdiFYtf72hnZCLBw5HrqpN+zjw00GjtlrmmNV\n" "+ILb/YRp5flCY5Se95ExzQqRKzvK5iJg0yEOVF0OcbO+\n" "-----END RSA PRIVATE KEY-----"; const char *aliceDigest = "623a1ffc94d8f8edcd5e47fbd45e08deb911d1bc"; const char *aliceTorID = "mi5b77eu3d4o3tk6"; const char *aliceSignedTestData = "23fdcd5c7d40b44a7e49619d9048c81931166a0adb80c8981cc8f9a9e02c3923d5fba6d92ea03dc672d009a5fe1be2b582fb935076f880d9aa55511c33620d2aa23336b579dd7ccd1dbf4c845e4100a114d8ac20dd47229e876444f79d5152456a8e26fefa67a12436b3c33728a2ff7cb12250c486f786647574e48bb9208f64"; const char *bob = "-----BEGIN RSA PUBLIC KEY-----\n" "MIGJAoGBAMP8GyAg/kzwXizpUWjWIMw/lvDffXjsxcq1qmZWZxXJQH/oE8bX+WAf\n" "VS8iUHVqTykubR0W3QNL6aWSZKBqDQUTN0QBJUF4qdkg3x56C0kwcWa+seDMAvJw\n" "pcHK9wN7mtWHIhFwhikP//NylrY1MaUxcPjvOKcdJ90k988nnmpZAgMBAAE=\n" "-----END RSA PUBLIC KEY-----\n"; const char *bobDigest = "b4780cabdfc3593004431644977cf73bf8475848"; const char *bobTorID = "wr4azk67ynmtabcd"; void TestCryptoKey::load() { CryptoKey key; QVERIFY(!key.isLoaded()); // Private key QVERIFY(key.loadFromData(alice, CryptoKey::PrivateKey)); QVERIFY(key.isLoaded()); QVERIFY(key.isPrivate()); QCOMPARE(key.bits(), 1024); key.clear(); QVERIFY(!key.isLoaded()); // Public key QVERIFY(key.loadFromData(bob, CryptoKey::PublicKey)); QVERIFY(key.isLoaded()); QVERIFY(!key.isPrivate()); QCOMPARE(key.bits(), 1024); // DER public key QByteArray derEncoded = key.encodedPublicKey(CryptoKey::DER); key.clear(); QVERIFY(key.loadFromData(derEncoded, CryptoKey::PublicKey, CryptoKey::DER)); QCOMPARE(key.encodedPublicKey(CryptoKey::DER), derEncoded); key.clear(); // Invalid key QVERIFY(!key.loadFromData(QByteArray(alice).mid(0, 150), CryptoKey::PrivateKey)); QVERIFY(!key.isLoaded()); // Invalid DER key QVERIFY(!key.loadFromData(derEncoded.mid(0, derEncoded.size()-2), CryptoKey::PublicKey, CryptoKey::DER)); QVERIFY(!key.isLoaded()); // Empty key QVERIFY(!key.loadFromData("", CryptoKey::PublicKey)); QVERIFY(!key.isLoaded()); } void TestCryptoKey::publicKeyDigest() { CryptoKey key; QVERIFY(key.loadFromData(bob, CryptoKey::PublicKey)); QCOMPARE(key.publicKeyDigest().toHex(), QByteArray(bobDigest)); key.clear(); QVERIFY(key.loadFromData(alice, CryptoKey::PrivateKey)); QCOMPARE(key.publicKeyDigest().toHex(), QByteArray(aliceDigest)); } void TestCryptoKey::encodedPublicKey() { CryptoKey key; QVERIFY(key.loadFromData(bob, CryptoKey::PublicKey)); QByteArray pemEncoded = key.encodedPublicKey(CryptoKey::PEM); QVERIFY(pemEncoded.contains("BEGIN RSA PUBLIC KEY")); QByteArray derEncoded = key.encodedPublicKey(CryptoKey::DER); QCOMPARE(derEncoded.size(), 140); CryptoKey key2; QVERIFY(key2.loadFromData(pemEncoded, CryptoKey::PublicKey)); QCOMPARE(key.encodedPublicKey(CryptoKey::PEM), key2.encodedPublicKey(CryptoKey::PEM)); QCOMPARE(key.publicKeyDigest(), key2.publicKeyDigest()); CryptoKey key3; QVERIFY(key3.loadFromData(derEncoded, CryptoKey::PublicKey, CryptoKey::DER)); QCOMPARE(key.encodedPublicKey(CryptoKey::DER), key3.encodedPublicKey(CryptoKey::DER)); QCOMPARE(key.publicKeyDigest(), key3.publicKeyDigest()); // Doesn't contain a private key CryptoKey key4; QVERIFY(!key4.loadFromData(pemEncoded, CryptoKey::PrivateKey)); } void TestCryptoKey::encodedPrivateKey() { CryptoKey key; QVERIFY(key.loadFromData(alice, CryptoKey::PrivateKey)); QByteArray pemEncoded = key.encodedPrivateKey(CryptoKey::PEM); QVERIFY(pemEncoded.contains("BEGIN RSA PRIVATE KEY")); QByteArray derEncoded = key.encodedPrivateKey(CryptoKey::DER); QVERIFY(!derEncoded.isEmpty()); CryptoKey key2; QVERIFY(key2.loadFromData(pemEncoded, CryptoKey::PrivateKey)); QCOMPARE(key.encodedPrivateKey(CryptoKey::PEM), key2.encodedPrivateKey(CryptoKey::PEM)); QCOMPARE(key.publicKeyDigest(), key2.publicKeyDigest()); CryptoKey key3; QVERIFY(key3.loadFromData(derEncoded, CryptoKey::PrivateKey, CryptoKey::DER)); QCOMPARE(key.encodedPrivateKey(CryptoKey::DER), key3.encodedPrivateKey(CryptoKey::DER)); QCOMPARE(key.publicKeyDigest(), key3.publicKeyDigest()); } void TestCryptoKey::torServiceID() { CryptoKey key; QVERIFY(key.loadFromData(bob, CryptoKey::PublicKey)); QString id = key.torServiceID(); QCOMPARE(id.size(), 16); QCOMPARE(id, QLatin1String(bobTorID)); } void TestCryptoKey::sign() { CryptoKey key; QVERIFY(key.loadFromData(alice, CryptoKey::PrivateKey)); QByteArray data = "test data"; QByteArray data2 = "different"; // Good signature QByteArray signature = key.signData(data); QVERIFY(!signature.isEmpty()); QVERIFY(key.verifyData(data, signature)); // Bad signature QVERIFY(!key.verifyData(data2, signature)); // Corrupt signature QVERIFY(!key.verifyData(data, signature.mid(0, signature.size() - 10))); // Wrong public key CryptoKey key2; QVERIFY(key2.loadFromData(bob, CryptoKey::PublicKey)); QVERIFY(!key2.verifyData(data, signature)); // Compare to signSHA256 QByteArray dataDigest = QCryptographicHash::hash(data, QCryptographicHash::Sha256); QByteArray signature2 = key.signSHA256(dataDigest); QVERIFY(!signature2.isEmpty()); // signSHA256 and verifySHA256 QVERIFY(key.verifySHA256(dataDigest, signature2)); // signSHA256 and verifyData QVERIFY(key.verifyData(data, signature2)); // signData and verifySHA256 QVERIFY(key.verifySHA256(dataDigest, signature)); // Compare to precomputed signature QByteArray signaturep = QByteArray::fromHex(aliceSignedTestData); QVERIFY(key.verifyData(data, signaturep)); QVERIFY(key.verifySHA256(dataDigest, signaturep)); } QTEST_MAIN(TestCryptoKey) #include "tst_cryptokey.moc" ricochet-1.1.4/tests/tst_cryptokey/tst_cryptokey.pro000066400000000000000000000011471300720305500230450ustar00rootroot00000000000000include(../tests.pri) SOURCES += tst_cryptokey.cpp \ $${SRC}/utils/CryptoKey.cpp \ $${SRC}/utils/SecureRNG.cpp unix { !isEmpty(OPENSSLDIR) { INCLUDEPATH += $${OPENSSLDIR}/include LIBS += -L$${OPENSSLDIR}/lib -lcrypto } else { CONFIG += link_pkgconfig PKGCONFIG += libcrypto } } win32 { isEmpty(OPENSSLDIR):error(You must pass OPENSSLDIR=path/to/openssl to qmake on this platform) INCLUDEPATH += $${OPENSSLDIR}/include LIBS += -L$${OPENSSLDIR}/lib -llibeay32 # required by openssl LIBS += -lUser32 -lGdi32 -ladvapi32 } macx:LIBS += -lcrypto ricochet-1.1.4/translation/000077500000000000000000000000001300720305500156465ustar00rootroot00000000000000ricochet-1.1.4/translation/embedded.qrc000066400000000000000000000017731300720305500201160ustar00rootroot00000000000000 ricochet_en.qm ricochet_it.qm ricochet_es.qm ricochet_da.qm ricochet_pt_BR.qm ricochet_de.qm ricochet_bg.qm ricochet_cs.qm ricochet_fi.qm ricochet_fr.qm ricochet_ru.qm ricochet_uk.qm ricochet_tr.qm ricochet_nl_NL.qm ricochet_fil_PH.qm ricochet_sv.qm ricochet_pl.qm ricochet_he.qm ricochet_sl.qm ricochet_zh.qm ricochet_et_EE.qm ricochet_it_IT.qm ricochet_nb.qm ricochet_pt_PT.qm ricochet_sq.qm ricochet_zh_HK.qm ricochet-1.1.4/translation/inno/000077500000000000000000000000001300720305500166115ustar00rootroot00000000000000ricochet-1.1.4/translation/inno/Albanian.isl000066400000000000000000000416441300720305500210400ustar00rootroot00000000000000; *** Inno Setup version 5.1.11+ Albanian messages *** ; ; To download user-contributed translations of this file, go to: ; http://www.jrsoftware.org/files/istrans/ ; ; Note: When translating this text, do not add periods (.) to the end of ; messages that didn't have them already, because on those messages Inno ; Setup adds the periods automatically (appending a period would result in ; two periods being displayed). ; ; Prkthyer nga Besmir Godole ; Posta-e: bgodole@gmail.com ; M kontaktoni me post-e pr ndonj gabim ose sugjerim rreth prkthimit. [LangOptions] ; The following three entries are very important. Be sure to read and ; understand the '[LangOptions] section' topic in the help file. LanguageName=Albanian LanguageID=$041C LanguageCodePage=1252 ; If the language you are translating to requires special font faces or ; sizes, uncomment any of the following entries and change them accordingly. ;DialogFontName= ;DialogFontSize=8 ;WelcomeFontName=Verdana ;WelcomeFontSize=12 ;TitleFontName=Arial ;TitleFontSize=29 ;CopyrightFontName=Arial ;CopyrightFontSize=8 [Messages] ; *** Application titles SetupAppTitle=Sistemim SetupWindowTitle=Sistemon - %1 UninstallAppTitle=instalim UninstallAppFullTitle=instalon %1 ; *** Misc. common InformationTitle=Informacion ConfirmTitle=Miratim ErrorTitle=Gabim ; *** SetupLdr messages SetupLdrStartupMessage=Tani do instalosh %1. Do vijosh? LdrCannotCreateTemp=Nuk mund t krijohet nj sked kohshkurtr. Ndrpritet sistemimi LdrCannotExecTemp=Nuk mund t ekzekutohet skeda n direktorin kohshkurtr. Ndrpritet sistemimi ; *** Startup error messages LastErrorMessage=%1.%n%nGabim %2: %3 SetupFileMissing=Skeda %1 mungon n direktorin e instalimit. T lutem korrigjo problemin ose gjej nj kopje t re t programit. SetupFileCorrupt=Skedat e sistemimit jan prishur. T lutem gjej nj kopje t re t programit. SetupFileCorruptOrWrongVer=Skedat e sistemimit jan prishur ose nuk pajtohen me kt version t Sistemimit. T lutem korrigjo problemin ose gjej nj kopje t re t programit. NotOnThisPlatform=Ky program nuk do veproj n %1. OnlyOnThisPlatform=Ky program duhet t veproj n %1. OnlyOnTheseArchitectures=Ky program mund t instalohet vetm n versionet e Windows-it q jan modeluar pr kto modele arkitekturore t procesorit:%n%n%1 MissingWOW64APIs=Versioni i Windows-it q ke nuk prmban funksionet q krkon Sistemimi pr t kryer nj instalim 64-bit. Pr ta korrigjuar kt problem, t lutem instalo Paketn e Shrbimit %1. WinVersionTooLowError=Ktij programi i nevojitet %1 me version %2 a m von. WinVersionTooHighError=Ky program nuk mund t instalohet n %1 me versionin %2 a m von. AdminPrivilegesRequired=Kur e instalon kt program duhet t hysh si administrator. PowerUserPrivilegesRequired=Kur e instalon kt program duhet t hysh si administrator ose antar i grupit Prdorues me Fuqi. SetupAppRunningError=Sistemimi diktoi se aktualisht po vepron %1.%n%nT lutem mbylli tani tr rastet e tij dhe kliko OK pr t vijuar, ose Anulo pr t dal. UninstallAppRunningError=instalimi diktoi se aktualisht po vepron %1.%n%nT lutem mbylli tani tr rastet e tij dhe kliko OK pr t vijuar, ose Anulo pr t dal. ; *** Misc. errors ErrorCreatingDir=Sistemimi nuk arrin t krijoj direktorin "%1" ErrorTooManyFilesInDir=N direktorin "%1" nuk mund t krijohen skeda sepse ka shum t tjera ; *** Setup common messages ExitSetupTitle=Dalje nga Sistemimi ExitSetupMessage=Sistemimi nuk sht plotsuar. Po e mbylle, programi nuk instalohet.%n%nQ instalimi t plotsohet, mund ta lshosh Sistemimin nj her tjetr.%n%nT Mbyllet Sistemimi? AboutSetupMenuItem=&Pr Sistemimin... AboutSetupTitle=Pr Sistemimin AboutSetupMessage=%1 versioni %2%n%3%n%n%1 faqe zyrtare:%n%4 AboutSetupNote= TranslatorNote= ; *** Buttons ButtonBack=< &Pas ButtonNext=&Tjetr > ButtonInstall=&Instalo ButtonOK=OK ButtonCancel=Anulo ButtonYes=&Po ButtonYesToAll=Po, &Gjithka ButtonNo=&Jo ButtonNoToAll=J&o, Asnj ButtonFinish=&Prfundo ButtonBrowse=&Shfleto... ButtonWizardBrowse=S&hfleto... ButtonNewFolder=&Krijo Dosje t Re ; *** "Select Language" dialog messages SelectLanguageTitle=Zgjedh Gjuhn e Sistemimit SelectLanguageLabel=Zgjidhe gjuhn e prdorimit gjat instalimit: ; *** Common wizard text ClickNext=Kliko Tjetr pr t vijuar, ose Anulo q Sistemimi t mbyllet. BeveledLabel= BrowseDialogTitle=Shfleton dosjen BrowseDialogLabel=Zgjidhe nj dosje n kt list, pastaj kliko OK. NewFolderName=Dosje e re ; *** "Welcome" wizard page WelcomeLabel1=Mir se erdhe te Udhrrfyesi pr Sistemimin e [name] WelcomeLabel2=[name/ver] do instalohet tani n kompjuter.%n%nPara se t vijosh, rekomandohet t mbyllen aplikimet e tjera. ; *** "Password" wizard page WizardPassword=Fjalkalimi PasswordLabel1=Ky instalim sht i mbrojtur me fjalkalim. PasswordLabel3=T lutem shkruaj fjalkalimin, pastaj kliko Tjetr q t vijosh. Fjalkalimi duhet vn me kujdes. PasswordEditLabel=&Fjalkalimi: IncorrectPassword=Fjalkalimi i shkruar nuk sht i sakt. T lutem provoje prsri. ; *** "License Agreement" wizard page WizardLicense=Marrveshja e Licencs LicenseLabel=T lutem lexo informacionin e rndsishm m posht para se t vijosh. LicenseLabel3=T lutem lexo Marrveshjen e Licencs m posht. Para se t vijosh me instalimin, duhet t pranosh detyrimet e ksaj marrveshjeje. LicenseAccepted=&Pranoj marrveshjen LicenseNotAccepted=&Nuk e pranoj marrveshjen ; *** "Information" wizard pages WizardInfoBefore=Informacion InfoBeforeLabel=T lutem lexo informacionin e rndsishm m posht para se t vijosh. InfoBeforeClickLabel=Kur t jesh gati pr t vijuar me Sistemimin, kliko Tjetr. WizardInfoAfter=Informacion InfoAfterLabel=T lutem lexo informacionin e rndsishm m posht para se t vijosh. InfoAfterClickLabel=Kur t jesh gati pr t vijuar me Sistemimin, kliko Tjetr. ; *** "User Information" wizard page WizardUserInfo=Informacion i Prdoruesit UserInfoDesc=T lutem vendos informacionin tnd. UserInfoName=&Prdoruesi: UserInfoOrg=&Organizata: UserInfoSerial=&Numri i Seris: UserInfoNameRequired=Duhet shkruar nj emr. ; *** "Select Destination Location" wizard page WizardSelectDir=Zgjedh Destinacionin SelectDirDesc=Ku duhet t instalohet [name]? SelectDirLabel3=Sistemimi do e instaloj [name] n kt dosje. SelectDirBrowseLabel=Pr t vijuar, kliko Tjetr. Nse do t zgjedhsh nj dosje ndryshe, kliko Shfleto. DiskSpaceMBLabel=Krkon jo m pak se [mb] MB hapsir t lir n disk. ToUNCPathname=Sistemimi nuk mund t instaloj n nj shteg UNC. Nse po provon instalimin n rrjet, duhet t lokalizosh drajvin e rrjetit. InvalidPath=Duhet t shkruhet shtegu i plot me shkronjn e drajvit; pr shembull:%n%nC:\APP%n%nose shtegu UNC n formn:%n%n\\server\share InvalidDrive=Drajvi ose shprndarsi UNC i zgjedhur nuk ekziston ose nuk hapet. T lutem zgjidhe nj tjetr. DiskSpaceWarningTitle=Hapsir e Pamjaftueshme DiskSpaceWarning=Sistemimi krkon t paktn %1 KB hapsir t lir pr t instaluar, por ky drajv ka vetm %2 KB me vler.%n%nGjithsesi, do vijosh? DirNameTooLong=Emri ose shtegu i dosjes sht tepr i gjat. InvalidDirName=Emri i dosjes nuk ka vler. BadDirName32=Emri i dosjes nuk mund t prmbaj kto shkronja:%n%n%1 DirExistsTitle=Dosje Ekzistuese DirExists=Dosja:%n%n%1%n%nekziston q m par. Gjithsesi, t instalohet n kt dosje? DirDoesntExistTitle=Dosje Joekzistuese DirDoesntExist=Dosja:%n%n%1%n%nnuk ekziston. T krijohet kjo dosje? ; *** "Select Components" wizard page WizardSelectComponents=Zgjedh Prbrsit SelectComponentsDesc=Cilt prbrs t instalohen? SelectComponentsLabel2=Zgjidhi prbrsit q do t instalosh; mos i shno prbrsit q nuk do t instalosh. Kliko Tjetr kur t jesh gati pr t vijuar. FullInstallation=Instalim i Plot ; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language) CompactInstallation=Instalim i Pakt CustomInstallation=Instalim i Porositur NoUninstallWarningTitle=Prbrs Ekzistues NoUninstallWarning=Sistemimi diktoi se kta prbrs jan tashm t instaluar n kompjuter:%n%n%1%n%nMoszgjedhja e ktyre prbrsve nuk do i instaloj ata.%n%nGjithsesi, do vijosh? ComponentSize1=%1 KB ComponentSize2=%1 MB ComponentsDiskSpaceMBLabel=Kjo zgjedhje krkon jo m pak se [mb] MB hapsir t lir n disk. ; *** "Select Additional Tasks" wizard page WizardSelectTasks=Zgjedh Detyrat Shtes SelectTasksDesc=Cilat detyra shtes t kryhen? SelectTasksLabel2=Zgjidhi detyrat shtes q duhet t kryej Sistemimi gjat instalimit t [name], pastaj kliko Tjetr. ; *** "Select Start Menu Folder" wizard page WizardSelectProgramGroup=Zgjedh Dosjen n Menyn Nis SelectStartMenuFolderDesc=Ku duhet t'i vendos Sistemimi shkurtoret e programit? SelectStartMenuFolderLabel3=Sistemimi do i krijoj shkurtoret e programit n kt dosje t Menys Nis. SelectStartMenuFolderBrowseLabel=Pr t vijuar, kliko Tjetr. Nse do t zgjedhsh nj dosje ndryshe, kliko Shfleto. MustEnterGroupName=Duhet shkruar emri i dosjes. GroupNameTooLong=Emri ose shtegu i dosjes sht tepr i gjat. InvalidGroupName=Emri i dosjes nuk ka vler. BadGroupName=Emri i dosjes nuk duhet t prmbaj asnj nga kto shkronja:%n%n%1 NoProgramGroupCheck2=&Mos krijo dosje n Menyn Nis ; *** "Ready to Install" wizard page WizardReady=Gati t Instalohet ReadyLabel1=Sistemimi sht gati t nis instalimin e [name] n kompjuter. ReadyLabel2a=Kliko Instalo pr t vijuar me instalimin, ose kliko Pas pr t rishikuar apo ndryshuar ndonj vendosje. ReadyLabel2b=Kliko Instalo pr t vijuar me instalimin. ReadyMemoUserInfo=Informacioni i prdoruesit: ReadyMemoDir=Destinacioni: ReadyMemoType=Lloji i sistemimit: ReadyMemoComponents=Prbrsit e zgjedhur: ReadyMemoGroup=Dosja n Menyn Nis: ReadyMemoTasks=Detyra shtes: ; *** "Preparing to Install" wizard page WizardPreparing=Gati t Instalohet PreparingDesc=Sistemimi sht gati t instaloj [name] n kompjuter. PreviousInstallNotCompleted=Nuk plotsohet instalimi/heqja e programit t mparshm. Kompjuteri duhet t rinis q t plotsohet instalimi.%n%nPasi kompjuteri t riniset, vepro prsri Sistemimin q t plotsohet instalimi i [name]. CannotContinue=Sistemimi nuk mund t vijoj. T lutem kliko Anulo pr t dal. ; *** "Installing" wizard page WizardInstalling=Instalim InstallingLabel=T lutem prit q Sistemimi t instaloj [name] n kompjuter. ; *** "Setup Completed" wizard page FinishedHeadingLabel=Plotson Udhrrfyesin pr Sistemimin e [name] FinishedLabelNoIcons=Sistemimi prfundon instalimin n kompjuter t [name]. FinishedLabel=Sistemimi prfundon instalimin n kompjuter t [name]. Aplikimi mund t lshohet duke zgjedhur ikonn e instaluar. ClickFinish=Kliko Prfundo pr t dal nga Sistemimi. FinishedRestartLabel=Q t prfundoj instalimi i [name], Sistemimi duhet t rinis kompjuterin. T rinis tani? FinishedRestartMessage=Q t prfundoj instalimi i [name], Sistemimi duhet t rinis kompjuterin.%n%nT rinis tani? ShowReadmeCheck=Po, dua t shikoj skedn README YesRadio=&Po, rinis kompjuterin tani NoRadio=&Jo, do rinis kompjuterin m von ; used for example as 'Run MyProg.exe' RunEntryExec=Lsho %1 ; used for example as 'View Readme.txt' RunEntryShellExec=Shiko %1 ; *** "Setup Needs the Next Disk" stuff ChangeDiskTitle=Sistemimi Krkon Diskun Tjetr SelectDiskLabel2=T lutem vendos Diskun %1 dhe kliko OK.%n%nNse skedat n kt disk mund t ndodhen n nj dosje ndryshe nga ajo e shfaqur ktu, vendos shtegun e sakt ose kliko Shfleto. PathLabel=&Shtegu: FileNotInDir2=Skeda "%1" nuk ndodhet n "%2". T lutem vendos diskun e sakt ose zgjidhe nj dosje tjetr. SelectDirectoryLabel=T lutem prcakto vendin e diskut tjetr. ; *** Installation phase messages SetupAborted=Nuk plotsohet sistemimi.%n%nT lutem korrigjo problemin dhe lshoje at prsri. EntryAbortRetryIgnore=Kliko Riprovo pr t provuar prsri, Prbuz pr t vijuar gjithsesi, ose Ndrprit q instalimi t anulohet. ; *** Installation status messages StatusCreateDirs=Krijon direktorit... StatusExtractFiles=Nxjerr skedat... StatusCreateIcons=Krijon shkurtoret... StatusCreateIniEntries=Krijon hyrjet INI... StatusCreateRegistryEntries=Krijon hyrjet n regjistr... StatusRegisterFiles=Regjistron skedat... StatusSavingUninstall=Ruan informacionin e instalimit... StatusRunProgram=Prfundon instalimin... StatusRollback=Kthen ndryshimin... ; *** Misc. errors ErrorInternal2=Gabim i brendshm: %1 ErrorFunctionFailedNoCode=%1 ndalon ErrorFunctionFailed=%1 ndalon; kodi %2 ErrorFunctionFailedWithMessage=%1 ndalon; kodi %2.%n%3 ErrorExecutingProgram=Kjo sked nuk ekzekutohet:%n%1 ; *** Registry errors ErrorRegOpenKey=Gabim n hapjen e kodit t regjistrit:%n%1\%2 ErrorRegCreateKey=Gabim n krijimin e kodit t regjistrit:%n%1\%2 ErrorRegWriteKey=Gabim n shkrimin e kodit t regjistrit:%n%1\%2 ; *** INI errors ErrorIniEntry=Gabim n krijimin e hyrjes INI te skeda "%1". ; *** File copying errors FileAbortRetryIgnore=Kliko Riprovo pr t provuar prsri, Prbuz pr ta kaluar kt sked (nuk rekomandohet), ose Ndrprit q instalimi t anulohet. FileAbortRetryIgnore2=Kliko Riprovo pr t provuar prsri, Prbuz pr t vijuar gjithsesi (nuk rekomandohet), ose Ndrprit q instalimi t anulohet. SourceIsCorrupted=sht prishur skeda e burimit SourceDoesntExist=Nuk ekziston skeda e burimit "%1" ExistingFileReadOnly=Skeda ekzistuese sht shnuar vetm pr lexim.%n%nKliko Riprovo pr t hequr atributin e leximit dhe provoje prsri, Prbuz pr ta kaluar kt sked, ose Ndrprit q instalimi t anulohet. ErrorReadingExistingDest=Ndodhi nj gabim gjat provs pr t lexuar skedn ekzistuese: FileExists=Skeda ekziston q m par.%n%nTa mbishkruaj Sistemimi at? ExistingFileNewer=Skeda ekzistuese sht me e re sesa ajo q Sistemimi provon t instaloj. Rekomandohet q t mbash skedn ekzistuese.%n%nDo mbash skedn ekzistuese? ErrorChangingAttr=Ndodhi nj gabim gjat provs pr t ndryshuar atributet e skeds ekzistuese: ErrorCreatingTemp=Ndodhi nj gabim gjat provs pr t krijuar nj sked n direktorin e destinacionit: ErrorReadingSource=Ndodhi nj gabim gjat provs pr t lexuar skedn burim: ErrorCopying=Ndodhi nj gabim gjat orvatjes pr t kopjuar nj sked: ErrorReplacingExistingFile=Ndodhi nj gabim gjat orvatjes pr t zvendsuar skedn ekzistuese: ErrorRestartReplace=Ndalon RestartReplace: ErrorRenamingTemp=Ndodhi nj gabim gjat riemrtimit t skeds n direktorin e destinacionit: ErrorRegisterServer=Nuk mund t regjistrohet DLL/OCX: %1 ErrorRegSvr32Failed=RegSvr32 ndalon me kodin e daljes %1 ErrorRegisterTypeLib=Nuk mund t regjistrohet lloji i libraris: %1 ; *** Post-installation errors ErrorOpeningReadme=Ndodhi nj gabim gjat hapjes s skeds README. ErrorRestartingComputer=Sistemimi nuk arriti t rinis kompjuterin. T lutem bje vet. ; *** Uninstaller messages UninstallNotFound=Skeda "%1" nuk ekziston. Nuk mund t instalohet. UninstallOpenError=Skeda "%1" nuk mund t hapet. Nuk mund t instalohet. UninstallUnsupportedVer=Ditari i instalimit "%1" ka nj format q nuk njihet nga ky version i instaluesit. Nuk mund t instalohet UninstallUnknownEntry=Te ditari i instalimit vrehet nj hyrje e panjohur (%1) ConfirmUninstall=Je i sigurt pr fshirjen e plot t %1 dhe tr prbrsit e vet? UninstallOnlyOnWin64=Ky instalim mund t instalohet vetm n Windows 64-bit. OnlyAdminCanUninstall=Ky instalim mund t instalohet vetm nga nj prdorues me privilegje administrimi. UninstallStatusLabel=T lutem prit ndrkoh q %1 fshihet nga kompjuteri. UninstalledAll=%1 u fshi me sukses nga kompjuteri. UninstalledMost=Prfundon instalimi i %1.%n%nDisa elemente nuk mund t fshiheshin. Kto mund t'i fshish vet. UninstalledAndNeedsRestart=Q t plotsohet instalimi i %1, kompjuteri duhet t riniset.%n%nT rinis tani? UninstallDataCorrupted="%1" sht sked e prishur. Nuk mund t instalohet. ; *** Uninstallation phase messages ConfirmDeleteSharedFileTitle=Fshin Skedn e Ndar? ConfirmDeleteSharedFile2=Sistemimi tregon se kjo sked e ndar nuk prdoret m nga asnj program. Ta fshij instalimi kt sked t ndar?%n%nNse ndonj program e prdor akoma skedn, kur ajo t fshihet programi mund t mos punoj si duhet. Nse nuk je i sigurt, zgjidhe Jo. Lnia e skeds n sistem nuk do shkaktoj dme. SharedFileNameLabel=Emri i skeds: SharedFileLocationLabel=Vendi: WizardUninstalling=Statusi i instalimit StatusUninstalling=instalon %1... ; The custom messages below aren't used by Setup itself, but if you make ; use of them in your scripts, you'll want to translate them. [CustomMessages] NameAndVersion=%1 versioni %2 AdditionalIcons=Ikona shtes: CreateDesktopIcon=Krijo nj ikon n &tryez CreateQuickLaunchIcon=Krijo nj ikon n &Quick Launch ProgramOnTheWeb=%1 n Rrjet UninstallProgram=instalo %1 LaunchProgram=Lsho %1 AssocFileExtension=&Shoqro %1 me sigln e skeds %2 AssocingFileExtension=%1 shoqrohet me sigln e skeds %2... ricochet-1.1.4/translation/inno/Bulgarian.isl000066400000000000000000000466221300720305500212400ustar00rootroot00000000000000; *** Inno Setup version 5.5.3+ Bulgarian messages *** ; Mikhail Balabanov ; ; , , : ; http://www.jrsoftware.org/files/istrans/ ; ; : , (.) , ; , Inno Setup ( ; ). [LangOptions] ; . , ; "[LangOptions]" . LanguageName=<0431><044A><043B><0433><0430><0440><0441><043A><0438> LanguageID=$0402 LanguageCodePage=1251 ; , , ; , - ; . ;DialogFontName= ;DialogFontSize=8 ;WelcomeFontName=Verdana ;WelcomeFontSize=12 ;TitleFontName=Arial ;TitleFontSize=29 ;CopyrightFontName=Arial ;CopyrightFontSize=8 [Messages] ; *** SetupAppTitle= SetupWindowTitle= %1 UninstallAppTitle= UninstallAppFullTitle= %1 ; *** InformationTitle= ConfirmTitle= ErrorTitle= ; *** SetupLdrStartupMessage= %1. ? LdrCannotCreateTemp= . LdrCannotExecTemp= . ; *** LastErrorMessage=%1.%n%n %2: %3 SetupFileMissing= %1 . , . SetupFileCorrupt= . , . SetupFileCorruptOrWrongVer= . , . InvalidParameter= :%n%n%1 SetupAlreadyRunning= . WindowsVersionNotSupported= Windows, . WindowsServicePackRequired= %1 Service Pack %2 -. NotOnThisPlatform= %1. OnlyOnThisPlatform= %1. OnlyOnTheseArchitectures= Windows :%n%n%1 MissingWOW64APIs= Windows , 64- . , Service Pack %1. WinVersionTooLowError= %1 %2 -. WinVersionTooHighError= %1 %2 -. AdminPrivilegesRequired= , . PowerUserPrivilegesRequired= , . SetupAppRunningError= , %1 .%n%n, "OK", , "Cancel" . UninstallAppRunningError= , %1 .%n%n, "OK", , "Cancel" . ; *** ErrorCreatingDir= "%1" ErrorTooManyFilesInDir= "%1", ; *** ExitSetupTitle= ExitSetupMessage= . , .%n%n- , .%n%n ? AboutSetupMenuItem=& ... AboutSetupTitle= AboutSetupMessage=%1 %2%n%3%n%n:%n%4 AboutSetupNote= TranslatorNote= : ; *** ButtonBack=< & ButtonNext=& > ButtonInstall=& ButtonOK=OK ButtonCancel= ButtonYes=& ButtonYesToAll= & ButtonNo=& ButtonNoToAll= & ButtonFinish=& ButtonBrowse=&... ButtonWizardBrowse=&... ButtonNewFolder=& ; *** SelectLanguageTitle= SelectLanguageLabel= : ; *** ClickNext= "", , "" . BeveledLabel= BrowseDialogTitle= BrowseDialogLabel= "OK". NewFolderName= ; *** " " WelcomeLabel1= [name] WelcomeLabel2= [name/ver] .%n%n , . ; *** "" WizardPassword= PasswordLabel1= . PasswordLabel3=, "", . . PasswordEditLabel=&: IncorrectPassword= . , . ; *** " " WizardLicense= LicenseLabel=, , . LicenseLabel3=, . , . LicenseAccepted=& LicenseNotAccepted=& ; *** "" WizardInfoBefore= InfoBeforeLabel=, , . InfoBeforeClickLabel= , "". WizardInfoAfter= InfoAfterLabel=, , . InfoAfterClickLabel= , "". ; *** " " WizardUserInfo= UserInfoDesc=, . UserInfoName=&: UserInfoOrg=&: UserInfoSerial=& : UserInfoNameRequired= . ; *** " " WizardSelectDir= SelectDirDesc= [name]? SelectDirLabel3=[name] . SelectDirBrowseLabel= "", . , "". DiskSpaceMBLabel= [mb] . CannotInstallToNetworkDrive= . CannotInstallToUNCPath= UNC . InvalidPath= , :%n%nC:\APP%n%n UNC :%n%n\\\ InvalidDrive= UNC . , . DiskSpaceWarningTitle= DiskSpaceWarning= %1 , %2 .%n%n ? DirNameTooLong= . InvalidDirName= . BadDirName32= :%n%n%1 DirExistsTitle= DirExists=:%n%n%1%n%n . ? DirDoesntExistTitle= DirDoesntExist=:%n%n%1%n%n . ? ; *** " " WizardSelectComponents= SelectComponentsDesc= ? SelectComponentsLabel2= , , . "", . FullInstallation= ; "Compact" "Minimal" ( "Minimal" ) CompactInstallation= CustomInstallation= NoUninstallWarningTitle= NoUninstallWarning= , a:%n%n%1%n%n .%n%n ? ComponentSize1=%1 ComponentSize2=%1 ComponentsDiskSpaceMBLabel= [mb] . ; *** " " WizardSelectTasks= SelectTasksDesc= ? SelectTasksLabel2= [name], "". ; *** " "" WizardSelectProgramGroup= "" SelectStartMenuFolderDesc= ? SelectStartMenuFolderLabel3= "". SelectStartMenuFolderBrowseLabel= "", . , "". MustEnterGroupName= . GroupNameTooLong= . InvalidGroupName= . BadGroupName= :%n%n%1 NoProgramGroupCheck2=& "" ; *** " " WizardReady= ReadyLabel1= [name] . ReadyLabel2a= "", , "" . ReadyLabel2b= "", . ReadyMemoUserInfo= : ReadyMemoDir=: ReadyMemoType= : ReadyMemoComponents= : ReadyMemoGroup= "": ReadyMemoTasks= : ; *** " " WizardPreparing= PreparingDesc= [name] . PreviousInstallNotCompleted= . , .%n%n , , [name]. CannotContinue= . , "" . ApplicationsFound= , . . ApplicationsFound2= , . . . CloseApplications= & DontCloseApplications= & ErrorCloseApplications= . , , , . ; *** "" WizardInstalling= InstallingLabel=, [name] . ; *** " " FinishedHeadingLabel= [name] FinishedLabelNoIcons= [name] . FinishedLabel= [name] . . ClickFinish= "", . FinishedRestartLabel= , [name]. ? FinishedRestartMessage= , [name].%n%n ? ShowReadmeCheck=, README YesRadio=&, NoRadio=&, - ; " MyProg.exe" RunEntryExec= %1 ; " Readme.txt" RunEntryShellExec= %1 ; *** " " ChangeDiskTitle= SelectDiskLabel2=, %1 "".%n%n - , "". PathLabel=&: FileNotInDir2= "%1" "%2". , . SelectDirectoryLabel=, . ; *** "" SetupAborted= .%n%n, . EntryAbortRetryIgnore= "Retry" , "Ignore" "Abort" . ; *** StatusClosingApplications= ... StatusCreateDirs= ... StatusExtractFiles= ... StatusCreateIcons= ... StatusCreateIniEntries= INI ... StatusCreateRegistryEntries= ... StatusRegisterFiles= ... StatusSavingUninstall= ... StatusRunProgram= ... StatusRestartingApplications= ... StatusRollback= ... ; *** ErrorInternal2= : %1 ErrorFunctionFailedNoCode= %1 ErrorFunctionFailed= %1; : %2 ErrorFunctionFailedWithMessage= %1; : %2.%n%3 ErrorExecutingProgram= :%n%1 ; *** , ErrorRegOpenKey= :%n%1\%2 ErrorRegCreateKey= :%n%1\%2 ErrorRegWriteKey= :%n%1\%2 ; *** , INI ErrorIniEntry= INI "%1". ; *** FileAbortRetryIgnore= "Retry" , "Ignore" ( ) "Abort" . FileAbortRetryIgnore2= "Retry" , "Ignore" ( ) "Abort" . SourceIsCorrupted= - SourceDoesntExist= - "%1" ExistingFileReadOnly= " ".%n%n "Retry" , "Ignore" "Abort" . ErrorReadingExistingDest= : FileExists= .%n%n ? ExistingFileNewer= - , . .%n%n ? ErrorChangingAttr= : ErrorCreatingTemp= : ErrorReadingSource= - : ErrorCopying= : ErrorReplacingExistingFile= : ErrorRestartReplace= : ErrorRenamingTemp= : ErrorRegisterServer= DLL/OCX: %1 ErrorRegSvr32Failed= RegSvr32 %1 ErrorRegisterTypeLib= : %1 ; *** ErrorOpeningReadme= README. ErrorRestartingComputer= . , . ; *** UninstallNotFound= "%1" . . UninstallOpenError= "%1" . UninstallUnsupportedVer= "%1" . UninstallUnknownEntry= (%1) ConfirmUninstall= %1 ? UninstallOnlyOnWin64= 64- Windows. OnlyAdminCanUninstall= . UninstallStatusLabel=, %1 . UninstalledAll=%1 . UninstalledMost= %1 .%n%n . . UninstalledAndNeedsRestart= %1, .%n%n ? UninstallDataCorrupted= "%1" . ; *** "" ConfirmDeleteSharedFileTitle= ? ConfirmDeleteSharedFile2= , . ?%n%n , . , "". . SharedFileNameLabel= : SharedFileLocationLabel=: WizardUninstalling= StatusUninstalling=%1 ... ; *** ShutdownBlockReasonInstallingApp= %1. ShutdownBlockReasonUninstallingApp= %1. ; - , ; , . [CustomMessages] NameAndVersion=%1, %2 AdditionalIcons= : CreateDesktopIcon= & CreateQuickLaunchIcon= "& " ProgramOnTheWeb=%1 UninstallProgram= %1 LaunchProgram= %1 AssocFileExtension=& %1 %2 AssocingFileExtension=%1 %2... AutoStartProgramGroupDescription=: AutoStartProgram= %1 AddonHostProgramNotFound=%1 .%n%n ? ricochet-1.1.4/translation/inno/ChineseSimplified.isl000066400000000000000000000335151300720305500227150ustar00rootroot00000000000000; *** Inno Setup version 5.5.3+ Chinese (Simplified) messages *** ; By Qiming Li (qiming at clault.com) ; ; To download user-contributed translations of this file, go to: ; http://www.jrsoftware.org/files/istrans/ ; ; Note: When translating this text, do not add periods (.) to the end of ; messages that didn't have them already, because on those messages Inno ; Setup adds the periods automatically (appending a period would result in ; two periods being displayed). [LangOptions] ; The following three entries are very important. Be sure to read and ; understand the '[LangOptions] section' topic in the help file. LanguageName=<4E2D><6587><7B80><4F53> LanguageID=$0804 LanguageCodePage=936 ; If the language you are translating to requires special font faces or ; sizes, uncomment any of the following entries and change them accordingly. DialogFontName= ;DialogFontSize=8 ;WelcomeFontName=Verdana ;WelcomeFontSize=12 ;TitleFontName=Arial ;TitleFontSize=29 ;CopyrightFontName=Arial ;CopyrightFontSize=8 [Messages] ; *** Application titles SetupAppTitle=װ SetupWindowTitle=װ - %1 UninstallAppTitle=ж UninstallAppFullTitle=%1ж ; *** Misc. common InformationTitle=Ϣ ConfirmTitle=ȷ ErrorTitle= ; *** SetupLdr messages SetupLdrStartupMessage=װ򵼽ĵϰװ%1ȷҪ LdrCannotCreateTemp=޷ʱļװֹ LdrCannotExecTemp=޷ʱļеļװֹ ; *** Startup error messages LastErrorMessage=%1.%n%n %2: %3 SetupFileMissing=װĿ¼ȱʧļ%1⣬»ȡһݳ򿽱 SetupFileCorrupt=װļѱ𻵡»ȡһݳ򿽱 SetupFileCorruptOrWrongVer=װļѱ𻵣뱾װ򵼰汾ݡ⣬»ȡһݳ򿽱 InvalidParameter=Чв%n%n%1 SetupAlreadyRunning=װѾС WindowsVersionNotSupported=֧еWindows汾 WindowsServicePackRequired=Ҫ%1 Service Pack %2°汾 NotOnThisPlatform=򲻿%1С OnlyOnThisPlatform=%1С OnlyOnTheseArchitectures=ֻΪ´ܹƵWindows汾ϰװ%n%n%1 MissingWOW64APIs=ʹõWindows汾ûа64λװĹܡ밲װService Pack %1⡣ WinVersionTooLowError=Ҫ%2汾ϵ%1 WinVersionTooHighError=򲻿ɰװ%2߰汾%1ϡ AdminPrivilegesRequired=¼ΪԱܰװ˳ PowerUserPrivilegesRequired=¼ΪԱȨûܰװ˳ SetupAppRunningError=װ򵼼⵽%1С%n%nرдڲȷȡ˳װ UninstallAppRunningError=ж򵼼⵽%1С%n%nرдڣȻȷȡ˳ ; *** Misc. errors ErrorCreatingDir=װ޷ļС%1 ErrorTooManyFilesInDir=ļС%1ļ࣬޷дļ ; *** Setup common messages ExitSetupTitle=˳װ ExitSetupMessage=װδɡ˳򽫲ᱻװ %n%n´аװɳİװ%n%nȷ˳װ AboutSetupMenuItem=ڰװ(&A) AboutSetupTitle=ڰװ AboutSetupMessage=%1汾%2%n%3%n%n%1ҳ%n%4 AboutSetupNote= TranslatorNote= ; *** Buttons ButtonBack=< һ(&B) ButtonNext=һ(&N) > ButtonInstall=װ(&I) ButtonOK=ȷ ButtonCancel=ȡ ButtonYes=(&Y) ButtonYesToAll=ȫѡ(&A) ButtonNo=(&N) ButtonNoToAll=ȫѡ(&O) ButtonFinish=(&F) ButtonBrowse=(&B) ButtonWizardBrowse=(&R) ButtonNewFolder=ļ(&M) ; *** "Select Language" dialog messages SelectLanguageTitle=ѡ SelectLanguageLabel=ѡװʱʹԣ ; *** Common wizard text ClickNext=һȡ˳װ򵼡 BeveledLabel= BrowseDialogTitle=ѡļ BrowseDialogLabel=бѡȡһļУȷ NewFolderName=½ļ ; *** "Welcome" wizard page WelcomeLabel1=ӭʹ[name]װ WelcomeLabel2=򵼽ĵϰװ[name/ver]%n%nڼ֮ǰرӦó ; *** "Password" wizard page WizardPassword= PasswordLabel1=װ뱣 PasswordLabel3=룬һִСд PasswordEditLabel=(&P) IncorrectPassword=벻ȷԡ ; *** "License Agreement" wizard page WizardLicense=Э LicenseLabel=ĶҪϢȻٽһ LicenseLabel3=ĶЭ顣ܴЭȻܼװ LicenseAccepted=ҽЭ(&A) LicenseNotAccepted=ҲЭ(&D) ; *** "Information" wizard pages WizardInfoBefore=Ϣ InfoBeforeLabel=ĶҪϢٽһ InfoBeforeClickLabel=׼üװ󣬵һ WizardInfoAfter=Ϣ InfoAfterLabel=ĶҪϢٽһ InfoAfterClickLabel=׼üװ󣬵һ ; *** "User Information" wizard page WizardUserInfo=ûϢ UserInfoDesc=Ϣ UserInfoName=û(&U) UserInfoOrg=(&O) UserInfoSerial=к(&S) UserInfoNameRequired=û ; *** "Select Destination Location" wizard page WizardSelectDir=ѡװλ SelectDirDesc=[name]װδ SelectDirLabel3=װ򵼽[name]װļС SelectDirBrowseLabel=һҪѡͬļУ DiskSpaceMBLabel=[mb]ֽڣMBô̿ռ䡣 CannotInstallToNetworkDrive=޷װ CannotInstallToUNCPath=޷װUNC· InvalidPath=̷·磺%n%nC:\Ӧó%n%n¸ʽUNC·%n%n\\\Ŀ¼ InvalidDrive=ѡUNCڻ򲻿ɷʡѡһ DiskSpaceWarningTitle=̿ռ䲻 DiskSpaceWarning=%1ǧֽڣKBÿռſɰװѡ%2ǧֽڣKBÿռ䡣%n%nȷҪ DirNameTooLong=ļƻ·̫ InvalidDirName=ļЧ BadDirName32=ļƲַܰ%n%n%1 DirExistsTitle=ļѴ DirExists=ļ%n%n%1%n%nѴڡȷҪװļ DirDoesntExistTitle=ļв DirDoesntExist=ļ%n%n%1%n%nڡҪļ ; *** "Select Components" wizard page WizardSelectComponents=ѡ SelectComponentsDesc=ҪװЩ SelectComponentsLabel2=ѡҪװҪװ׼úһ FullInstallation=ȫװ ; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language) CompactInstallation=లװ CustomInstallation=Զ尲װ NoUninstallWarningTitle=Ѵ NoUninstallWarning=װ򵼼⵽Ѿװ%n%n%1%n%nȡѡжЩ%n%nȷҪװ ComponentSize1=%1ǧֽڣKB ComponentSize2=%1ֽڣMB ComponentsDiskSpaceMBLabel=ĿǰѡҪ[mb]ֽڣMB̿ռ䡣 ; *** "Select Additional Tasks" wizard page WizardSelectTasks=ѡ񸽼 SelectTasksDesc=ҪִЩ SelectTasksLabel2=ѡװ[name]ʱҪִеĸȻһ ; *** "Select Start Menu Folder" wizard page WizardSelectProgramGroup=ѡʼ˵ļ SelectStartMenuFolderDesc=ѳݷʽŵ SelectStartMenuFolderLabel3=װ򵼽¿ʼ˵ļдݷʽ SelectStartMenuFolderBrowseLabel=һҪѡһļУ MustEnterGroupName=ļ GroupNameTooLong=ļƻ·̫ InvalidGroupName=ļЧ BadGroupName=ļƲַܰ%n%n%1 NoProgramGroupCheck2=Ҫʼ˵ļ(&D) ; *** "Ready to Install" wizard page WizardReady=װ׼ ReadyLabel1=װ׼ϣʼĵϰװ[name] ReadyLabel2a=װʼװҪȷϻһ ReadyLabel2b=װʼװ ReadyMemoUserInfo=ûϢ ReadyMemoDir=װλã ReadyMemoType=װͣ ReadyMemoComponents=ѡ ReadyMemoGroup=ʼ˵ļУ ReadyMemoTasks= ; *** "Preparing to Install" wizard page WizardPreparing=׼װ PreparingDesc=װ׼ĵϰװ[name] PreviousInstallNotCompleted=ϴγװ/жδɡҪϴΰװ%n%n֮аװװ[name] CannotContinue=װ޷ȡ˳ ApplicationsFound=װҪµļӦóռáװԶرЩӦó ApplicationsFound2=װҪµļӦóռáװԶرЩӦó򡣰װɺ󣬰װ򵼽ЩӦó CloseApplications=ԶرӦó(&A) DontCloseApplications=ԶرӦó(&D) ErrorCloseApplications=װ޷ԶرеӦóڽһ֮ǰرЩռðװҪļӦó ; *** "Installing" wizard page WizardInstalling=ڰװ InstallingLabel=Ժ򣬰װĵϰװ[name] ; *** "Setup Completed" wizard page FinishedHeadingLabel=[name]װ FinishedLabelNoIcons=װĵϰװ[name] FinishedLabel=װĵϰװ[name]ͨѰװĿݷʽ򿪴Ӧó ClickFinish=˳װ FinishedRestartLabel=Ϊ[name]İװװ򵼱ĵԡҪ FinishedRestartMessage=Ϊ[name]İװװ򵼱ĵԡ%n%nҪ ShowReadmeCheck=ǣҪĶļ YesRadio=ǣ(&Y) NoRadio=Ժ(&N) ; used for example as 'Run MyProg.exe' RunEntryExec=%1 ; used for example as 'View Readme.txt' RunEntryShellExec=%1 ; *** "Setup Needs the Next Disk" stuff ChangeDiskTitle=װҪһŴ SelectDiskLabel2=%1 ȷ%n%nôеļʾļУȷ· PathLabel=·(&P) FileNotInDir2=ļ%1ڡ%2СȷĴ̻ѡļС SelectDirectoryLabel=ָһŴ̵λá ; *** Installation phase messages SetupAborted=װδɡ%n%nаװ򵼡 EntryAbortRetryIgnore=ԡ³ԣԡװֹȡװ ; *** Installation status messages StatusClosingApplications=ڹرӦó StatusCreateDirs=ڴļС StatusExtractFiles=ȡļ StatusCreateIcons=ڴݷʽ StatusCreateIniEntries=ڴINIĿ StatusCreateRegistryEntries=ڴעĿ StatusRegisterFiles=ڴעĿ StatusSavingUninstall=ڱжϢ StatusRunProgram=ڽװ StatusRestartingApplications=Ӧó StatusRollback=ڳġ ; *** Misc. errors ErrorInternal2=ڲ%1 ErrorFunctionFailedNoCode=%1ʧ ErrorFunctionFailed=%1ʧܣ%2 ErrorFunctionFailedWithMessage=%1ʧܣ%2%n%3 ErrorExecutingProgram=޷г%n%1 ; *** Registry errors ErrorRegOpenKey=עʱ%n%1\%2 ErrorRegCreateKey=עʱ%n%1\%2 ErrorRegWriteKey=дעʱ%n%1\%2 ; *** INI errors ErrorIniEntry=ļ%1дINIĿʱ ; *** File copying errors FileAbortRetryIgnore=ԡ³ԣԡļƼֹȡװ FileAbortRetryIgnore2=ԡ³ԣԡװƼֹȡװ SourceIsCorrupted=Դļ SourceDoesntExist=Դļ%1 ExistingFileReadOnly=ļΪֻ%n%nԡƳֻԲ³ԣԡļֹȡװ ErrorReadingExistingDest=ȡļʱ FileExists=ļѴڡ%n%nðװ򵼸 ExistingFileNewer=ļȰװͼװĻҪ¡鱣ļ%n%nҪļ ErrorChangingAttr=ļʱ ErrorCreatingTemp=Ŀļдļʱ ErrorReadingSource=ȡԴļʱ ErrorCopying=ļʱ ErrorReplacingExistingFile=滻ļʱ ErrorRestartReplace=滻ʧܣ ErrorRenamingTemp=ΪĿļļʱ ErrorRegisterServer=޷עᶯ̬ؼDLL/OCX%1 ErrorRegSvr32Failed=RegSvr32ʧܣ䷵ֵΪ%1 ErrorRegisterTypeLib=޷עͿ⣺%1 ; *** Post-installation errors ErrorOpeningReadme=ļʱ ErrorRestartingComputer=װ޷ԡֶ ; *** Uninstaller messages UninstallNotFound=ļ%1ڡ޷жء UninstallOpenError=޷ļ%1޷ж UninstallUnsupportedVer=˰汾ж޷ʶж־ļ%1ĸʽ޷ж UninstallUnknownEntry=ж־δ֪Ŀ (%1) ConfirmUninstall=ǷȷҪȫɾ%1 UninstallOnlyOnWin64=˰װֻ64λWindowsжء OnlyAdminCanUninstall=˰װֻɾ߱ԱȨ޵ûжء UninstallStatusLabel=Ժɾ%1 UninstalledAll=ѳɹشĵɾ%1 UninstalledMost=%1жϡ%n%nijЩĿ޷жعɾֶɾЩĿ UninstalledAndNeedsRestart=Ҫ%1жأԡ%n%nҪ UninstallDataCorrupted=ļ%1𻵡޷ж ; *** Uninstallation phase messages ConfirmDeleteSharedFileTitle=ɾļ ConfirmDeleteSharedFile2=ϵͳʾûκγʹ¹ļҪɾùļ%n%nгʹøļɾЩ޷Сȷѡ񡰷񡱡¸ļϵͳκΣ SharedFileNameLabel=ļ SharedFileLocationLabel=λã WizardUninstalling=ж״̬ StatusUninstalling=ж%1 ; *** Shutdown block reasons ShutdownBlockReasonInstallingApp=ڰװ%1 ShutdownBlockReasonUninstallingApp=ж%1 ; The custom messages below aren't used by Setup itself, but if you make ; use of them in your scripts, you'll want to translate them. [CustomMessages] NameAndVersion=%1汾%2 AdditionalIcons=ӿݷʽ CreateDesktopIcon=ݷʽ(&D) CreateQuickLaunchIcon=ݷʽ(&Q) ProgramOnTheWeb=%1վ UninstallProgram=ж%1 LaunchProgram=%1 AssocFileExtension=%1%2ļչ(&A) AssocingFileExtension=ڽ%1%2ļչ AutoStartProgramGroupDescription= AutoStartProgram=Զ%1 AddonHostProgramNotFound=ѡļҲ%1%n%nǷȻ ricochet-1.1.4/translation/inno/Estonian.isl000066400000000000000000000432061300720305500211070ustar00rootroot00000000000000; *** Inno Setup version 5.5.3+ Estonian messages *** ; ; Estonian translation by LiivaneLord ; E-mail: liivane.lord@mail.ee ; Last modification date: 2013-01-09 ; Tlge baseerub rix'i tlkele, mida on parandatud ja kohandatud uuemale versioonile. ; ; To download user-contributed translations of this file, go to: ; http://www.jrsoftware.org/files/istrans/ ; ; Note: When translating this text, do not add periods (.) to the end of ; messages that didn't have them already, because on those messages Inno ; Setup adds the periods automatically (appending a period would result in ; two periods being displayed). [LangOptions] ; The following three entries are very important. Be sure to read and ; understand the '[LangOptions] section' topic in the help file. LanguageName=Eesti LanguageID=$0425 LanguageCodePage=1257 ; If the language you are translating to requires special font faces or ; sizes, uncomment any of the following entries and change them accordingly. ;DialogFontName= ;DialogFontSize=8 ;WelcomeFontName=Verdana ;WelcomeFontSize=12 ;TitleFontName=Arial ;TitleFontSize=29 ;CopyrightFontName=Arial ;CopyrightFontSize=8 [Messages] ; *** Application titles SetupAppTitle=Paigalda SetupWindowTitle=Paigalda %1 UninstallAppTitle=Eemalda UninstallAppFullTitle=Eemalda %1 ; *** Misc. common InformationTitle=Informatsioon ConfirmTitle=Kinnita ErrorTitle=Viga ; *** SetupLdr messages SetupLdrStartupMessage=Paigaldatakse %1. Kas soovid jtkata? LdrCannotCreateTemp=Ei saanud luua ajutist faili. Paigaldamine katkestati LdrCannotExecTemp=Ei saanud kivitada faili ajutises kataloogis. Paigaldamine katkestati ; *** Startup error messages LastErrorMessage=%1.%n%nViga %2: %3 SetupFileMissing=%1 on paigaldamise kaustast kadunud. Palun paranda see viga vi hangi programmi uus koopia. SetupFileCorrupt=Paigaldaja failid on rikutud. Palun hangi programmi uus koopia. SetupFileCorruptOrWrongVer=Paigaldaja failid on kas rikutud vi ei tta selle paigaldaja versiooniga. Palun paranda see viga vi hangi programmi uus koopia. InvalidParameter=Ksureale anti vale parameeter:%n%n%1 SetupAlreadyRunning=Paigaldaja alles ttab. WindowsVersionNotSupported=Seda programmi ei saa selle Windowsi versiooniga kasutada, mis arvutis praegu ttab. WindowsServicePackRequired=See programm vajab %1 Service Pack (Hoolduspakett) %2 vi uuemat. NotOnThisPlatform=See programm ei tta %1'i platvormil. OnlyOnThisPlatform=See programm peab ttama %1'i platvormil. OnlyOnTheseArchitectures=Seda programmi saab paigaldada ainult neile Windowsi versioonidele, mis on vlja ttatud jrgmistele protsessori arhitektuuridele:%n%n%1 MissingWOW64APIs=Sinu arvutis ttaval Windowsil puuduvad 64-bitise paigaldamise jaoks vajalik funktsionaalsus. Selle probleemi parandamiseks paigalda palun Service Pack (Hoolduspakett) %1. WinVersionTooLowError=See programm vajab %1 versiooniga %2 vi uuemat. WinVersionTooHighError=Seda programmi ei saa paigaldada %1 versiooniga %2 vi uuema puhul. AdminPrivilegesRequired=Selle programmi paigaldamiseks pead olema administraatorina sisse logitud. PowerUserPrivilegesRequired=Selle programmi paigaldamiseks pead olema sisse logitud administraatorina vi Power user liikmena. SetupAppRunningError=Paigaldaja tuvastas, et %1 ttab hetkel.%n%nPalun sulge see programm ning seejrel jtkamiseks vajuta OK, katkestamiseks Katkesta. UninstallAppRunningError=Eemaldaja tuvastas, et %1 ttab hetkel.%n%nPalun sulge see programm ning seejrel jtkamiseks vajuta OK, katkestamiseks Katkesta. ; *** Misc. errors ErrorCreatingDir=Paigaldaja ei saanud luua kataloogi "%1" ErrorTooManyFilesInDir=Ei saanud luua faili kataloogi "%1", kuna seal on juba liiga palju faile ; *** Setup common messages ExitSetupTitle=Vlju paigaldajast ExitSetupMessage=Paigaldamine pole valmis. Kui praegu vljud, siis programmi ei paigaldata.%n%nPaigaldamise lpetamiseks vid paigaldaja mni teine kord uuesti kivitada.%n%nSoovid vljuda paigaldajast? AboutSetupMenuItem=&Teave paigaldajast... AboutSetupTitle=Teave paigaldajast AboutSetupMessage=%1 versiooniga %2%n%3%n%n%1 koduleht:%n%4 AboutSetupNote= TranslatorNote=Tlkis LiivaneLord (liivane[dot]lord[at]mail[dot]ee) ; *** Buttons ButtonBack=< &Tagasi ButtonNext=&Edasi > ButtonInstall=&Paigalda ButtonOK=OK ButtonCancel=Katkesta ButtonYes=&Jah ButtonYesToAll=Kikidele J&ah ButtonNo=&Ei ButtonNoToAll=Kikidele E&i ButtonFinish=&Valmis ButtonBrowse=&Sirvi... ButtonWizardBrowse=S&irvi... ButtonNewFolder=&Loo uus kaust ; *** "Select Language" dialog messages SelectLanguageTitle=Vali paigaldaja keel SelectLanguageLabel=Vali keel, mida soovid kasutada paigaldamise kigus: ; *** Common wizard text ClickNext=Jtkamiseks vajuta Edasi, paigaldajast vljumiseks vajuta Katkesta. BeveledLabel= BrowseDialogTitle=Sirvi kausta BrowseDialogLabel=Vali allolevast nimekirjast kaust ja vajuta OK. NewFolderName=Uus kaust ; *** "Welcome" wizard page WelcomeLabel1=Tere tulemast [name] paigaldaja viisardisse WelcomeLabel2=Sinu arvutisse paigaldatakse [name/ver].%n%nEnne jtkamist on soovitatav sulgeda kik muud programmid. ; *** "Password" wizard page WizardPassword=Parool PasswordLabel1=See paigaldaja on kaitstud parooliga. PasswordLabel3=Palun sisesta parool ja vajuta Edasi. Paroolid on tstutundlikud. PasswordEditLabel=&Parool: IncorrectPassword=Sisestatud parool on vale. Palun proovi uuesti. ; *** "License Agreement" wizard page WizardLicense=Litsentsileping LicenseLabel=Palun loe enne jtkamist see informatsioon lbi. LicenseLabel3=Palun loe jrgnevat litsentsilepingut. Paigaldamise jtkamiseks pead nustuma selle lepingu tingimustega. LicenseAccepted=Ma &nustun lepinguga LicenseNotAccepted=Ma &ei nustu lepinguga ; *** "Information" wizard pages WizardInfoBefore=Informatsioon InfoBeforeLabel=Palun loe enne jtkamist see oluline informatsioon lbi. InfoBeforeClickLabel=Kui oled valmis jtkama paigaldamist, vajuta Edasi. WizardInfoAfter=Informatsioon InfoAfterLabel=Palun loe enne jtkamist see oluline informatsioon lbi. InfoAfterClickLabel=Kui oled valmis jtkama paigaldamist, vajuta Edasi. ; *** "User Information" wizard page WizardUserInfo=Andmed kasutaja kohta UserInfoDesc=Palun sisesta oma andmed. UserInfoName=&Kasutaja nimi: UserInfoOrg=&Organisatsioon: UserInfoSerial=&Seerianumber: UserInfoNameRequired=Pead sisestama nime. ; *** "Select Destination Location" wizard page WizardSelectDir=Vali programmile kaust SelectDirDesc=Kuhu [name] paigaldada? SelectDirLabel3=Paigaldaja paigaldab [name]'i jrgnevasse kausta. SelectDirBrowseLabel=Jtkamiseks vajuta Edasi. Kui soovid valida muu kausta, vajuta Sirvi. DiskSpaceMBLabel=Programm vajab vhemalt [mb] MB vaba ruumi. CannotInstallToNetworkDrive=Programmi ei saa paigaldada vrgudraivile. CannotInstallToUNCPath=Programmi ei saa paigaldada UNC kataloogi. InvalidPath=Pead sisestama tispika draivitee koos draivithisega; niteks:%n%nC:\APP%n%nvi UNC kataloog kujul:%n%n\\server\share InvalidDrive=Sinu valitud draivi vi UNC kataloogi ei eksisteeri vi puudub sellele ligips. Palun vali mni teine. DiskSpaceWarningTitle=Pole piisavalt ruumi DiskSpaceWarning=Paigaldamiseks on vaja vhemalt %1 KB vaba ruumi, aga valitud draivil on vaba ainult %2 KB.%n%nKas soovid sellegipoolest jtkata? DirNameTooLong=Kausta nimi vi kaustatee on liiga pikk InvalidDirName=Kausta nimi on vale. BadDirName32=Kausta nimed ei tohi sisaldada htegi jrgnevatest smbolitest:%n%n%1 DirExistsTitle=Kaust on olemas DirExists=Kaust:%n%n%1%n%non juba olemas. Kas soovid sellegipoolest sinna paigaldada? DirDoesntExistTitle=Kaust puudub DirDoesntExist=Kaust:%n%n%1%n%npuudub. Kas soovid, et see kaust luuakse? ; *** "Select Components" wizard page WizardSelectComponents=Vali komponendid SelectComponentsDesc=Millised komponendid paigaldada? SelectComponentsLabel2=Vali komponendid, mida paigaldada; eemalda mrgid komponentidelt, mida ei soovi paigaldada. Kui oled valmis jtkama, vajuta Edasi. FullInstallation=Tielik paigaldamine ; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language) CompactInstallation=Kompaktne paigaldamine CustomInstallation=Kohandatud paigaldamine NoUninstallWarningTitle=Komponendid on juba olemas NoUninstallWarning=Paigaldaja tuvastas, et jrgnevad komponendid on sinu arvutis juba olemas:%n%n%1%n%nNende mittevalimine ei eemalda neid.%n%nKas soovid sellegipoolest jtkata? ComponentSize1=%1 KB ComponentSize2=%1 MB ComponentsDiskSpaceMBLabel=Praegune valik vajab vhemalt [mb] MB vaba ruumi. ; *** "Select Additional Tasks" wizard page WizardSelectTasks=Vali tiendavad lesanded SelectTasksDesc=Milliseid tiendavaid lesanded tita? SelectTasksLabel2=Vali, milliseid tiendavaid lesandeid [name] paigaldaja peab titma ja vajuta Edasi. ; *** "Select Start Menu Folder" wizard page WizardSelectProgramGroup=Vali Start men kaust SelectStartMenuFolderDesc=Kuhu luua programmi otseteed? SelectStartMenuFolderLabel3=Paigaldaja loob programmi otseteed jrgnevasse Start men kausta. SelectStartMenuFolderBrowseLabel=Jtkamiseks vajuta Edasi. Kui soovid valida muu kausta, vajuta Sirvi. MustEnterGroupName=Pead sisestama kausta nime. GroupNameTooLong=Kausta nimi vi kaustatee on liiga pikk. InvalidGroupName=Kausta nimi on vale. BadGroupName=Kausta nimi ei tohi sisaldada jrgnevatest smbolitest:%n%n%1 NoProgramGroupCheck2=&ra loo Start men kausta ; *** "Ready to Install" wizard page WizardReady=Valmis paigaldama ReadyLabel1=Paigaldaja on valmis paigaldama [name]'i sinu arvutisse. ReadyLabel2a=Paigaldamise jtkamiseks vajuta Paigalda vi vajuta Tagasi, et nha vi muuta seadeid. ReadyLabel2b=Paigaldamise jtkamiseks vajuta Paigalda. ReadyMemoUserInfo=Kasutaja andmed: ReadyMemoDir=Sihtkaust: ReadyMemoType=Paigalduse tp: ReadyMemoComponents=Valitud komponendid: ReadyMemoGroup=Start men kaust: ReadyMemoTasks=Lisalesanded: ; *** "Preparing to Install" wizard page WizardPreparing=Paigaldamiseks valmistumine PreparingDesc=Paigaldaja valmistub paigaldama [name]'i sinu arvutisse. PreviousInstallNotCompleted=Eelmise programmi paigaldamine/eemaldamine ei ole lpetatud. Paigaldamise lpetamiseks pead arvuti taaskivitama.%n%nPrast taaskivitust kivitage [name]'i paigaldaja uuesti, et lpetada paigaldamine. CannotContinue=Paigaldaja ei saa jtkata. Vljumiseks vajuta palun Katkesta. ApplicationsFound=Jrgnevad rakendused kasutavad faile, mida paigaldaja peab uuendama. Soovitatav on lubada paigaldajal need rakendused automaatselt sulgeda. ApplicationsFound2=Jrgnevad rakendused kasutavad faile, mida paigaldaja peab uuendama. Soovitatav on lubada paigaldajal need rakendused automaatselt sulgeda. Prasta paigaldamise lpetamist ritab paigaldaja need rakendused taaskivitada. CloseApplications=&Sulge rakendused automaatselt DontCloseApplications=ra s&ulge rakendusi ErrorCloseApplications=Paigaldaja ei saanud kiki rakendusi automaatselt sulgeda. Enne jtkamist on soovitatav sul sulgeda kik rakendused, mis kasutavad faile, mida paigaldaja peab uuendama. ; *** "Installing" wizard page WizardInstalling=Paigaldamine InstallingLabel=Palun oota, kuni [name] paigaldatakse sinu arvutisse. ; *** "Setup Completed" wizard page FinishedHeadingLabel=[name]'i paigaldamise lpetamine FinishedLabelNoIcons=[name]'i paigaldamine on lpetatud. FinishedLabel=[name]'i paigaldamine on lpetatud. Programmi saab kivitada paigaldatud ikoonide abil. ClickFinish=Paigaldajast vljumiseks vajuta Valmis. FinishedRestartLabel=[name]'i paigaldamise lpetamiseks peab arvuti taaskivituma. Kas soovid kohe taaskivitada? FinishedRestartMessage=[name]'i paigaldamise lpetamiseks peab arvuti taaskivituma.%n%nKas soovid kohe taaskivitada? ShowReadmeCheck=Jah, sooviksin nha Readme (LoeMind) faili YesRadio=&Jah, taaskivita arvuti kohe NoRadio=&Ei, taaskivitan arvuti hiljem ; used for example as 'Run MyProg.exe' RunEntryExec=Kivita %1 ; used for example as 'View Readme.txt' RunEntryShellExec=Vaata %1 ; *** "Setup Needs the Next Disk" stuff ChangeDiskTitle=Paigaldaja vajab jrgmist diski SelectDiskLabel2=Palun sisesta disk %1 ja vajuta OK.%n%nKui diskil olevad failid asuvad kuskil mujal, siis sisesta ige kaustatee vi vajuta Sirvi. PathLabel=&Asukoht: FileNotInDir2=Fail "%1" ei asu kohas "%2". Palun sisesta ige disk vi vali teine kaust. SelectDirectoryLabel=Palun tpsusta jrgmise diski asukoht. ; *** Installation phase messages SetupAborted=Paigaldamist ei lpetatud.%n%nPalun paranda viga ja kivita paigaldaja uuesti. EntryAbortRetryIgnore=Uuesti proovimiseks vajuta Proovi uuesti, jtkamiseks Ignoreeri vi lpetamiseks Katkesta. ; *** Installation status messages StatusClosingApplications=Rakenduste sulgemine... StatusCreateDirs=Kaustade loomine... StatusExtractFiles=Failide lahtipakkimine... StatusCreateIcons=Otseteede loomine... StatusCreateIniEntries=INI kirjete loomine... StatusCreateRegistryEntries=Registri kirjete loomine... StatusRegisterFiles=Failide registreerimine... StatusSavingUninstall=Eemaldamise teabe salvestamine... StatusRunProgram=Paigaldamise lpetamine... StatusRestartingApplications=Rakenduste taaskivitamine... StatusRollback=Muudatuste tagasivtmine... ; *** Misc. errors ErrorInternal2=Sisemine viga: %1 ErrorFunctionFailedNoCode=%1 luhtus ErrorFunctionFailed=%1 luhtus; kood %2 ErrorFunctionFailedWithMessage=%1 luhtus; kood %2.%n%3 ErrorExecutingProgram=Ei saanud kivitada faili:%n%1 ; *** Registry errors ErrorRegOpenKey=Ei saanud avada registri vtit:%n%1\%2 ErrorRegCreateKey=Ei saanud luua registri vtit:%n%1\%2 ErrorRegWriteKey=Ei saanud kirjutada registri vtit:%n%1\%2 ; *** INI errors ErrorIniEntry=Viga INI kirje loomisel failis "%1". ; *** File copying errors FileAbortRetryIgnore=Uuesti proovimiseks vajuta Proovi uuesti, faili vahelejtmiseks Ignoreeri (mittesoovitatav) vi paigaldamisest loobumiseks Katkesta. FileAbortRetryIgnore2=Uuesti proovimiseks vajuta Proovi uuesti, jtkamiseks Ignoreeri (mittesoovitatav) vi paigaldamisest loobumiseks Katkesta. SourceIsCorrupted=Lhtefail on rikutud SourceDoesntExist=Lhtefaili "%1" ei eksisteeri ExistingFileReadOnly=Fail on mrgitud kui kirjutuskaitstud.%n%nKirjutuskaitstuse mahavtmiseks vajuta Proovi uuesti ja proovi uuesti, faili vahelejtmiseks Ignoreeri vi paigaldamisest loobumiseks Katkesta. ErrorReadingExistingDest=Faili lugemisel ilmnes viga: FileExists=Fail on juba olemas.%n%nKas soovid, et paigaldaja selle le kirjutaks? ExistingFileNewer=Olemasolev fail on uuem kui see, mida paigaldaja ritab paigaldada. Soovitatav on olemasolev fail alles jtta.%n%nKas soovid olemasoleva faili alles jtta? ErrorChangingAttr=Faili atribuutide muutmisel ilmnes viga: ErrorCreatingTemp=Faili loomisel sihtkataloogi ilmnes viga: ErrorReadingSource=Lhtefaili lugemisel ilmnes viga: ErrorCopying=Faili kopeerimisel ilmnes viga: ErrorReplacingExistingFile=Olemasoleva faili asendamisel ilmnes viga: ErrorRestartReplace=Faili asendamine peale taaskivitust ei nnestunud: ErrorRenamingTemp=Faili nime muutmisel sihtkataloogis ilmnes viga: ErrorRegisterServer=Ei saanud registreerida DLL/OCX: %1 ErrorRegSvr32Failed=RegSvr32 luhtus koodiga %1 ErrorRegisterTypeLib=Unable to register the type library: %1 ; *** Post-installation errors ErrorOpeningReadme=README (LoeMind) faili avamisel ilmnes viga. ErrorRestartingComputer=Paigaldaja ei suutnud arvutit taaskivitada. Palun tee seda ksitsi. ; *** Uninstaller messages UninstallNotFound=Faili "%1" ei ole olemas. Ei saa eemaldada. UninstallOpenError=Faili "%1" ei saanud avada. Ei saa eemaldada. UninstallUnsupportedVer=Eemaldamise logifaili "%1" formaat on tundmatu selle versiooni eemaldaja jaoks. Ei saa eemaldada UninstallUnknownEntry=Eemaldaja logis on tundmatu kirje (%1) ConfirmUninstall=Oled kindel, et soovid eemaldada %1'i ja kik selle komponendid? UninstallOnlyOnWin64=Seda paigaldamist saab eemaldada ainult 64-bitises Windowsis. OnlyAdminCanUninstall=Seda paigaldamist saab eemaldada ainult administraatoriigustega kasutaja. UninstallStatusLabel=Palun oota, kuni %1 eemaldatakse sinu arvutist. UninstalledAll=%1 eemaldati sinu arvutist edukalt. UninstalledMost=%1'i eemaldamine nnestus.%n%nMned elemendid jid alles. Need vib ksitsi kustutada. UninstalledAndNeedsRestart=%1'i eemaldamise lpetamiseks peab arvuti taaskivituma.%n%nKas soovid kohe taaskivitada? UninstallDataCorrupted="%1" fail on rikutud. Ei saa eemaldada ; *** Uninstallation phase messages ConfirmDeleteSharedFileTitle=Kas kustutan hiskasutuses oleva faili? ConfirmDeleteSharedFile2=Ssteem kinnitab, et hiskasutuses olevat faili ei kasuta kski teine programm. Kas soovid, et eemaldaja selle hiskasutuses oleva faili kustutaks?%n%nKui mni programm seda siiski veel kasutab, siis ei pruugi see enam korralikult ttada. Kui sa pole kindel, vali Ei. Faili allesjtmine ei tekita probleeme. SharedFileNameLabel=Faili nimi: SharedFileLocationLabel=Asukoht: WizardUninstalling=Eemaldamise staatus StatusUninstalling=%1'i eemaldamine... ; *** Shutdown block reasons ShutdownBlockReasonInstallingApp=%1'i paigaldamine. ShutdownBlockReasonUninstallingApp=%1'i eemaldamine. ; The custom messages below aren't used by Setup itself, but if you make ; use of them in your scripts, you'll want to translate them. [CustomMessages] NameAndVersion=%1 versiooniga %2 AdditionalIcons=Tiendavad ikoonid: CreateDesktopIcon=Loo &tlaua ikoon CreateQuickLaunchIcon=Loo &kiirkivituse ikoon ProgramOnTheWeb=%1 veebis UninstallProgram=%1 - eemalda LaunchProgram=Kivita %1 AssocFileExtension=&Seosta %1 %2 faililaiendiga AssocingFileExtension=Seostan %1 %2 faililaiendiga... AutoStartProgramGroupDescription=Kivitus: AutoStartProgram=Kivita %1 automaatselt AddonHostProgramNotFound=%1 ei asu sinu valitud kaustas.%n%nKas soovid sellegipoolest jtkata? ricochet-1.1.4/translation/inno/Swedish.isl000066400000000000000000000452431300720305500207400ustar00rootroot00000000000000; *** Inno Setup version 5.5.3+ Swedish messages *** ; ; To download user-contributed translations of this file, go to: ; http://www.jrsoftware.org/files/istrans/ ; ; Note: When translating this text, do not add periods (.) to the end of ; messages that didn't have them already, because on those messages Inno ; Setup adds the periods automatically (appending a period would result in ; two periods being displayed). ; ; Translated by christer_1@hotmail.com (Christer Toivonen) ; [LangOptions] ; The following three entries are very important. Be sure to read and ; understand the '[LangOptions] section' topic in the help file. LanguageName=Svenska LanguageID=$041D LanguageCodePage=1252 ; If the language you are translating to requires special font faces or ; sizes, uncomment any of the following entries and change them accordingly. ;DialogFontName= ;DialogFontSize=8 ;WelcomeFontName=Verdana ;WelcomeFontSize=12 ;TitleFontName=Arial ;TitleFontSize=29 ;CopyrightFontName=Arial ;CopyrightFontSize=8 [Messages] ; *** Application titles SetupAppTitle=Installationsprogram SetupWindowTitle=Installationsprogram fr %1 UninstallAppTitle=Avinstallation UninstallAppFullTitle=%1 Avinstallation ; *** Misc. common InformationTitle=Information ConfirmTitle=Bekrfta ErrorTitle=Fel ; *** SetupLdr messages SetupLdrStartupMessage=%1 kommer att installeras. Vill du fortstta? LdrCannotCreateTemp=Kan ej skapa en temporr fil. Installationen avbryts LdrCannotExecTemp=Kan inte kra fil i temporr katalog. Installationen avbryts ; *** Startup error messages LastErrorMessage=%1.%n%nFel %2: %3 SetupFileMissing=Filen %1 saknas i installationskatalogen. Rtta till problemet eller hmta en ny kopia av programmet. SetupFileCorrupt=Installationsfilerna r felaktiga. Hmta en ny kopia av programmet SetupFileCorruptOrWrongVer=Installationsfilerna r felaktiga, eller stmmer ej verens med denna version av installationsprogrammet. Rtta till felet eller hmta en ny programkopia. InvalidParameter=En ogiltig parameter angavs p kommandoraden:%n%n%1 SetupAlreadyRunning=Setup krs redan. WindowsVersionNotSupported=Programmet stdjer inte den version av Windows som krs p datorn. WindowsServicePackRequired=Programmet krver %1 Service Pack %2 eller nyare. NotOnThisPlatform=Detta program kan ej kras p %1. OnlyOnThisPlatform=Detta program mste ha %1. OnlyOnTheseArchitectures=Detta program kan bara installeras p Windows versioner med fljande processorarkitekturer:%n%n%1 MissingWOW64APIs=Den versionen av Windows du kr har inte den funktionalitet installationsprogrammet behver fr att genomfra en 64-bitars installation. Rtta till problemet genom att installera Service Pack %1. WinVersionTooLowError=Detta program krver %1, version %2 eller senare. WinVersionTooHighError=Programmet kan inte installeras p %1 version %2 eller senare. AdminPrivilegesRequired=Du mste vara inloggad som administratr nr du installerar detta program. PowerUserPrivilegesRequired=Du mste vara inloggad som administratr eller medlem av gruppen Privilegierade anvndare (Power Users) nr du installerar detta program. SetupAppRunningError=Installationsprogrammet har upptckt att %1 r igng.%n%nAvsluta det angivna programmet nu. Klicka sedan p OK fr att g vidare, eller p Avbryt fr att avsluta. UninstallAppRunningError=Avinstalleraren har upptckt att %1 krs fr tillfllet.%n%nStng all ppna instanser av det nu, klicka sedan p OK fr att g vidare, eller p Avbryt fr att avsluta. ; *** Misc. errors ErrorCreatingDir=Kunde inte skapa katalogen "%1" ErrorTooManyFilesInDir=Kunde inte skapa en fil i katalogen "%1" drfr att den innehller fr mnga filer ; *** Setup common messages ExitSetupTitle=Avsluta installationen ExitSetupMessage=Installationen r inte frdig. Om du avslutar nu, kommer programmet inte att installeras.%n%nDu kan kra installationsprogrammet vid ett senare tillflle fr att slutfra installationen.%n%nVill du avbryta installationen? AboutSetupMenuItem=&Om installationsprogrammet... AboutSetupTitle=Om installationsprogrammet AboutSetupMessage=%1 version %2%n%3%n%n%1 hemsida:%n%4 AboutSetupNote=Svensk versttning r gjord av dickg@go.to 1999, 2002%n%nUppdatering till 3.0.2+ av peter@peterandlinda.com, 4.+ av stefan@bodingh.se TranslatorNote= ; *** Buttons ButtonBack=< &Tillbaka ButtonNext=&Nsta > ButtonInstall=&Installera ButtonOK=OK ButtonCancel=Avbryt ButtonYes=&Ja ButtonYesToAll=Ja till &Allt ButtonNo=&Nej ButtonNoToAll=N&ej till allt ButtonFinish=&Slutfr ButtonBrowse=&Blddra... ButtonWizardBrowse=&Blddra... ButtonNewFolder=Skapa ny katalog ; *** "Select Language" dialog messages SelectLanguageTitle=Vlj sprk fr installationen SelectLanguageLabel=Vlj sprk som skall anvndas under installationen: ; *** Common wizard text ClickNext=Klicka p Nsta fr att fortstta eller p Avbryt fr att avsluta installationen. BeveledLabel= BrowseDialogTitle=Vlj katalog BrowseDialogLabel=Vlj en katalog i listan nedan, klicka sedan p OK. NewFolderName=Ny katalog ; *** "Welcome" wizard page WelcomeLabel1=Vlkommen till installationsprogrammet fr [name]. WelcomeLabel2=Detta kommer att installera [name/ver] p din dator.%n%nDet rekommenderas att du avslutar alla andra program innan du fortstter. Det frebygger konflikter under installationens gng. ; *** "Password" wizard page WizardPassword=Lsenord PasswordLabel1=Denna installation r skyddad med lsenord. PasswordLabel3=Var god ange lsenordet, klicka sedan p Nsta fr att fortstta. Lsenord skiljer p versaler/gemener. PasswordEditLabel=&Lsenord: IncorrectPassword=Lsenordet du angav r inkorrekt. Frsk igen. ; *** "License Agreement" wizard page WizardLicense=Licensavtal LicenseLabel=Var god och ls fljande viktiga information innan du fortstter. LicenseLabel3=Var god och ls fljande licensavtal. Du mste acceptera villkoren i avtalet innan du kan fortstta med installationen. LicenseAccepted=Jag &accepterar avtalet LicenseNotAccepted=Jag accepterar &inte avtalet ; *** "Information" wizard pages WizardInfoBefore=Information InfoBeforeLabel=Var god ls fljande viktiga information innan du fortstter. InfoBeforeClickLabel=Nr du r klar att fortstta med installationen klickar du p Nsta. WizardInfoAfter=Information InfoAfterLabel=Var god ls fljande viktiga information innan du fortstter. InfoAfterClickLabel=Nr du r klar att fortstta med installationen klickar du p Nsta. ; *** "User Information" wizard page WizardUserInfo=Anvndarinformation UserInfoDesc=Var god och fyll i fljande uppgifter. UserInfoName=&Namn: UserInfoOrg=&Organisation: UserInfoSerial=&Serienummer: UserInfoNameRequired=Du mste fylla i ett namn. ; *** "Select Destination Directory" wizard page WizardSelectDir=Vlj installationsplats SelectDirDesc=Var skall [name] installeras? SelectDirLabel3=Installationsprogrammet kommer att installera [name] i fljande katalog SelectDirBrowseLabel=Fr att fortstta klickar du p Nsta. Om du vill vlja en annan katalog klickar du p Blddra. DiskSpaceMBLabel=Programmet krver minst [mb] MB hrddiskutrymme. CannotInstallToNetworkDrive=Setup kan inte installeras p ntverksdisk. CannotInstallToUNCPath=Setup kan inte installeras p UNC skvg. InvalidPath=Du mste skriva en fullstndig skvg med enhetsbeteckning; till exempel:%n%nC:\Program%n%neller en UNC-skvg i formatet:%n%n\\server\resurs InvalidDrive=Enheten du har valt finns inte eller r inte tillgnglig. Vlj en annan. DiskSpaceWarningTitle=Ej tillrckligt med diskutrymme DiskSpaceWarning=Installationsprogrammet behver tminstone %1 KB ledigt diskutrymme fr installationen, men den valda enheten har bara %2 KB tillgngligt.%n%nVill du fortstta nd? DirNameTooLong=Katalogens namn eller skvg r fr lng. InvalidDirName=Katalogen du har valt r inte tillgnglig. BadDirName32=Katalogens namn fr ej innehlla ngot av fljande tecken:%n%n%1 DirExistsTitle=Katalogen finns DirExists=Katalogen:%n%n%1%n%nfinns redan. Vill du nd fortstta installationen till den valda katalogen? DirDoesntExistTitle=Katalogen finns inte DirDoesntExist=Katalogen:%n%n%1%n%nfinns inte. Vill du skapa den? ; *** "Select Components" wizard page WizardSelectComponents=Vlj komponenter SelectComponentsDesc=Vilka komponenter skall installeras? SelectComponentsLabel2=Vlj de komponenter som du vill ska installeras; avmarkera de komponenter som du inte vill ha. Klicka sedan p Nsta nr du r klar att fortstta. FullInstallation=Fullstndig installation ; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language) CompactInstallation=Kompakt installation CustomInstallation=Anpassad installation NoUninstallWarningTitle=Komponenter finns NoUninstallWarning=Installationsprogrammet har upptckt att fljande komponenter redan finns installerade p din dator:%n%n%1%n%nAtt avmarkera dessa komponenter kommer inte att avinstallera dom.%n%nVill du fortstta nd? ComponentSize1=%1 KB ComponentSize2=%1 MB ComponentsDiskSpaceMBLabel=Aktuella val krver minst [mb] MB diskutrymme. ; *** "Select Additional Tasks" wizard page WizardSelectTasks=Vlj extra uppgifter SelectTasksDesc=Vilka extra uppgifter skall utfras? SelectTasksLabel2=Markera ytterligare uppgifter att utfra vid installation av [name], tryck sedan p Nsta. ; *** "Select Start Menu Folder" wizard page WizardSelectProgramGroup=Vlj Startmenykatalogen SelectStartMenuFolderDesc=Var skall installationsprogrammet placera programmets genvgar? SelectStartMenuFolderLabel3=Installationsprogrammet kommer att skapa programmets genvgar i fljande katalog. SelectStartMenuFolderBrowseLabel=Fr att fortstta klickar du p Nsta. Om du vill vlja en annan katalog, klickar du p Blddra. MustEnterGroupName=Du mste ange en katalog. GroupNameTooLong=Katalogens namn eller skvg r fr lng. InvalidGroupName=Katalogen du har valt r inte tillgnglig. BadGroupName=Katalognamnet kan inte innehlla ngon av fljande tecken:%n%n%1 NoProgramGroupCheck2=&Skapa ingen Startmenykatalog ; *** "Ready to Install" wizard page WizardReady=Redo att installera ReadyLabel1=Installationsprogrammet r nu redo att installera [name] p din dator. ReadyLabel2a=Tryck p Installera om du vill fortstta, eller p g Tillbaka om du vill granska eller ndra p ngot. ReadyLabel2b=Vlj Installera fr att pbrja installationen. ReadyMemoUserInfo=Anvndarinformation: ReadyMemoDir=Installationsplats: ReadyMemoType=Installationstyp: ReadyMemoComponents=Valda komponenter: ReadyMemoGroup=Startmenykatalog: ReadyMemoTasks=Extra uppgifter: ; *** "Preparing to Install" wizard page WizardPreparing=Frbereder installationen PreparingDesc=Installationsprogrammet frbereder installationen av [name] p din dator. PreviousInstallNotCompleted=Installationen/avinstallationen av ett tidigare program har inte slutfrts. Du mste starta om datorn fr att avsluta den installationen.%n%nEfter att ha startat om datorn kr du installationsprogrammet igen fr att slutfra installationen av [name]. CannotContinue=Installationsprogrammet kan inte fortstta. Klicka p Avbryt fr att avsluta. ApplicationsFound=Fljande program anvnder filer som mste uppdateras av Setup. Vi rekommenderar att du lter Setup automatiskt stnga dessa program. ApplicationsFound2=Fljande program anvnder filer som mste uppdateras av Setup. Vi rekommenderar att du lter Setup automatiskt stnga dessa program. Efter installationen kommer Setup att frska starta programmen igen. CloseApplications=&Stng programmen automatiskt DontCloseApplications=&Stng inte programmen ErrorCloseApplications=Installationsprogrammet kunde inte stnga alla program. Innan installationen fortstter rekommenderar vi att du stnger alla program som anvnder filer som Setup behver uppdatera. ; *** "Installing" wizard page WizardInstalling=Installerar InstallingLabel=Vnta medan [name] installeras p din dator. ; *** "Setup Completed" wizard page FinishedHeadingLabel=Avslutar installationen av [name] FinishedLabelNoIcons=[name] har nu installerats p din dator. FinishedLabel=[name] har nu installerats p din dator. Programmet kan startas genom att vlja ngon av ikonerna. ClickFinish=Vlj Slutfr fr att avsluta installationen. FinishedRestartLabel=Fr att slutfra installationen av [name], mste datorn startas om. Vill du starta om nu? FinishedRestartMessage=Fr att slutfra installationen av [name], mste datorn startas om.%n%nVill du starta om datorn nu? ShowReadmeCheck=Ja, jag vill se filen LS MIG YesRadio=&Ja, jag vill starta om datorn nu NoRadio=&Nej, jag startar sjlv om datorn senare ; used for example as 'Run MyProg.exe' RunEntryExec=Kr %1 ; used for example as 'View Readme.txt' RunEntryShellExec=Ls %1 ; *** "Setup Needs the Next Disk" stuff ChangeDiskTitle=Installationsprogrammet behver nsta diskett SelectDiskLabel2=Var god stt i diskett %1 och tryck OK.%n%nOm filerna kan hittas i en annan katalog n den som visas nedan, skriv in rtt skvg eller vlj Blddra. PathLabel=&Skvg: FileNotInDir2=Kunde inte hitta filen "%1" i "%2". Var god stt i korrekt diskett eller vlj en annan katalog. SelectDirectoryLabel=Var god ange skvgen fr nsta diskett. ; *** Installation phase messages SetupAborted=Installationen slutfrdes inte.%n%nVar god rtta till felet och kr installationen igen. EntryAbortRetryIgnore=Vlj Frsk igen eller Ignorera fr att fortstta nd, eller vlj Avbryt fr att avbryta installationen. ; *** Installation status messages StatusClosingApplications=Stnger program... StatusCreateDirs=Skapar kataloger... StatusExtractFiles=Packar upp filer... StatusCreateIcons=Skapar programikoner... StatusCreateIniEntries=Skriver INI-vrden... StatusCreateRegistryEntries=Skriver register-vrden... StatusRegisterFiles=Registrerar filer... StatusSavingUninstall=Sparar information fr avinstallation... StatusRunProgram=Slutfr installationen... StatusRestartingApplications=Startar om program... StatusRollback=terstller ndringar... ; *** Misc. errors ErrorInternal2=Internt fel: %1 ErrorFunctionFailedNoCode=%1 misslyckades ErrorFunctionFailed=%1 misslyckades; kod %2 ErrorFunctionFailedWithMessage=%1 misslyckades; kod %2.%n%3 ErrorExecutingProgram=Kan inte kra filen:%n%1 ; *** Registry errors ErrorRegOpenKey=Fel vid ppning av registernyckel:%n%1\%2 ErrorRegCreateKey=Kan ej skapa registernyckel:%n%1\%2 ErrorRegWriteKey=Kan ej skriva till registernyckel:%n%1\%2 ; *** INI errors ErrorIniEntry=Kan inte skriva nytt INI-vrde i filen "%1". ; *** File copying errors FileAbortRetryIgnore=Vlj Frsk igen eller Ignorera fr att hoppa ver denna fil (ej rekommenderat), eller vlj Avbryt installationen. FileAbortRetryIgnore2=Vlj Frsk igen eller Ignorera och fortstt nd (ej rekommenderat), eller vlj Avbryt installationen. SourceIsCorrupted=Kllfilen r felaktig SourceDoesntExist=Kllfilen "%1" finns inte ExistingFileReadOnly=Den nuvarande filen r skrivskyddad.%n%nVlj Frsk igen fr att ta bort skrivskyddet, Ignorera fr att hoppa ver denna fil, eller vlj Avbryt installationen. ErrorReadingExistingDest=Ett fel uppstod vid frsk att lsa den befintliga filen: FileExists=Filen finns redan.%n%nVill du skriva ver den? ExistingFileNewer=Den befintliga filen r nyare n den som ska installeras. Du rekommenderas att behlla den befintliga filen. %n%nVill Du behlla den befintliga filen? ErrorChangingAttr=Ett fel uppstod vid frsk att ndra attribut p den befintliga filen: ErrorCreatingTemp=Ett fel uppstod vid ett frsk att skapa installationskatalogen: ErrorReadingSource=Ett fel uppstod vid ett frsk att lsa kllfilen: ErrorCopying=Ett fel uppstod vid kopiering av filen: ErrorReplacingExistingFile=Ett fel uppstod vid ett frsk att erstta den befintliga filen: ErrorRestartReplace=terstartaErstt misslyckades: ErrorRenamingTemp=Ett fel uppstod vid ett frsk att byta namn p en fil i installationskatalogen: ErrorRegisterServer=Kunde inte registrera DLL/OCX: %1 ErrorRegSvr32Failed=RegSvr32 misslyckades med felkod %1 ErrorRegisterTypeLib=Kunde inte registrera typbibliotek: %1 ; *** Post-installation errors ErrorOpeningReadme=Ett fel uppstod vid ppnandet av LS MIG-filen. ErrorRestartingComputer=Installationsprogrammet kunde inte starta om datorn. Var god gr det manuellt. ; *** Uninstaller messages UninstallNotFound=Filen "%1" finns inte. Kan inte avinstallera. UninstallOpenError=Filen "%1" kan inte ppnas. Kan inte avinstallera. UninstallUnsupportedVer=Avinstallationsloggen "%1" r i ett format som denna version inte knner igen. Kan ej avinstallera UninstallUnknownEntry=En oknd rad (%1) hittades i avinstallationsloggen ConfirmUninstall=r du sker p att du vill ta bort %1 och alla tillhrande komponenter? UninstallOnlyOnWin64=Denna installation kan endast avinstalleras p en 64-bitarsversion av Windows. OnlyAdminCanUninstall=Denna installation kan endast avinstalleras av en anvndare med administrativa rttigheter. UninstallStatusLabel=Var god och vnta medan %1 tas bort frn din dator. UninstalledAll=%1 r nu borttaget frn din dator. UninstalledMost=Avinstallationen av %1 r nu klar.%n%nEn del filer/kataloger gick ej att ta bort. Dessa kan tas bort manuellt. UninstalledAndNeedsRestart=Fr att slutfra avinstallationen av %1 mste datorn startas om.%n%nVill du starta om nu? UninstallDataCorrupted=Filen "%1" r felaktig. Kan inte avinstallera ; *** Uninstallation phase messages ConfirmDeleteSharedFileTitle=Ta bort delad fil? ConfirmDeleteSharedFile2=Systemet indikerar att fljande delade fil inte lngre anvnds av ngra program. Vill du ta bort den delade filen?%n%n%1%n%nOm ngot program fortfarande anvnder denna fil och den raderas, kommer programmet kanske att sluta fungera. Om du r osker, vlj Nej. Att lta filen ligga kvar i systemet kommer inte att orsaka ngon skada. SharedFileNameLabel=Filnamn: SharedFileLocationLabel=Plats: WizardUninstalling=Avinstallationsstatus StatusUninstalling=Avinstallerar %1... ; *** Shutdown block reasons ShutdownBlockReasonInstallingApp=Installerar %1. ShutdownBlockReasonUninstallingApp=Avinstallerar %1. ; The custom messages below aren't used by Setup itself, but if you make ; use of them in your scripts, you'll want to translate them. [CustomMessages] NameAndVersion=%1 version %2 AdditionalIcons=terstende ikoner: CreateDesktopIcon=Skapa en ikon p skrivbordet CreateQuickLaunchIcon=Skapa en ikon i Snabbstartfltet ProgramOnTheWeb=%1 p Webben UninstallProgram=Avinstallera %1 LaunchProgram=Starta %1 AssocFileExtension=Associera %1 med %2 filnamnstillgg AssocingFileExtension=Associerar %1 med %2 filnamnstillgg... AutoStartProgramGroupDescription=Autostart: AutoStartProgram=Starta automatiskt %1 AddonHostProgramNotFound=%1 kunde inte hittas i katalogen du valde.%n%nVill du fortstta nd? ricochet-1.1.4/translation/installer_bg.isl000066400000000000000000000012461300720305500210270ustar00rootroot00000000000000[Messages] WelcomeLabel2= . [CustomMessages] AppTitle= - UninstallShortcut= RunShortcut= PortableDesc= ? PortableText= , . . PortableTitle= PortableOptInstall= () PortableOptExtract= () BtnExtract= ExtractDirText= ExtractDirDesc= ? ricochet-1.1.4/translation/installer_cs.isl000066400000000000000000000012741300720305500210450ustar00rootroot00000000000000[Messages] WelcomeLabel2=Instalace programu Ricochet na v pota. [CustomMessages] AppTitle=Ricochet - anonymn instantn komuniktor UninstallShortcut=Odinstalovat Ricochet RunShortcut=Spustit Ricochet PortableDesc=Chcete nainstalovat penosnou instalaci? PortableText=Ricochet lze nainstalovat na v systm, nebo rozbalit do penosn sloky soubor. Penosnou instalaci lze penet mezi nkolika potai nebo zabezpeit na zaifrovanm pevnm disku. PortableTitle=Zpsob instalace PortableOptInstall=Instalovat (doporueno) PortableOptExtract=Rozbalit (penosn) BtnExtract=Rozbalit ExtractDirText=Ricochet bude rozbalen do nsledujcho adrese ExtractDirDesc=Kam m bt Ricochet rozbalen? ricochet-1.1.4/translation/installer_da.isl000066400000000000000000000012441300720305500210210ustar00rootroot00000000000000[Messages] WelcomeLabel2=Dette vil installere Ricochet p din computer. [CustomMessages] AppTitle=Ricochet - Anonym Instant Messaging UninstallShortcut=Afinstaller Ricochet RunShortcut=Start Ricochet PortableDesc=Vil du have en portabel installation? PortableText=Ricochet kan installeres p dit system, eller udpakkes til en portabel mappe. Den portable installation kan flyttes mellem computere eller holdes sikker p en krypteret harddisk. PortableTitle=Installationstilstand PortableOptInstall=Installer (Anbefalt) PortableOptExtract=Udpak (Portabel) BtnExtract=Udpak ExtractDirText=Rirochet vil blive udpakket i flgende mappe ExtractDirDesc=Hvor skal Ricochet udpakkes? ricochet-1.1.4/translation/installer_de.isl000066400000000000000000000014111300720305500210210ustar00rootroot00000000000000[Messages] WelcomeLabel2=Dies installiert Ricochet auf Ihrem Computer. [CustomMessages] AppTitle=Ricochet - Anonymes Instant Messaging UninstallShortcut=Ricochet deinstallieren RunShortcut=Ricochet starten PortableDesc=Wollen Sie Ricochet als Portable-Variante installieren? PortableText=Ricochet kann auf Ihrem System installiert, oder in einen Ordner als Portable-Variante entpackt werden. Die Portable-Variante kann zwischen verschiedenen Computern bewegt werden oder auf einer verschlsselten Festplatte gespeichert werden. PortableTitle=Installationsmodus PortableOptInstall=Installieren (Empfohlen) PortableOptExtract=Entpacken (Portable) BtnExtract=Entpacken ExtractDirText=Ricochet wird in den folgenden Ordner entpackt ExtractDirDesc=Wo soll Ricochet entpackt werden? ricochet-1.1.4/translation/installer_en.isl000066400000000000000000000012601300720305500210350ustar00rootroot00000000000000[Messages] WelcomeLabel2=This will install Ricochet on your computer. [CustomMessages] AppTitle=Ricochet - Anonymous Instant Messaging UninstallShortcut=Uninstall Ricochet RunShortcut=Launch Ricochet PortableDesc=Do you want a portable installation? PortableText=Ricochet can be installed on your system, or extracted to a portable folder. The portable installation can be moved between computers or kept secure on an encrypted harddrive. PortableTitle=Installation Mode PortableOptInstall=Install (Recommended) PortableOptExtract=Extract (Portable) BtnExtract=Extract ExtractDirText=Ricochet will be extracted into the following folder ExtractDirDesc=Where should Ricochet be extracted? ricochet-1.1.4/translation/installer_es.isl000066400000000000000000000012701300720305500210430ustar00rootroot00000000000000[Messages] WelcomeLabel2=Esto instalar Ricochet en su computadora. [CustomMessages] AppTitle=Ricochet - Mensajera Instantnea Annima UninstallShortcut=Desinstalar Ricochet RunShortcut=Ejecutar Ricochet PortableDesc=Quiere una instalacin portable? PortableText=Ricochet puede ser instalado en su sistema, o extrado a una carpeta portable. La instalacin portable pude ser trasladada entre computadoras, o permanecer segura en un disco duro cifrado. PortableTitle=Modo de Instalacin PortableOptInstall=Instalar (Recomendado) PortableOptExtract=Extraer (Portable) BtnExtract=Extraer ExtractDirText=Ricochet ser extrado en la siguiente carpeta ExtractDirDesc=Dnde debe extraerse Ricochet? ricochet-1.1.4/translation/installer_et_EE.isl000066400000000000000000000013031300720305500214120ustar00rootroot00000000000000[Messages] WelcomeLabel2=See paigaldab programmi Ricochet sinu arvutisse. [CustomMessages] AppTitle=Ricochet - Anonmsed kiirsnumid UninstallShortcut=Eemalda Ricochet RunShortcut=Kivita Ricochet PortableDesc=Kas sa tahad portatiivset paigaldust? PortableText=Programmi Ricochet saab paigaldada sinu ssteemi vi teha portatiivseks kataloogiks. Portatiivset paigaldusmeetodit saab le viia teise arvutisse vi jtta turvaliselt mnda krpteeritud kvakettale. PortableTitle=Paigaldusmeetod PortableOptInstall=Paigalda (Soovitav) PortableOptExtract=Paki lahti (Portatiivne) BtnExtract=Paki lahti ExtractDirText=Ricochet paigaldatakse jrgnevasse kataloogi ExtractDirDesc=Kuhu Ricochet tuleb lahti pakkida? ricochet-1.1.4/translation/installer_fi.isl000066400000000000000000000014551300720305500210370ustar00rootroot00000000000000[Messages] WelcomeLabel2=Tietokoneeseesi asennetaan Ricochet ohjelmisto [CustomMessages] AppTitle=Ricochet - Anonyymi Pikaviestin UninstallShortcut=Poista Ricochet RunShortcut=Kynnist Ricochet PortableDesc=Asennetaanko ohjelmisto siirrettvlle tietovlineelle? PortableText=Ricochet voidaan asentaa suoraan tmn tietokoneen kiintelle kovalevylle (Asenna) tai purkaa haluttuun kansioon / tietovlineeseen (Pura).\n\nSiirrettv vaihtoehto mahdollistaa ohjelmiston kytn useissa eri tietokoneissa ja ohjelmiston tallentamisen salatuille tietovlineille. PortableTitle=Asennustyyppi PortableOptInstall=Asenna (Suositeltu vaihtoehto) PortableOptExtract=Pura (Siirrettv asennus) BtnExtract=Pura kansioon ExtractDirText=Ricochet puretaan seuraavaan sijaintiin ExtractDirDesc=Mihin sijaintiin Ricochet puretaan? ricochet-1.1.4/translation/installer_fr.isl000066400000000000000000000014021300720305500210400ustar00rootroot00000000000000[Messages] WelcomeLabel2=Ceci va installer Ricochet sur votre ordinateur. [CustomMessages] AppTitle=Ricochet - Messagerie instantane anonyme UninstallShortcut=Dsinstaller Ricochet RunShortcut=Dmarrer Ricochet PortableDesc=Voulez-vous une installation portable ? PortableText=Ricochet peut tre install sur votre systme, ou extrait dans un dossier portable. L'installation portable peut tre dplace et utilise sur plusieurs ordinateurs ou conserve de faon scurise sur un disque dur crypt. PortableTitle=Mode d'installation PortableOptInstall=Installation rapide (Recommand) PortableOptExtract=Extraction (Portable) BtnExtract=Extraire ExtractDirText=Ricochet sera dcompress dans le dossier suivant : ExtractDirDesc=O Ricochet devrait tre dcompress ? ricochet-1.1.4/translation/installer_it.isl000066400000000000000000000013271300720305500210530ustar00rootroot00000000000000[Messages] WelcomeLabel2=Questo installer Ricochet sul tuo computer [CustomMessages] AppTitle=Ricochet - Messaggistica Istantanea Anonima UninstallShortcut=Disinstalla Ricochet RunShortcut=Avvia Ricochet PortableDesc=Vuoi un installazione portabile? PortableText=Ricochet pu essere installato sul tuo sistema, o estratto in una cartella portabile. L'installazione portabile pu essere spostata fra computer o tenuta al sicuro su un disco rigido crittografato. PortableTitle=Modalit di installazione PortableOptInstall=Installazione (Consigliato) PortableOptExtract=Estrazione (Portabile) BtnExtract=Estrai ExtractDirText=Ricochet verr estratto nella seguente cartella ExtractDirDesc=Dove dovrebbe essere estratto Ricochet? ricochet-1.1.4/translation/installer_it_IT.isl000066400000000000000000000013151300720305500214440ustar00rootroot00000000000000[Messages] WelcomeLabel2=Questo installer Ricochet sul tuo computer [CustomMessages] AppTitle=Ricochet - Messaggistica istantanea anonima UninstallShortcut=Disinstalla Ricochet RunShortcut=Avvia Ricochet PortableDesc=Vuoi un'installazione portabile? PortableText=Ricochet pu essere installato sul tuo sistema, o estratto in una cartella portabile. L'installazione portabile pu essere spostata fra computer o tenuta al sicuro su un disco rigido crittografato. PortableTitle=Modalit di installazione PortableOptInstall=Installa (Consigliato) PortableOptExtract=Estrai (Portabile) BtnExtract=Estrai ExtractDirText=Ricochet verr estratto nella seguente cartella ExtractDirDesc=Dove vuoi che venga estratto Ricochet? ricochet-1.1.4/translation/installer_nb.isl000066400000000000000000000013011300720305500210260ustar00rootroot00000000000000[Messages] WelcomeLabel2=Dette vil installere Ricochet p din datamaskin. [CustomMessages] AppTitle=Ricochet - anonyme lynmeldinger UninstallShortcut=Avinstaller Ricochet RunShortcut=Start Ricochet PortableDesc=nsker du en flyttbar installasjon? PortableText=Ricochet kan enten installeres eller bli pakket ut til en vanlig flyttbar mappe. Som flyttbar mappe kan installasjonen flyttes mellom datamaskiner eller i stedet bli oppbevart sikkert p en kryptert harddisk. PortableTitle=Installasjonsmodus PortableOptInstall=Installer (anbefalt) PortableOptExtract=Pakk ut (flyttbar) BtnExtract=Pakk ut ExtractDirText=Ricochet vil bli pakket ut til flgende mappe ExtractDirDesc=Hvor skal Ricochet pakkes ut? ricochet-1.1.4/translation/installer_nl_NL.isl000066400000000000000000000013141300720305500214350ustar00rootroot00000000000000[Messages] WelcomeLabel2=Hiermee installeer je Ricochet op je computer. [CustomMessages] AppTitle=Ricochet - Anonieme Instant Messaging UninstallShortcut=De-installeer Ricochet RunShortcut=Start Ricochet PortableDesc=Wil je een portable installatie? PortableText=Ricochet kan worden genstalleerd op je computer, of uitgepakt in een portable map. De portable installatie kan worden verplaatst tussen computers of veilig worden bewaard op een versleutelde harde schijf. PortableTitle=Installatiemodus PortableOptInstall=Installeren (aanbevolen) PortableOptExtract=Uitpakken (Portable) BtnExtract=Uitpakken ExtractDirText=Ricochet wordt uitgepakt in de volgende map ExtractDirDesc=Waar moet Ricochet worden uitgepakt? ricochet-1.1.4/translation/installer_pl.isl000066400000000000000000000013271300720305500210520ustar00rootroot00000000000000[Messages] WelcomeLabel2=Program Ricochet zostanie zainstalowany na Twoim komputerze. [CustomMessages] AppTitle=Ricochet - Anonimowy Komunikator UninstallShortcut=Odinstaluj Ricochet RunShortcut=Uruchom Ricochet PortableDesc=Czy chcesz zainstalowa program w wersji przenonej? PortableText=Ricochet moe zosta zainstalowany w Twoim systemie albo rozpakowany do przenonego katalogu. Instalacja przenona moe by przechowywana na wielu komputerach lub zabezpieczona na zaszyfrowanym dysku. PortableTitle=Tryb instalacji PortableOptInstall=Instaluj (Zalecane) PortableOptExtract=Rozpakuj (Wersja przenona) BtnExtract=Rozpakuj ExtractDirText=Ricochet zostanie rozpakowany do katalogu ExtractDirDesc=Gdzie rozpakowa Ricochet? ricochet-1.1.4/translation/installer_pt_BR.isl000066400000000000000000000013111300720305500214360ustar00rootroot00000000000000[Messages] WelcomeLabel2=Isso vai instalar o Ricochet no seu computador. [CustomMessages] AppTitle=Ricochet - Mensagens Instantneas Annimas UninstallShortcut=Desinstalar o Ricochet RunShortcut=Executar o Ricochet PortableDesc=Voc deseja uma instalao porttil? PortableText=O Ricochet pode ser instalado no seu sistema, ou extrado para uma pasta porttil. A instalao porttil pode ser movida entre computadores ou mantida segura em um disco rgido criptografado. PortableTitle=Modo de Instalao PortableOptInstall=Instalar (Recomendado) PortableOptExtract=Extrair (Porttil) BtnExtract=Extrair ExtractDirText=O Ricochet ser extrado na seguinte pasta ExtractDirDesc=Onde o Ricochet deveria ser extrado? ricochet-1.1.4/translation/installer_pt_PT.isl000066400000000000000000000012731300720305500214650ustar00rootroot00000000000000[Messages] WelcomeLabel2=Isto vai instalar o Ricochet no seu computador. [CustomMessages] AppTitle=Ricochet - Mensagens Instantneas Annimas UninstallShortcut=Desinstalar o Ricochet RunShortcut=Iniciar o Ricochet PortableDesc=Deseja uma instalao porttil? PortableText=O Ricochet pode ser instalado no seu sistema, ou extrado para uma pasta porttil. A instalao porttil pode ser movida entre computadores ou mantida segura num disco encriptado. PortableTitle=Modo de Instalao PortableOptInstall=Instalar (Recomendado) PortableOptExtract=Extrair (Porttil) BtnExtract=Extrair ExtractDirText=O Ricochet ser extrado na seguinte pasta ExtractDirDesc=Onde deseja que o Ricochet seja extrado? ricochet-1.1.4/translation/installer_ru.isl000066400000000000000000000013451300720305500210650ustar00rootroot00000000000000[Messages] WelcomeLabel2= Ricochet . [CustomMessages] AppTitle=Ricochet - UninstallShortcut= Ricochet RunShortcut= Ricochet PortableDesc= ? PortableText=Ricochet . . PortableTitle= PortableOptInstall= () PortableOptExtract= ( ) BtnExtract= ExtractDirText=Ricochet ExtractDirDesc= Ricochet? ricochet-1.1.4/translation/installer_sl.isl000066400000000000000000000012111300720305500210450ustar00rootroot00000000000000[Messages] WelcomeLabel2=To bo namestilo Ricochet na va raunalnik. [CustomMessages] AppTitle=Ricochet - anonimno takojnje sporoanje /IM/ UninstallShortcut=Odstrani Ricochet RunShortcut=Zaeni Ricochet PortableDesc=elite prenosno namestitev? PortableText=Ricochet lahko namestite na va sistem ali razpakirate v prenosno mapo. Prenosno namestitev lahko selite med raunalniki ali hranite na ifriranem disku. PortableTitle=Nain namestitve PortableOptInstall=Namesti (priporoeno) PortableOptExtract=Razpakiraj (prenosno) BtnExtract=Razpakiraj ExtractDirText=Ricochet bo razpakiran v spodnjo mapo ExtractDirDesc=Kam elite razpakirati Ricochet? ricochet-1.1.4/translation/installer_sq.isl000066400000000000000000000013241300720305500210570ustar00rootroot00000000000000[Messages] WelcomeLabel2=Ky do t instaloj Ricochet-in n kompjuterin tuaj. [CustomMessages] AppTitle=Ricochet - Shkmbim Anonim Mesazhesh t Atypratyshm UninstallShortcut=instaloje Ricochet-in RunShortcut=Nise Ricochet-in PortableDesc=Doni nj instalim mbarts? PortableText=Ricochet-i mund t instalohet n sistemin tuaj, ose t prftohet n nj dosje mbartse. Instalimi mbarts mund t lvizet nga nj kompjuter n nj tjetr, ose t ruhet i parrezik n nj disk t fshehtzuar. PortableTitle=Mnyr Instalimi PortableOptInstall=Instalim (E kshilluar) PortableOptExtract=Prftim (Mbarts) BtnExtract=Prftoje ExtractDirText=Ricochet-i do t prftohet n dosjen vijuese ExtractDirDesc=Ku duhet prftuar Ricochet-i? ricochet-1.1.4/translation/installer_sv.isl000066400000000000000000000012501300720305500210620ustar00rootroot00000000000000[Messages] WelcomeLabel2=Det hr kommer att installera Ricochet p din dator. [CustomMessages] AppTitle=Ricochet - Anonym direktkommunikation UninstallShortcut=Avinstallera Ricochet RunShortcut=Starta Ricochet PortableDesc=Vill du ha en portabel installation? PortableText=Ricochet kan installeras p ditt system eller packas upp till en portabel mapp. Den portabla mappen kan flyttas mellan datorer eller lagras skert p en krypterad disk. PortableTitle=Installationstyp PortableOptInstall=Installera (rekommenderas) PortableOptExtract=Packa upp (portabel) BtnExtract=Packa upp ExtractDirText=Ricochet packas upp till fljande mapp ExtractDirDesc=Var skall Ricochet packas upp? ricochet-1.1.4/translation/installer_tr.isl000066400000000000000000000012161300720305500210610ustar00rootroot00000000000000[Messages] WelcomeLabel2=Bu yazlm, bilgisayarnza Ricochet ykleyecek. [CustomMessages] AppTitle=Ricochet - Anonim Anlk Mesajlama UninstallShortcut=Ricochet Kaldr RunShortcut=Ricochet Balat PortableDesc=Tanabilir kurulum istiyor musunuz? PortableText=Ricochet, sisteminize yklenebilir veya tanabilir bir klasre karlabilir. Tanabilir kurulum, bilgisayarlar aras tanabilir veya ifreli sabit diskte gvenli saklanabilir. PortableTitle=Kurulum Kipi PortableOptInstall=Kur (nerilen) PortableOptExtract=kar (Tanabilir) BtnExtract=kar ExtractDirText=Ricochet aadaki klasre karlacak ExtractDirDesc=Ricochet nereye karlsn? ricochet-1.1.4/translation/installer_uk.isl000066400000000000000000000013271300720305500210560ustar00rootroot00000000000000[Messages] WelcomeLabel2= Ricochet '. [CustomMessages] AppTitle=Ricochet - UninstallShortcut= Ricochet RunShortcut= Ricochet PortableDesc= ? PortableText=Ricochet . ' . PortableTitle= PortableOptInstall= () PortableOptExtract= () BtnExtract= ExtractDirText=Ricochet ExtractDirDesc= Ricochet? ricochet-1.1.4/translation/installer_zh.isl000066400000000000000000000007701300720305500210610ustar00rootroot00000000000000[Messages] WelcomeLabel2=װ Ricochet. [CustomMessages] AppTitle=Ricochet - ˽ܵļʱͨѶ UninstallShortcut=ж Ricochet RunShortcut= Ricochet PortableDesc=Ҫ̻װ? PortableText=Ricochet ֱӰװϵͳֱӽѹһļС̻װֱӿʹãҲֱڼܵӲʹá PortableTitle=װģʽ PortableOptInstall=װ(Ƽ) PortableOptExtract=ѹ(ɫ) BtnExtract=ѹ ExtractDirText=Ricochet ѹļ ExtractDirDesc=ָѹļ ricochet-1.1.4/translation/installer_zh_HK.isl000066400000000000000000000010431300720305500214350ustar00rootroot00000000000000[Messages] WelcomeLabel2=Xϰb Ricochet [CustomMessages] AppTitle= Ricochet-ļrӍϢ UninstallShortcut=Ƴ Ricochet RunShortcut= Ricochet PortableDesc=ǷҪʹÿɔyʽGɫb PortableText=Ricochet԰bϵyϻǽ≺һɔyUSBYĿ¡ɔyʽľGɫbڲͬXʹû򱣴һܵӲ PortableTitle=bģʽ PortableOptInstall=b (h) PortableOptExtract=≺s (ɔyʽb) BtnExtract=≺s ExtractDirText=Ricochet≺sµYϊA ExtractDirDesc=Ricochetęn≺s̎? ricochet-1.1.4/translation/ricochet_bg.ts000066400000000000000000000727721300720305500205050ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Рикошет %1 AddContactDialog Share your Ricochet ID to allow connection requests Сподели своят Рикошет Идентификатор, за да позволиш нови покани Cancel Откажи Add Добави ContactActions Open Window Отвори Прозорец Details... Допълнитлно... Rename Преименувай Remove Премахни ContactIDField <b>%1</b> is already your contact <b>%1</b> вече е твой контакт You can't add yourself as a contact Не може да добавиш себе си като контакт Enter an ID starting with <b>ricochet:</b> Въведи Идентификатор с <b>рикошет:</b> Copied to clipboard Копирано Copy Копирай ContactList Online На линия Offline Отписан Requests Молби Rejected Отхвърлени Outdated Стара версия ContactPreferences Date added: Дата на добавяне: Last seen: Последно видян: Request: Покана: Pending connection В очакване на връзка Delivered Доставено Accepted Прието Error Грешка Rejected Отхвърлено %1 (Connected) %1 status, e.g. "Accepted" %1 (Свързани) Response: Отговор: Rename Преименувай Remove Премахни ContactRequestDialog Someone new is asking to connect to you Нов човек те моли да се свържете Reject Отхвърли Accept Приеми ContactRequestFields ID: Индентификатор: Name: Име: Message: Съобщение: GeneralPreferences Use a single window for conversations Използвай само един прозорец за всички разговори Open links in default browser without prompting Отваряй връзките в браузъра по подразбиране без да искаш разрешение Play audio notifications Използвай звукови известия Volume Звук Language Език Restart Ricochet to apply changes Рестартирай Richchet, за да приложиш промените LanguagePreferences Select Language Избери език Restart Ricochet to apply changes Рестартирай Richchet, за да приложиш промените LanguagesModel System default Настройки по подразбиране Main Ricochet Error Рикошет Грешка MainToolBar Add Contact Добави Контакт Preferences Настройки Click to add contacts Кликни, за да добавиш контакт MainWindow Remove %1 Премахни %1 Do you want to permanently remove %1? Наистина ли искаш да премахнеш %1 завинаги? MessageDelegate %1 is offline %1 nickname %1 е отписан Copy ID Копирай Идентификатора Copy Link Копирай Връзката Open with Browser Отвори в Браузъра Add as Contact Доабви като Контакт Copy Message Копирай съобщение Copy Selection Копирай селекцията MessageDialogWrapper Remove %1 Премахни %1 Do you want to permanently remove %1? %1 nickname Наистина ли искаш да премахнеш %1 завинаги? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Този контакт повече няма да може да ти пише, и ще бъде уведомен за премахването. Той може да реши да ти прати нова покана за връзка. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. Интернет връзката на този компютър не е възпрепятствана. Искам да се свържа директно с Тор мрежата. Connect Свържи This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. Интернет връзката на този компютър е цензурирана, филтрирана или минава през прокси. Трябва да променя настройките на мрежата . Configure Настрой OfflineStateItem Configure Настрой Details Допълнително Connection failed Неуспешна връзка Connecting… \u2026 is ellipsis Свързване... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Внимание!</b> Отварянето на връзки с браузъра по подразбиране може да прекрати сигурната и анонимността ти.<br><br>Вместо това може да го <a href='.'>копираш</a>. Don't ask again for links from %1 Не искай повече връзки от %1 Don't ask again for any links (not recommended!) Не искай повече никакви връзки (не се препоръчва!) Open Browser Отвори Браузъра Cancel Откажи PreferencesDialog Ricochet Preferences Настройки на Рикошет General Главни Language Език Contacts Контакти Tor Тор About За Програмта QCocoaMenuItem Preference Настройки StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Неуспешно стартиране на Тор процесът. Това най-вероятно се дължи на инсталационна или системна грешка. Quit Изход TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Свързване с Тор мрежата... Back Назад Hide details Скрий допълнителната информация Show details Покажи допълнителна информация Done Готово TorConfigurationPage Does this computer need a proxy to access the internet? Този компютър нуждае ли се от прокси, за да се свърже с интернет? Proxy type: Вид Прокси: None Никакво Address: Адрес: IP address or hostname IP адрес или hostname Port: Порт: Username: Потребителско Име: Optional Незадължително Password: Парола: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? Минава ли интернет връзката на този компютър през firewall програма, която позволява връзка само през определени портове? Allowed ports: Разрешени портове: Example: 80,443 Пример: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Ако интернет връзката на този компютър е цензурирана, ще се наложи да намериш и използваш bridge relays. Enter one or more bridge relays (one per line): Въведи едно или повече bridge relays (по едно на ред): Back Назад Connect Свържи TorPreferences Running: Изпълнява: Yes Да No Не External Външен Control connected: Control connected: Circuits established: Circuits established: Hidden service: Hidden service: Online На Линия Offline Отписан Version: Версия: Error: <b>%1</b> %1 is error message Грешка: <b>%1</b> Configure Настрой TorStateWidget Connection failed Неуспешна връзка Connecting… \u2026 is ellipsis Свързване... Connecting… (%1%) %1 is progress percentage, e.g. 100 Свързване... (%1%) Online На линия Connected Свързан ricochet-1.1.4/translation/ricochet_cs.ts000066400000000000000000000671011300720305500205100ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Sdílejte vaše Richochet ID pro umožnění zádostí připojení Cancel Zrušit Add Přidat ContactActions Open Window Otevřít okno Details... Podrobnosti... Rename Přejmenovat Remove Odstranit ContactIDField <b>%1</b> is already your contact <b>%1</b> již je v seznamu kontaktů You can't add yourself as a contact Nelze přidat vlastní adresu na seznam kontaktů Enter an ID starting with <b>ricochet:</b> Zadejte ID začínající slovem <b>ricochet:</b> Copied to clipboard Zkopírováno do schránky Copy Zkopírovat ContactList Online Online Offline Offline Requests Žádosti Rejected Zamítnuto Outdated Zastaralé ContactPreferences Date added: Přidáno dne: Last seen: Naposledy online: Request: Žádost: Pending connection Čeká se na připojení Delivered Doručeno Accepted Přijato Error Chyba Rejected Zamítnuto %1 (Connected) %1 status, e.g. "Accepted" %1 (Připojeno) Response: Odpověď: Rename Přejmenovat Remove Odstranit ContactRequestDialog Someone new is asking to connect to you Někdo nový se s vámi chce spojit Reject Zamítnout Accept Přijmout ContactRequestFields ID: ID: Name: Jméno: Message: Zpráva: GeneralPreferences Use a single window for conversations Používat totéž okno pro všechny konverzace Open links in default browser without prompting Otevírat odkazy v defaultním prohlížeči bez dotazování Play audio notifications Povolit zvukové upozornění Volume Hlasitost Language Jazyk Restart Ricochet to apply changes Restartovat Ricochet aby se změny projevily LanguagePreferences Select Language Vybrat jazyk Restart Ricochet to apply changes Restartovat Ricochet aby se změny projevily LanguagesModel System default Systémový Main Ricochet Error Chyba programu Ricochet MainToolBar Add Contact Přidat kontakt Preferences Nastavení Click to add contacts Klikněte pro přidání kontaktů MainWindow Remove %1 Odstranit %1 Do you want to permanently remove %1? Chcete trvale odstranit kontakt %1? MessageDelegate %1 is offline %1 nickname %1 je offline Copy ID Kopírovat ID Copy Link Kopírovat odkaz Open with Browser Otevřít v prohlížeči Add as Contact Přidat jako kontakt Copy Message Kopírovat zprávu Copy Selection Kopírovat výběr MessageDialogWrapper Remove %1 Odstranit %1 Do you want to permanently remove %1? %1 nickname Chcete trvale odstranit kontakt %1? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Tento kontakt vám nebude moci posílat zprávy a bude informován o odstranění ze seznamu kontaktů. Může však zaslat novou žádost o spojení. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. Tento počítač se připojuje na internet bez jakýchkoli překážek. Chci se připojit přímo na síť Tor. Connect Připojit This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. Připojení tohoto počítače na internet je cenzurováno, filtrováno nebo vedeno přes proxy. Potřebuji upravit nastavení sítě. Configure Nastavit OfflineStateItem Configure Nastavit Details Podrobnosti Connection failed Připojení selhalo Connecting… \u2026 is ellipsis Připojuje se... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>-Upozornění!</b> Otvírání odkazů v defaultním prohlížeči naruší vaši bezpečnost a anonymitu.<br><br>Můžete místo toho odkaz <a href='.'>zkopírovat do schránky</a>. Don't ask again for links from %1 Znovu nepožadovat odkazy od kontaktu %1 Don't ask again for any links (not recommended!) Znovu nepožadovat žádné odkazy (nedoporučuje se!) Open Browser Otevřít prohlížeč Cancel Zrušit PreferencesDialog Ricochet Preferences Nastavení programu Ricochet General Obecné Language Jazyk Contacts Kontakty Tor Tor About O programu QCocoaMenuItem Preference Nastavení StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Proces programu Tor nebyl úspěšně spuštěn. Pravděpodobně se jedná o chybu instalace či systému. Quit Ukončit TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Připojuje se k síti Tor… Back Zpět Hide details Skrýt podrobnosti Show details Zobrazit podrobnosti Done Hotovo TorConfigurationPage Does this computer need a proxy to access the internet? Potřebuje tento počítač pro přístup na internet proxy? Proxy type: Typ proxy: None Žádný Address: Adresa: IP address or hostname IP adresa nebo hostname Port: Port: Username: Username: Optional Volitelné Password: Heslo: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? Prochází připojení tohoto počítače na internet přes firewall, jenž umožňuje připojení pouze na určité porty? Allowed ports: Povolené porty: Example: 80,443 Příklad: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Je-li připojení tohoto počítače na internet cenzurováno, potřebujete získat a používat bridge relays. Enter one or more bridge relays (one per line): Zadejte jeden či více bridge relays (na jednotlivé řádky): Back Zpět Connect Připojit TorPreferences Running: Spuštěno: Yes Ano No Ne External Externí Control connected: Kontrola připojena: Circuits established: Okruhy sestaveny: Hidden service: Skrytá služba: Online Online Offline Offline Version: Verze: Error: <b>%1</b> %1 is error message Chyba: <b>%1</b> Configure Nastavit TorStateWidget Connection failed Připojení selhalo Connecting… \u2026 is ellipsis Připojuje se... Connecting… (%1%) %1 is progress percentage, e.g. 100 Připojuje se… (%1%) Online Online Connected Připojeno ricochet-1.1.4/translation/ricochet_da.ts000066400000000000000000000665461300720305500205030ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Del dit Ricochet ID for at tillade kontaktanmodninger Cancel Annuller Add Tilføj ContactActions Open Window Åbn Vindue Details... Detaljer... Rename Omdøb Remove Fjern ContactIDField <b>%1</b> is already your contact <b>%1</b> er allerede på din kontaktliste You can't add yourself as a contact Du kan ikke tilføje dig selv som kontaktperson Enter an ID starting with <b>ricochet:</b> Skriv et ID der begynder med <b>ricochet:</b> Copied to clipboard Kopieret til clipboardet Copy Kopiér ContactList Online Online Offline Offline Requests Anmodninger Rejected Afvist Outdated Forældet ContactPreferences Date added: Dato tilføjet: Last seen: Sidst set: Request: Anmodning: Pending connection Afventer forbindelse Delivered Afleveret Accepted Accepteret Error Fejl Rejected Afvist %1 (Connected) %1 status, e.g. "Accepted" %1 (Forbundet) Response: Svar: Rename Omdøb Remove Fjern ContactRequestDialog Someone new is asking to connect to you Du har en ny kontaktanmodning Reject Afvis Accept Acceptér ContactRequestFields ID: ID: Name: Navn: Message: Besked: GeneralPreferences Use a single window for conversations Hold samtaler i ét vindue Open links in default browser without prompting Åbn links i browser uden at spørge Play audio notifications Afspil lydnotifikationer Volume Lydstyrke Language Sprog Restart Ricochet to apply changes Genstart Ricochet for at anvende nye indstillinger LanguagePreferences Select Language Vælg sprog Restart Ricochet to apply changes Genstart Ricochet for at anvende nye indstillinger LanguagesModel System default Standardindstillinger Main Ricochet Error Ricochet Fejl MainToolBar Add Contact Tilføj Kontaktperson Preferences Indstillinger Click to add contacts Klik for at tilføje kontaktpersoner MainWindow Remove %1 Fjern %1 Do you want to permanently remove %1? Vil du fjerne %1 permanent? MessageDelegate %1 is offline %1 nickname %1 er offline Copy ID Kopiér ID Copy Link Kopiér Link Open with Browser Åbn i browser Add as Contact Tilføj som kontaktperson Copy Message Kopiér besked Copy Selection Kopiér markeret tekst MessageDialogWrapper Remove %1 Fjern %1 Do you want to permanently remove %1? %1 nickname Vil du fjerne %1 permanent? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Denne kontaktperson vil ikke længere være i stand til at sende dig beskeder, og vil blive notificeret om fjernelsen. Kontaktpersonen kan vælge at sende dig en ny kontaktanmodning. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. Denne computers internetadgang er uhindret. Jeg vil gerne forbinde direkte til Tor-netværket. Connect Forbind This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. Denne computers internetadgang er censureret, filtreret eller bag en proxy. Jeg har brug for at konfigurere netværksindstillinger. Configure Konfigurér OfflineStateItem Configure Konfigurér Details Detaljer Connection failed Forbindelsen mislykkedes Connecting… \u2026 is ellipsis Forbinder… OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Advarsel!</b> Ved at åbne links med din browser kan du forringe din sikkerhed og anonymitet.<br><br>Du kan <a href='.'>kopiere adressen til clipboardet</a> i stedet. Don't ask again for links from %1 Spørg ikke igen om links fra %1 Don't ask again for any links (not recommended!) Spørg ikke om nogen links (ikke anbefalt!) Open Browser Åbn Browser Cancel Annullér PreferencesDialog Ricochet Preferences Indstillinger for Ricochet General Generelt Language Sprog Contacts Kontaktpersoner Tor Tor About Om QCocoaMenuItem Preference Indstilling StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Tor-processen kunne ikke starte. Dette er sandsynligvis en installations- eller systemfejl. Quit Afslut TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Forbinder til Tor-netværket… Back Tilbage Hide details Skjul detaljer Show details Vis detaljer Done Færdig TorConfigurationPage Does this computer need a proxy to access the internet? Skal denne computer forbinde gennem en proxy for at tilgå internettet? Proxy type: Proxy type: None Ingen Address: Adresse: IP address or hostname IP-adresse eller værtsnavn Port: Port: Username: Brugernavn: Optional Valgfrit Password: Adgangskode: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? Går denne computers internetforbindelse gennem en firewall der kun tillader forbindelser på visse porte? Allowed ports: Tilladte porte: Example: 80,443 Eksempel: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Hvis denne computers internetforbindelse er censureret, er du nødt til at finde og benytte bridge relæer. Enter one or more bridge relays (one per line): Indsæt én eller flere bridgerelæer (én per linje): Back Tilbage Connect Forbind TorPreferences Running: Kører: Yes Ja No Nej External Ekstern Control connected: Kontrol forbundet: Circuits established: Kredsløb etableret: Hidden service: Skjult service: Online Online Offline Offline Version: Version: Error: <b>%1</b> %1 is error message Fejl: <b>%1</b> Configure Konfigurér TorStateWidget Connection failed Forbindelse mislykkedes Connecting… \u2026 is ellipsis Forbinder… Connecting… (%1%) %1 is progress percentage, e.g. 100 Forbinder… (%1) Online Online Connected Forbundet ricochet-1.1.4/translation/ricochet_de.ts000066400000000000000000000670641300720305500205030ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Teile deine Ricochet ID, um Verbindungsanfragen zu erlauben Cancel Abbrechen Add Hinzufügen ContactActions Open Window Fenster öffnen Details... Details... Rename Umbenennen Remove Entfernen ContactIDField <b>%1</b> is already your contact <b>%1</b> ist bereits dein Kontakt You can't add yourself as a contact Du kannst dich nicht selbst als Kontakt hinzufügen Enter an ID starting with <b>ricochet:</b> Gebe eine ID an, beginnend mit <b>ricochet:<b> Copied to clipboard In die Zwischenablage kopiert Copy Kopieren ContactList Online Online Offline Offline Requests Anfragen Rejected Abgewiesen Outdated veraltet ContactPreferences Date added: Hinzugefügt am: Last seen: Zuletzt gesehen: Request: Anfrage: Pending connection Laufende Verbindung Delivered Gesendet Accepted Akzeptiert Error Fehler Rejected Abgewiesen %1 (Connected) %1 status, e.g. "Accepted" %1 (Verbunden) Response: Antwort: Rename Umbenennen Remove Entfernen ContactRequestDialog Someone new is asking to connect to you Jemand unbekanntes möchte sich zu Ihnen verbinden Reject Abweisen Accept Annehmen ContactRequestFields ID: ID: Name: Name: Message: Nachricht: GeneralPreferences Use a single window for conversations Nutze ein einzelnes Fenster für Unterhaltungen Open links in default browser without prompting Öffne Links im Standardbrowser ohne Nachfrage Play audio notifications Benachrichtigungston abspielen Volume Lautstärke Language Sprache Restart Ricochet to apply changes Starte Ricochet neu, um Änderungung anzuwenden LanguagePreferences Select Language Sprache auswählen Restart Ricochet to apply changes Starte Ricochet neu, um Änderungung anzuwenden LanguagesModel System default Systemvorgabe Main Ricochet Error Ricochet Fehler MainToolBar Add Contact Kontakt hinzufügen Preferences Einstellungen Click to add contacts Klicken, um Kontakte hinzuzufügen MainWindow Remove %1 Entferne %1 Do you want to permanently remove %1? Möchten Sie 1% permanent entfernen? MessageDelegate %1 is offline %1 nickname %1 ist offline Copy ID Kopiere ID Copy Link Kopiere Link Open with Browser Im Browser öffnen Add as Contact Als Kontakt hinzufügen Copy Message Nachricht kopieren Copy Selection Auswahl kopieren MessageDialogWrapper Remove %1 Entferne %1 Do you want to permanently remove %1? %1 nickname Möchten Sie %1 permanent entfernen? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Dieser Kontakt wird Ihnen nicht mehr schreiben können, und wird über das Entfernen in Kenntnis gesetzt. Eine neue Verbindungsanfrage kann von ihm/ihr gesendet werden. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. Die Internetverbindung dieses Rechners ist frei von Hindernissen. Ich möchte gerne eine direkte Verbindung zum Tor-Netzwerk herstellen. Connect Verbinden This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. Die Internetverbindung dieses Rechners ist zensiert, gefiltert oder vermittelt. Configure Konfigurieren OfflineStateItem Configure Konfigurieren Details Details Connection failed Verbindung gescheitert Connecting… \u2026 is ellipsis Verbindet... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Warnung!</b> Links mit deinem Standardbrowser zu öffnen kann deine Sicherheit und Anonymität beeinträchtigen.<br> <br>Du kannst stattdessen <a href='.'>in die Zwischenablage kopieren.</a> Don't ask again for links from %1 Nicht mehr fragen bei Links von %1 Don't ask again for any links (not recommended!) Für keinen Link mehr nachfragen (nicht empfohlen!) Open Browser Browser öffnen Cancel Abbrechen PreferencesDialog Ricochet Preferences Ricochet Einstellungen General Allgemein Language Sprache Contacts Kontakte Tor Tor About Über QCocoaMenuItem Preference Einstellung StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Der Tor-Prozess konnte nicht gestartet werden. Es handelt sich wahrscheinlich um einen Installations- oder einen Systemfehler. Quit Beenden TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Verbinde zum Tor-Netzwerk... Back Zurück Hide details Details verstecken Show details Details anzeigen Done Fertig TorConfigurationPage Does this computer need a proxy to access the internet? Benötigt dieser Rechner einen Proxy um sich mit dem Internet zu verbinden? Proxy type: Proxytyp: None Keiner Address: Adresse: IP address or hostname IP-Adresse oder Rechnername Port: Port: Username: Benutzername: Optional Fakultativ Password: Passwort: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? Geht die Verbindung dieses Rechners durch eine Firewall, die nur Verbindungen zu manchen Ports erlaubt? Allowed ports: Erlaubte Ports: Example: 80,443 Beispiel: 80, 443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Sollte die Internetverbindung dieses Rechners zensiert sein, werden Sie Brücken-Relays finden und nutzen müssen. Enter one or more bridge relays (one per line): Geben Sie ein oder mehrere Brücken-Relays an (eins pro Zeile): Back Zurück Connect Verbinden TorPreferences Running: Läuft: Yes Ja No Nein External Extern Control connected: Control verbunden: Circuits established: Circuits aufgebaut: Hidden service: Versteckter Service: Online Online Offline Offline Version: Version: Error: <b>%1</b> %1 is error message Fehler: <b>%1</b> Configure Konfigurieren TorStateWidget Connection failed Verbindung gescheitert Connecting… \u2026 is ellipsis Verbinde... Connecting… (%1%) %1 is progress percentage, e.g. 100 Verbinde... (%1%) Online Online Connected Verbunden ricochet-1.1.4/translation/ricochet_en.ts000066400000000000000000000645161300720305500205140ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 AddContactDialog Share your Ricochet ID to allow connection requests Cancel Add ContactActions Open Window Details... Rename Remove ContactIDField <b>%1</b> is already your contact You can't add yourself as a contact Enter an ID starting with <b>ricochet:</b> Copied to clipboard Copy ContactList Online Offline Requests Rejected Outdated ContactPreferences Date added: Last seen: Request: Pending connection Delivered Accepted Error Rejected %1 (Connected) %1 status, e.g. "Accepted" Response: Rename Remove ContactRequestDialog Someone new is asking to connect to you Reject Accept ContactRequestFields ID: Name: Message: GeneralPreferences Use a single window for conversations Open links in default browser without prompting Play audio notifications Volume Language Restart Ricochet to apply changes LanguagesModel System default Main Ricochet Error MainToolBar Add Contact Preferences Click to add contacts MainWindow Remove %1 Do you want to permanently remove %1? MessageDelegate %1 is offline %1 nickname Copy ID Copy Link Open with Browser Add as Contact Copy Message Copy Selection MessageDialogWrapper Remove %1 Do you want to permanently remove %1? %1 nickname This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. Connect This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. Configure OfflineStateItem Configure Details Connection failed Connecting… \u2026 is ellipsis OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. Don't ask again for links from %1 Don't ask again for any links (not recommended!) Open Browser Cancel PreferencesDialog Ricochet Preferences General Contacts Tor About QCocoaMenuItem Preference StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Quit TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Back Hide details Show details Done TorConfigurationPage Does this computer need a proxy to access the internet? Proxy type: None Address: IP address or hostname Port: Username: Optional Password: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? Allowed ports: Example: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Enter one or more bridge relays (one per line): Back Connect TorPreferences Running: Yes No External Control connected: Circuits established: Hidden service: Online Offline Version: Error: <b>%1</b> %1 is error message Configure TorStateWidget Connection failed Connecting… \u2026 is ellipsis Connecting… (%1%) %1 is progress percentage, e.g. 100 Online Connected ricochet-1.1.4/translation/ricochet_es.ts000066400000000000000000000672211300720305500205150ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Comparta su ID (identificador) de Ricochet para permitir solicitudes de conexión Cancel Cancelar Add Añadir ContactActions Open Window Abrir Ventana Details... Detalles... Rename Renombrar Remove Eliminar ContactIDField <b>%1</b> is already your contact <b>%1</b> ya es su contacto You can't add yourself as a contact No puede añadirse a usted mismo como un contacto Enter an ID starting with <b>ricochet:</b> Introduzca un ID comience con <b>ricochet:</b> Copied to clipboard Copiado al portapapeles Copy Copiar ContactList Online En línea Offline Fuera de línea Requests Solicitudes Rejected Rechazada Outdated Caducada ContactPreferences Date added: Añadido desde: Last seen: Visto por última vez: Request: Solicitud: Pending connection Conexión pendiente Delivered Entregado Accepted Aceptado Error Error Rejected Rechazado %1 (Connected) %1 status, e.g. "Accepted" %1 (Conectado) Response: Respuesta: Rename Renombrar Remove Eliminar ContactRequestDialog Someone new is asking to connect to you Alguien nuevo está pidiendo conectarse con usted Reject Rechazar Accept Aceptar ContactRequestFields ID: ID (identificador): Name: Nombre: Message: Mensaje: GeneralPreferences Use a single window for conversations Usar una única ventana para conversaciones Open links in default browser without prompting Abrir enlaces en el navegador predeterminado sin pedir confirmación Play audio notifications Reproducir notificaciones de audio Volume Volumen Language Idioma Restart Ricochet to apply changes Reiniciar Ricochet para aplicar los cambios LanguagePreferences Select Language Seleccionar idioma Restart Ricochet to apply changes Reiniciar Ricochet para aplicar los cambios LanguagesModel System default Predeterminado del sistema Main Ricochet Error Error de Ricochet MainToolBar Add Contact Añadir Contacto Preferences Preferencias Click to add contacts Haga clic para añadir contactos MainWindow Remove %1 Eliminar %1 Do you want to permanently remove %1? ¿Quiere eliminar permanentemente a %1? MessageDelegate %1 is offline %1 nickname %1 está fuera de línea Copy ID Copiar ID (identificador) Copy Link Copiar Enlace Open with Browser Abrir con el Navegador Add as Contact Añadir como Contacto Copy Message Copiar mensaje Copy Selection Copiar selección MessageDialogWrapper Remove %1 Eliminar %1 Do you want to permanently remove %1? %1 nickname ¿Quiere eliminar permanentemente a %1? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Este contacto ya no podrá enviarle mensajes, y será notificado sobre la eliminación. Pueden elegir enviar una nueva solicitud de conexión. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. La conexión a Internet de este equipo está libre de obstáculos. Me gustaría conectar directamente a la red Tor. Connect Conectar This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. La conexión a Internet de este equipo está censurada, filtrada, o proxyficada. Necesito configurar las preferencias de red. Configure Configurar OfflineStateItem Configure Configurar Details Detalles Connection failed Conexión fallida Connecting… \u2026 is ellipsis Conectando... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>¡Advertencia!</b> Abrir enlaces con su navegador predeterminado perjudicará su seguridad y anonimato. <br><br>En su lugar puede <a href='.'>copiarlos al portapapeles</a>. Don't ask again for links from %1 No volver a preguntar por enlaces desde %1 Don't ask again for any links (not recommended!) No volver a preguntar por ningún enlace (¡no recomendado!) Open Browser Abrir Navegador Cancel Cancelar PreferencesDialog Ricochet Preferences Preferencias de Ricochet General General Language Idioma Contacts Contactos Tor Tor About Acerca de QCocoaMenuItem Preference Preferencia StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. El proceso Tor no se inició con éxito. Lo más probable es que esto sea un error de instalación o de sistema. Quit Salir TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Conectando a la red Tor... Back Atrás Hide details Ocultar detalles Show details Mostrar detalles Done Hecho TorConfigurationPage Does this computer need a proxy to access the internet? ¿Este equipo necesita un proxy para acceder a Internet? Proxy type: Tipo de proxy: None Ninguno Address: Dirección: IP address or hostname Dirección IP o nombre del equipo Port: Puerto: Username: Nombre de usuario: Optional Opcional Password: Contraseña: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? ¿La conexión a Internet de este equipo va a través de un cortafuegos (firewall) que sólo permite conexiones a ciertos puertos? Allowed ports: Puertos permitidos: Example: 80,443 Ejemplo: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Si la conexión a Internet de este equipo está bajo censura, necesitará obtener y usar repetidores puente (bridge relays). Enter one or more bridge relays (one per line): Introduzca uno o más repetidores puente (uno por línea): Back Atrás Connect Conectar TorPreferences Running: Ejecutar: Yes No No External Externo Control connected: Control conectado: Circuits established: Circuitos establecidos: Hidden service: Servicio oculto: Online En línea Offline Fuera de línea Version: Versión: Error: <b>%1</b> %1 is error message Error: <b>%1</b> Configure Configurar TorStateWidget Connection failed Conexión fallida Connecting… \u2026 is ellipsis Conectando... Connecting… (%1%) %1 is progress percentage, e.g. 100 Conectando... (%1%) Online En línea Connected Conectado ricochet-1.1.4/translation/ricochet_et_EE.ts000066400000000000000000000663461300720305500210760ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Jaga oma Ricochet ID ühenduste jaoks Cancel Tühista Add Lisa ContactActions Open Window Ava aken Details... Detailid... Rename Nimeta ümber Remove Eemalda ContactIDField <b>%1</b> is already your contact <b>%1</b> on juba sinu kontaktides You can't add yourself as a contact Sa ei saa lisada ennast kontaktiks Enter an ID starting with <b>ricochet:</b> Sisesta ID koos <b>ricochet:</b> Copied to clipboard Kopeeri lõikeväljale Copy Kopeeri ContactList Online Võrgus Offline Võrgust väljas Requests Päringud Rejected Tagasi lükatud Outdated Vananenud ContactPreferences Date added: Lisatud kuupäeval: Last seen: Viimati nähtud: Request: Päring: Pending connection Ootel ühendus Delivered Tarnitud Accepted Aksepteeritud Error Viga Rejected Tagasi lükatud %1 (Connected) %1 status, e.g. "Accepted" %1 (Ühendatud) Response: Vastus: Rename Muuda nime Remove Eemalda ContactRequestDialog Someone new is asking to connect to you Keegi uus palub sinuga ühenduda Reject Lükka tagasi Accept Luba ContactRequestFields ID: ID: Name: Nimi: Message: Sõnum: GeneralPreferences Use a single window for conversations Kasuta eraldi akent suhtlemiseks Open links in default browser without prompting Ava lingid vaikimsi brauseriga ilma märguandeta Play audio notifications Mängi heli märguandeid Volume Heli valjus Language Keel Restart Ricochet to apply changes Taaskäivita Ricochet muudatuste jaoks LanguagePreferences Select Language Keelevalik Restart Ricochet to apply changes Taaskäivita Ricochet muudatuste jaoks LanguagesModel System default Süsteemi keel Main Ricochet Error Ricocheti viga MainToolBar Add Contact Lisa kontakt Preferences Eelistused Click to add contacts Kliki kontaktide lisamiseks MainWindow Remove %1 Eemalda %1 Do you want to permanently remove %1? Kas sa tahad tingimata eemaldada: %1? MessageDelegate %1 is offline %1 nickname %1 on võrgust väljas Copy ID Kopeeri ID Copy Link Kopeeri link Open with Browser Ava koos veebilehitsejaga Add as Contact Lisa kontaktina Copy Message Kopeeri sõnum Copy Selection Kopeeri valik MessageDialogWrapper Remove %1 Eemalda %1 Do you want to permanently remove %1? %1 nickname Kas sa tahad tingimata eemaldada %1? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. See kontakt ei saa sulle sõnumeid saata ja on märgitud eemaldamiseks. Kontakt saab saata sulle uue päringu ühendamiseks. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. Selle arvuti interneti ühendus on vaba takistustest. Tahaksin ühenduda Tor võrku. Connect Ühendus This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. Selle arvuti interneti ühendus on senseeritud, filtreeritud või proksi all. Vajan seadistamaks võrguühendust. Configure Seadistus OfflineStateItem Configure Seadistamine Details Detailid Connection failed Ühenduse viga Connecting… \u2026 is ellipsis Ühendumine... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Hoiatus!</b> Lingi avamine sinu veebilehitsejaga võib vähendada sinu turvalisust ja anonüümsust.<br><br>Sa võid hoopis<a href='.'>kopeerida lõikeväljale</a> . Don't ask again for links from %1 Ära küsi enam linke %1 Don't ask again for any links (not recommended!) Ära küsi rohkem mitte ühegi linkide kohta (ei ole soovitatud!) Open Browser Ava Brauser Cancel Tühista PreferencesDialog Ricochet Preferences Ricochet seadistused General Üldine Language Keel Contacts Kontaktid Tor Tor About Lähemalt QCocoaMenuItem Preference Eelistused StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Tor protsesside käivitamine ei ole õnnestunud. See on tavaliselt paigaldusel või süsteemiveal. Quit Lahku TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Ühendumine Tor võrku... Back Tagasi Hide details Peida detailid Show details Näita detaile Done Tehtud TorConfigurationPage Does this computer need a proxy to access the internet? Kas see arvuti vajab interneti ühendamiseks proksit? Proxy type: Proksi tüüp: None Ei ole Address: Aadress: IP address or hostname IP aadress või hostinimi Port: Port: Username: Kasutajanimi: Optional Valikuline Password: Parool: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? Kas see arvuti vajab interneti ühenduseks läbi tulemüüri ainult teatuid porte? Allowed ports: Lubatud pordid: Example: 80,443 Näide: 80.443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Kui selle arvuti internetiühendus on senseeritud, siis vajad ja pead saama kasutada sildühendusi. Enter one or more bridge relays (one per line): Sisesta üks või rohkem sildühendusi (üks rea kohta): Back Tagasi Connect Ühenda TorPreferences Running: Jooksutab: Yes Jah No Ei External Väline Control connected: Kontrollitud ühendus: Circuits established: Ringühendused kehtestatud: Hidden service: Peidetud teenus: Online Võrgus Offline Võrgust väljas Version: Versioon: Error: <b>%1</b> %1 is error message Viga: <b>%1</b> Configure Seadistus TorStateWidget Connection failed Ühenduse viga Connecting… \u2026 is ellipsis Ühendumine... Connecting… (%1%) %1 is progress percentage, e.g. 100 Ühendamine...(%1%) Online Võrgus Connected Ühendatud ricochet-1.1.4/translation/ricochet_fi.ts000066400000000000000000000670771300720305500205150ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Jaa oma ID kontakteillesi, että he voivat lähettää yhteyspyynnön Cancel Peruuta Add Lisää ContactActions Open Window Avaa ikkuna Details... Tiedot... Rename Nimeä uudelleen Remove Poista ContactIDField <b>%1</b> is already your contact <b>%1</b> on jo kontaktisi You can't add yourself as a contact Et voi lisätä itseäsi kontaktiksi Enter an ID starting with <b>ricochet:</b> Anna ID joka alkaa <b>ricochet:</b> Copied to clipboard Kopioitu leikepöydälle Copy Kopioi ContactList Online Linjoilla Offline Ei linjoilla Requests Pyynnöt Rejected Torjuttu Outdated Vanhentunut ContactPreferences Date added: Lisätty: Last seen: Nähty viimeksi: Request: Pyyntö: Pending connection Odotetaan yhteyttä Delivered Toimitettu Accepted Hyväksytty Error Virhe Rejected Torjuttu %1 (Connected) %1 status, e.g. "Accepted" %1 (Yhdistetty) Response: Vastaus: Rename Nimeä uudelleen Remove Poista ContactRequestDialog Someone new is asking to connect to you Sinulle on uusi yhteyspyyntö Reject Torju Accept Hyväksy ContactRequestFields ID: Saajan ID: Name: Nimi: Message: Viestisi: GeneralPreferences Use a single window for conversations Käytä yhtä ikkunaa keskusteluille Open links in default browser without prompting Avaa linkit oletusselaimella ilman vahvistusta Play audio notifications Soita huomioäänimerkkejä Volume Äänenvoimakkuus Language Kieli Restart Ricochet to apply changes Käynnistä Ricochet uudelleen ottaaksesi tehdyt muutokset käyttöön LanguagePreferences Select Language Valitse kieli Restart Ricochet to apply changes Käynnistä Ricochet uudelleen ottaaksesi tehdyt muutokset käyttöön LanguagesModel System default Systeemin oletus Main Ricochet Error Ricochetin virhe MainToolBar Add Contact Lisää kontakti Preferences Asetukset Click to add contacts Klikkaa lisätäksesi kontakteja MainWindow Remove %1 Poista %1 Do you want to permanently remove %1? Haluatko poistaa tämän pysyvästi: %1? MessageDelegate %1 is offline %1 nickname %1 ei ole linjoilla Copy ID Kopioi ID Copy Link Kopioi linkki Open with Browser Avaa selaimessa Add as Contact Lisää kontaktiksi Copy Message Kopioi viesti Copy Selection Kopioi valinta MessageDialogWrapper Remove %1 Poista %1 Do you want to permanently remove %1? %1 nickname Haluatko poistaa tämän pysyvästi: %1? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Tämä kontakti ei voi enää viestiä kanssasi ja hän saa tästä tiedon. Kontakti voi halutessaan lähettää sinulle uuden yhteyspyynnön. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. Tämän tietokoneen internet-yhteydelle ei ole rajoittavia esteitä. Haluan muodostaa suoran yhteyden Tor-verkkoon. Connect Yhdistä This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. Tämän tietokoneen internet-yhteyttä on sensuroitu tai filtteröity tai yhteys käyttää välityspalvelimia. Minun täytyy määrittää yhteysasetukset. Configure Määritä OfflineStateItem Configure Määritä Details Tiedot Connection failed Yhteys epäonnistui Connecting… \u2026 is ellipsis Yhdistetään... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Varoitus!</b> Linkkien avaaminen oletusselaimessasi voi vaarantaa tietoturvaasi ja yksityisyyttäsi.<br><br>Voit <a href='.'>kopioida linkin leikepöydälle</a> tämän sijaan. Don't ask again for links from %1 Älä kysy uudestaan linkeistä joiden lähteenä on %1 Don't ask again for any links (not recommended!) Älä kysy uudestaan mistään linkeistä (Ei suositella!) Open Browser Avaa selain Cancel Peruuta PreferencesDialog Ricochet Preferences Ricochet asetukset General Yleistä Language Kieli Contacts Kontaktit Tor Tor About Tietoa QCocoaMenuItem Preference Asetus StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Tor-prosessin käynnistys ei onnistunut. Tämä voi johtua asennus- tai järjestelmävirheestä. Quit Lopeta TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Yhdistetään Tor-verkkoon... Back Palaa Hide details Piilota tiedot Show details Näytä tiedot Done Valmis TorConfigurationPage Does this computer need a proxy to access the internet? Tarvitseeko tämä tietokone välityspalvelimen internet-yhteyden luomista varten? Proxy type: Välityspalvelimen tyyppi: None Ei mitään Address: Osoite: IP address or hostname IP-osoite tai isäntä: Port: Portti: Username: Käyttäjänimi: Optional Vaihtoehtoinen Password: Salasana: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? Suojaako tämän tietokoneen internet-yhteyttä palomuuri, joka sallii liikenteen vain ennalta määrätyistä porteista? Allowed ports: Sallitut portit: Example: 80,443 Esimerkiksi: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Jos tämän tietokoneen internet-yhteyttä on sensuroitu, sinun täytyy käyttää sillattuja reitittimiä Enter one or more bridge relays (one per line): Anna yksi tai useampi reititin (erottele rivinvaihdolla): Back Palaa Connect Yhdistä TorPreferences Running: Käynnissä: Yes Kyllä No Ei External Ulkoinen Control connected: Ohjaus kytketty: Circuits established: Alueet perustettu: Hidden service: Piilotettu palvelu: Online Kyllä - Online Offline ei - Offline Version: Versio: Error: <b>%1</b> %1 is error message Virhe: <b>%1</b> Configure Määritä TorStateWidget Connection failed Yhteys epäonnistui Connecting… \u2026 is ellipsis Yhdistetään... Connecting… (%1%) %1 is progress percentage, e.g. 100 Yhdistetään… (%1%) Online Online Connected Yhdistetty ricochet-1.1.4/translation/ricochet_fil_PH.ts000066400000000000000000000665601300720305500212540ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Ipamahagi ang iyong Ricochet ID para makatanggap ng mga kahilingan ng koneksyon Cancel Kanselahin Add Magdagdag ContactActions Open Window Magbukas ng Window Details... Detalye... Rename Palitan ang pangalan Remove Tanggalin ContactIDField <b>%1</b> is already your contact Si <b>%1</b> ay nasa iyong mga contact na. You can't add yourself as a contact Hindi mo maaaring idagdag ang iyong sarili bilang isang contact Enter an ID starting with <b>ricochet:</b> Magpasok ng isang ID na nagsisimula sa <b>ricochet:</ b> Copied to clipboard Kinopya sa clipboard Copy Kopyahin ContactList Online Online Offline Offline Requests Mga Kahilingan Rejected Tinanggihan Outdated ContactPreferences Date added: Petsa nang idinagdag: Last seen: Huling nakita: Request: Hiling: Pending connection Naghihintay na mga koneksyon Delivered Naihatid Accepted Natanggap Error Pagkakamali Rejected Tinanggihan %1 (Connected) %1 status, e.g. "Accepted" %1 (Nakakonekta) Response: Tugon: Rename Palitan ang pangalan Remove Tanggalin ContactRequestDialog Someone new is asking to connect to you May bagong tao na nagtatanong upang kumonekta sa iyo Reject Tanggihan Accept Tanggapin ContactRequestFields ID: ID: Name: Pangalan: Message: Mensahe: GeneralPreferences Use a single window for conversations Gumamit ng solong window para sa mga pag-uusap Open links in default browser without prompting Buksan ang mga link sa default na browser nang walang pagdikta Play audio notifications Volume Language Restart Ricochet to apply changes LanguagesModel System default Main Ricochet Error Pagkakamali sa Ricochet MainToolBar Add Contact Magdagdag ng Contact Preferences Kagustuhan Click to add contacts I-click upang magdagdag ng contacts MainWindow Remove %1 Tanggalin si %1 Do you want to permanently remove %1? Gusto mo bang permanenteng tanggalin si %1? MessageDelegate %1 is offline %1 nickname Si %1 ay offline Copy ID Kopyahin ang ID Copy Link Kopyahin ang Link Open with Browser Buksan gamit ang Browser Add as Contact Idagdag bilang Contact Copy Message Copy Selection MessageDialogWrapper Remove %1 Tanggalin si %1 Do you want to permanently remove %1? %1 nickname Gusto mo bang permanenteng tanggalin si %1? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Ang contact na ito ay hindi na magagawang magpadala ng mensahe sa iyo, siya ay aabisuhan tungkol sa pag-alis. Maaari nilang piliin na magpadala ng bagong kahilingan ng koneksyon. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. Ang koneksyon sa Internet ng computer na ito ay walang hadlang. Gusto kong direktang kumonekta sa Tor network. Connect Ikonekta This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. Ang koneksyon sa Internet ng computer na ito ay censored, na-filter, o proxied. Kailangan ko i-configure ang mga setting ng network. Configure I-configure OfflineStateItem Configure I-configure Details Detalye Connection failed Nabigo ang koneksyon Connecting… \u2026 is ellipsis Kumukonekta... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Babala!</ b> Pagbukas ng mga link gamit ang iyong default na browser ay makakapinsala sa iyong seguridad at anonymity.<br> <br> Maaari mong <a href='.'>kopyahin sa clipboard</a> sa halip. Don't ask again for links from %1 Huwag nang tatanungin ulit para sa mga link mula kay %1 Don't ask again for any links (not recommended!) Huwag nang tatanungin ulit para sa anumang mga link (hindi inirerekomenda!) Open Browser Buksan ang Browser Cancel Kanselahin PreferencesDialog Ricochet Preferences Kagustuhan sa Ricochet General Pangkalahatan Contacts Mga Contact Tor Tor About Patungkol QCocoaMenuItem Preference Kagustuhan StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Ang proseso ng Tor ay hindi matagumpay na nakapagsimula. Ito ay pinaka-malamang na pagkakamali sa pag-install o pagkakamali sa sistema. Quit Mag-quit TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Kumokonekta sa Tor network... Back Bumalik Hide details Itago ang mga detalye Show details Ipakita ang mga detalye Done Tapos na TorConfigurationPage Does this computer need a proxy to access the internet? Kailangan ba ng computer na ito ng proxy upang ma-access ang internet? Proxy type: Uri ng Proxy: None Wala Address: Address: IP address or hostname IP address o hostname Port: Port: Username: Username: Optional Opsyonal Password: Password: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? Pumupunta ba ang koneksyon sa Internet ng computer na ito sa pamamagitan ng isang firewall na nagbibigay-daan lamang ng mga koneksyon sa ilang mga ports? Allowed ports: Pinayagan na ports: Example: 80,443 Halimbawa: 80, 443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Kung ang koneksyon sa Internet ng computer na ito ay censored, kakailanganin mo na makuha at gumamit ng bridge relays. Enter one or more bridge relays (one per line): Magpasok ng isa o higit pang mga bridge relays (isa bawat linya): Back Bumalik Connect Kumonekta TorPreferences Running: Tumatakbo: Yes Oo No Hindi External Panlabas Control connected: Naakakonekta ang Control: Circuits established: Naitatag ang Circuits: Hidden service: Nakatagong serbisyo: Online Online Offline Offline Version: Bersyon: Error: <b>%1</b> %1 is error message Pagkakamali: <b>%1</b> Configure I-configure TorStateWidget Connection failed Nabigo ang koneksyon Connecting… \u2026 is ellipsis Kumukonekta... Connecting… (%1%) %1 is progress percentage, e.g. 100 Kumukonekta… (%1%) Online Online Connected Konektado ricochet-1.1.4/translation/ricochet_fr.ts000066400000000000000000000675511300720305500205230ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Partagez votre ID Ricochet pour autoriser les demandes de connexion Cancel Annuler Add Ajouter ContactActions Open Window Ouvrir la fenêtre Details... Détails... Rename Renommer Remove Supprimer ContactIDField <b>%1</b> is already your contact <b>%1</b> est déjà votre contact You can't add yourself as a contact Vous ne pouvez pas vous ajouter vous-même en tant que contact Enter an ID starting with <b>ricochet:</b> Entrez un ID commençant par <b>ricochet:</b> Copied to clipboard Copié dans le presse-papier Copy Copier ContactList Online Connecté Offline Déconnecté Requests Requêtes Rejected Rejeté Outdated Caduc ContactPreferences Date added: Date d'ajout : Last seen: Dernière apparition : Request: Requête : Pending connection En attente de connexion Delivered Délivré Accepted Accepté Error Erreur Rejected Rejeté %1 (Connected) %1 status, e.g. "Accepted" %1 (Connecté) Response: Réponse : Rename Renommer Remove Supprimer ContactRequestDialog Someone new is asking to connect to you Une nouvelle personne demande à se connecter à vous Reject Refuser Accept Accepter ContactRequestFields ID: ID : Name: Nom : Message: Message : GeneralPreferences Use a single window for conversations Utiliser une seule fenêtre pour les conversations Open links in default browser without prompting Ouvrir les hyperliens directement dans le navigateur par défaut sans demander à chaque fois Play audio notifications Jouer les notifications sonores Volume Volume Language Langue Restart Ricochet to apply changes Redémarrer l'application pour applique les modifications LanguagePreferences Select Language Choisir la langue Restart Ricochet to apply changes Redémarrer l'application pour applique les modifications LanguagesModel System default Par défaut du système Main Ricochet Error Erreur de Ricochet MainToolBar Add Contact Ajouter un contact Preferences Préférences Click to add contacts Cliquez pour ajouter des contacts MainWindow Remove %1 Supprimer %1 Do you want to permanently remove %1? Voulez-vous supprimer %1 définitivement ? MessageDelegate %1 is offline %1 nickname %1 est hors-ligne Copy ID Copier ID Copy Link Copier l'hyperlien Open with Browser Ouvrir avec le navigateur Add as Contact Ajouter comme contact Copy Message Copier le message Copy Selection Copier la sélection MessageDialogWrapper Remove %1 Supprimer %1 Do you want to permanently remove %1? %1 nickname Voulez-vous supprimer %1 définitivement ? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Ce contact ne sera plus en mesure de vous envoyer des messages et sera informé du retrait. Il pourra toutefois vous envoyer une nouvelle demande de connexion. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. La connexion internet de cet ordinateur est libre d'obstacles. Je voudrais me connecter directement au réseau Tor. Connect Connexion This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. La connexion internet de cet ordinateur est censurée, filtrée, ou derrière un proxy. J'ai besoin de configurer les paramètres réseau. Configure Configurer OfflineStateItem Configure Configurer Details Détails Connection failed Connexion échouée Connecting… \u2026 is ellipsis Connexion... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Attention !</b> Ouvrir des hyperliens avec votre navigateur par défaut pourrait nuire à votre sécurité et votre anonymat.<br><br>Vous pouvez <a href='.'>copier dans le presse-papier</a> à la place. Don't ask again for links from %1 Ne plus demander pour des hyperliens provenant de %1 Don't ask again for any links (not recommended!) Ne plus demander à nouveau pour tous les hyperliens (non recommandé) Open Browser Ouvrir le navigateur Cancel Annuler PreferencesDialog Ricochet Preferences Préférences de Ricochet General Général Language Langue Contacts Contacts Tor Tor About À propos QCocoaMenuItem Preference Préférences StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Le processus Tor n'a pas démarré avec succès. C'est probablement dû à une erreur d'installation ou du système. Quit Quitter TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Connexion au réseau Tor... Back Retour Hide details Cacher les détails Show details Afficher les détails Done Terminé TorConfigurationPage Does this computer need a proxy to access the internet? Est-ce que cet ordinateur à besoin d'un proxy pour accéder à Internet ? Proxy type: Type de proxy : None Aucun Address: Adresse : IP address or hostname Adresse IP ou nom d'hôte Port: Port : Username: Nom d'utilisateur : Optional En option Password: Mot de passe : Does this computer's Internet connection go through a firewall that only allows connections to certain ports? Est-ce que la connexion Internet de cet ordinateur passe à travers un pare-feu qui autorise uniquement les connexions à certains ports ? Allowed ports: Ports autorisés : Example: 80,443 Exemple : 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Si la connexion Internet de cet ordinateur est censurée, vous allez devoir obtenir et utiliser un pont Tor. Enter one or more bridge relays (one per line): Entrez un ou plusieurs ponts Tor (un par ligne) : Back Retour Connect Connexion TorPreferences Running: En cours : Yes Oui No Non External Externe Control connected: Contrôle connecté : Circuits established: Circuits établis : Hidden service: Service caché : Online En ligne Offline Hors-ligne Version: Version : Error: <b>%1</b> %1 is error message Erreur : <b>%1</b> Configure Configurer TorStateWidget Connection failed Connexion échouée Connecting… \u2026 is ellipsis Connexion... Connecting… (%1%) %1 is progress percentage, e.g. 100 Connexion… (%1%) Online En ligne Connected Connecté ricochet-1.1.4/translation/ricochet_he.ts000066400000000000000000000706161300720305500205040ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests שתף את ה Ricochet ID שלך על מנת לאפשר חיבורים חדשים Cancel בטל Add הוסיף ContactActions Open Window פתח חלון Details... פרטים נוספים... Rename שנה שם Remove מחק ContactIDField <b>%1</b> is already your contact <b>%1</b> הינו איש קשר קיים You can't add yourself as a contact אי אפשר להוסיף עצמך לאנשי קשר Enter an ID starting with <b>ricochet:</b> הכנס שם משתמש אשר מתחיל ב <b>ricochet:</b> Copied to clipboard הועתק ללוח Copy העתק ContactList Online מחובר Offline מנותק Requests בקשות Rejected קשרים שנדחו Outdated אינו בתוקף ContactPreferences Date added: הוסף בתאריך Last seen: נראה לאחרונה Request: בקשה: Pending connection חיבורים ממתינים לאישור Delivered נשלח Accepted התקבל Error שגיאה Rejected קשרים שנדחו %1 (Connected) %1 status, e.g. "Accepted" %1 (מחובר) Response: תגובה: Rename שנה שם Remove הסר ContactRequestDialog Someone new is asking to connect to you התקבלה בקשת תקשורת חדשה Reject דחיה Accept אשר ContactRequestFields ID: זהות: Name: שם: Message: הודעה: GeneralPreferences Use a single window for conversations השתמש בחלון בודד עבור כל השיחות Open links in default browser without prompting פתח קישורים בדפדפן ברירת המחדל ללא בקשת אישור Play audio notifications השמע התראה קולית Volume עוצמה Language שפה Restart Ricochet to apply changes הפעל מחדש את האפליקציה על מנת לעדכן את ההגדרות החדשות LanguagePreferences Select Language בחר שפה Restart Ricochet to apply changes הפעל מחדש את האפליקציה על מנת לעדכן את ההגדרות החדשות LanguagesModel System default ברירת מחדל המערכת Main Ricochet Error שגיאה ב Ricochet MainToolBar Add Contact הוסף איש קשר Preferences העדפות Click to add contacts הקלק להוספת אנשי קשר MainWindow Remove %1 הסר את %1 Do you want to permanently remove %1? האם אתה רוצה להסיר לצמיתות את %1 MessageDelegate %1 is offline %1 nickname %1 מנותק Copy ID העתק זהות Copy Link העתק לינק Open with Browser פתח בדפדפן Add as Contact הוסיף כאיש קשר Copy Message העתק הודעה Copy Selection העתק בחירה MessageDialogWrapper Remove %1 הסר את %1 Do you want to permanently remove %1? %1 nickname האם אתה רוצה להסיר לצמיתות את %1 This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. איש הקשר לא יהיה מסוגל לשלוח לך הודעות, הצד השני יקבל התראה על הסרת הקישור ביניכם. הם יוכלו לשלוח בקשת חיבור מחדש NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. החיבור של המחשב הזה הינו ללא הגבלה כל שהיא כגון רשת משרדית, שרתי פרוקסי, וכדומה ואני מעוניין להתחבר ישירות לרשת Tor. Connect התחבר This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. החיבור של מחשב זה לאינטרנט הינו מצונזר או עובר דרך פרוקסי ראשי ואני אהיה צריך להגדיר את הגדרות הרשת. Configure הגדר OfflineStateItem Configure הגדר Details פרטים Connection failed התחברות נכשלה Connecting… \u2026 is ellipsis מתחבר... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>זהירות!</b> פתיחת קישורים עם דפדפן הברירת מחדל שלך עלולה לסכן אותך ולחשוף פרטים עלייך!<br><br>תוכל במקום <a href=".">להעתיק את הקישור</a> ללוח. Don't ask again for links from %1 אין צורך לבקש אישור פתיחה נוסף מ %1 Don't ask again for any links (not recommended!) אין צורך לבקש אישור מאף איש קשר (לא מומלץ ומסוכן!) Open Browser פתח דפדפן Cancel בטל PreferencesDialog Ricochet Preferences הגדרות Ricochet General כללי Language שפה Contacts אנשי קשר Tor Tor About אודות QCocoaMenuItem Preference העדפות StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. תהליך Tor לא הופעל כראוי. ככל הנראה זאת תקלת מערכת או התקנה בעייתית. Quit יציאה / התנתק TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis מתחבר לרשת Tor Back אחורה Hide details הסתר פרטים Show details הראה פרטים Done סיום TorConfigurationPage Does this computer need a proxy to access the internet? האם מחשב זה משתמש בפרוקסי על מנת להתחבר לאינטרנט? Proxy type: סוג פרוקסי None ללא Address: כתובת: IP address or hostname כתובת IP או שם מחשב מארח Port: פורט: Username: שם משתמש: Optional אופציונלי Password: סיסמה: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? האם חיבור של מחשב זה עובר דרך חומת אש אשר מרשה שימוש בפורטים מסויימים? Allowed ports: פורטים מורשים: Example: 80,443 דוגמה: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. במידה וחיבור האינטרנט של מחשב זה מצונזר, יהיה עליך להגדיר שרתי גישור חיבור Enter one or more bridge relays (one per line): הכנס גשר חיבור (אחד לשורה): Back חזרה Connect התחברהתחבר TorPreferences Running: פעיל: Yes כן No לא External חיצוני Control connected: בקרה מחוברת: Circuits established: מעגלים מחוברים: Hidden service: שירות מוסתר: Online מחובר Offline מנותק Version: גירסה: Error: <b>%1</b> %1 is error message שגיאה: <b>%1</b> Configure הגדר TorStateWidget Connection failed התחברות נכשלה Connecting… \u2026 is ellipsis מתחבר... Connecting… (%1%) %1 is progress percentage, e.g. 100 מתחבר... (%1) Online מקוון Connected מחובר ricochet-1.1.4/translation/ricochet_it.ts000066400000000000000000000667011300720305500205240ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Condividi il tuo Ricochet ID per permettere le richieste di connessione Cancel Annulla Add Aggiungere ContactActions Open Window Apri Finestra Details... Dettagli... Rename Rinominare Remove Rimuovere ContactIDField <b>%1</b> is already your contact <b>%1</b> è già un tuo contatto You can't add yourself as a contact Non puoi aggiungere te stesso come contatto Enter an ID starting with <b>ricochet:</b> Inserisci un ID che inizi con <b>ricochet:</b> Copied to clipboard Copiato negli appunti Copy Copia ContactList Online Connesso Offline Disconnesso Requests Richieste Rejected Rifiutato Outdated ContactPreferences Date added: Data di inserimento: Last seen: Ultimo accesso: Request: Richiesta: Pending connection Connessione in sospeso Delivered Consegnato Accepted Accettato Error Errore Rejected Rifiutato %1 (Connected) %1 status, e.g. "Accepted" %1 (Collegato) Response: Risposta: Rename Rinomina Remove Rimuovere ContactRequestDialog Someone new is asking to connect to you Qualcuno di nuovo sta chiedendo di connettersi a te Reject Rifiuta Accept Accetta ContactRequestFields ID: ID: Name: Nome: Message: Messaggio: GeneralPreferences Use a single window for conversations Usa una finestra singola per le conversazioni Open links in default browser without prompting Apri link nel browser di default senza chiedere Play audio notifications Volume Volume Language Lingua Restart Ricochet to apply changes Riavvia Ricochet per applicare le modifiche LanguagePreferences Select Language Selezione lingua Restart Ricochet to apply changes Riavvia Ricochet per applicare le modifiche LanguagesModel System default Main Ricochet Error Errore Ricochet MainToolBar Add Contact Aggiungere Contatto Preferences Preferenze Click to add contacts Clicca per aggiungere contatti MainWindow Remove %1 Rimuovere %1 Do you want to permanently remove %1? Vuoi rimuovere perennemente %1? MessageDelegate %1 is offline %1 nickname %1 è sconnesso Copy ID Copia ID Copy Link Copia Link Open with Browser Apri con il Browser Add as Contact Aggiungi come Contatto Copy Message Copia messaggio Copy Selection Copia selezione MessageDialogWrapper Remove %1 Rimuovere %1 Do you want to permanently remove %1? %1 nickname Vuoi rimuovere perennemente %1? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Questo contatto non sarà più in grado di inviarti messaggi e sarà notificato di questo. Potrebbe decidere di mandare una nuova richiesta. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. La connessione a internet di questo computer è libera da ostacoli. Vorrei connettermi direttamente alla rete Tor. Connect Collegare This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. La connessione a internet di questo computer è soggetta a censure, filtri o proxy. Devo configurare le impostazioni del network. Configure Configurare OfflineStateItem Configure Configurare Details Dettagli Connection failed Connessione fallita Connecting… \u2026 is ellipsis Collegamento... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Attenzione!</b> Aprire link con il tuo browser di default danneggerà la tua sicurezza e anonimia.<br><br>Puoi <a href='.'>copiare negli appunti</a> invece. Don't ask again for links from %1 Non chiedere ancora per link da %1 Don't ask again for any links (not recommended!) Non chiedere più per ogni link (non raccomandato) Open Browser Apri Browser Cancel Annulla PreferencesDialog Ricochet Preferences Preferenze Ricochet General Generali Language Lingua Contacts Contatti Tor Tor About Riguardo a QCocoaMenuItem Preference Preferenza StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Tor non è stato avviato correttamente. Probabilmente si tratta di un errore di sistema o di installazione. Quit Esci TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Collegamento al network Tor... Back Indietro Hide details Nascondi dettagli Show details Mostra dettagli Done Fatto TorConfigurationPage Does this computer need a proxy to access the internet? Questo computer necessita di un proxy per accedere a internet? Proxy type: Tipo di proxy: None Nessuno Address: Indirizzo: IP address or hostname Indirizzo IP o nome host Port: Porta: Username: Nome utente: Optional Opzionale Password: Password: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? La connessione di questo computer passa attraverso un firewall che permette la connessione solo ad alcune porte? Allowed ports: Porte permesse: Example: 80,443 Esempio: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Se la connessione a internet di questo computer è censurata dovrai ottenere ed usare dei bridge relay Enter one or more bridge relays (one per line): Inserisci uno o più bridge relay (uno per linea): Back Indietro Connect Collegare TorPreferences Running: In esecuzione: Yes Si No No External Esterno Control connected: Controllo connesso: Circuits established: Circuiti stabiliti: Hidden service: Hidden service: Online Connesso Offline Disconnesso Version: Versione: Error: <b>%1</b> %1 is error message Errore: <b>%1</b> Configure Configurare TorStateWidget Connection failed Connessione fallita Connecting… \u2026 is ellipsis Collegamento... Connecting… (%1%) %1 is progress percentage, e.g. 100 Collegamento... (%1%) Online Connesso Connected Connesso ricochet-1.1.4/translation/ricochet_it_IT.ts000066400000000000000000000670401300720305500211150ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Condividi il tuo Ricochet ID per permettere agli altri utenti di inviarti richieste di connessione Cancel Annulla Add Aggiungi ContactActions Open Window Apri finestra Details... Dettagli... Rename Rinomina Remove Rimuovi ContactIDField <b>%1</b> is already your contact <b>%1</b> è già tra i tuoi contatti You can't add yourself as a contact Non puoi aggiungere te stesso ai tuoi contatti Enter an ID starting with <b>ricochet:</b> Inserisci un ID che inizi con <b>ricochet:</b> Copied to clipboard Copiato negli appunti Copy Copia ContactList Online Connesso Offline Disconnesso Requests Richieste Rejected Rifiutato Outdated Non aggiornato ContactPreferences Date added: Aggiunto il: Last seen: Ultimo accesso: Request: Richiesta: Pending connection Connessione in attesa Delivered Consegnato Accepted Accettato Error Errore Rejected Rifiutato %1 (Connected) %1 status, e.g. "Accepted" %1 (In linea) Response: Risposta: Rename Rinomina Remove Rimuovi ContactRequestDialog Someone new is asking to connect to you Un nuovo utente vuole connettersi con te Reject Rifiuta Accept Accetta ContactRequestFields ID: ID: Name: Nome: Message: Messaggio: GeneralPreferences Use a single window for conversations Usa una finestra singola per le conversazioni Open links in default browser without prompting Apri i link nel browser di default senza chiedere Play audio notifications Riproduci suono notifiche Volume Volume Language Lingua Restart Ricochet to apply changes Riavvia Ricochet per applicare le modifiche LanguagePreferences Select Language Seleziona lingua Restart Ricochet to apply changes Riavvia Ricochet per applicare le modifiche LanguagesModel System default Impostazioni di fabbrica Main Ricochet Error Errore Ricochet MainToolBar Add Contact Aggiungi contatto Preferences Preferenze Click to add contacts Clicca per aggiungere contatti MainWindow Remove %1 Rimuovi %1 Do you want to permanently remove %1? Vuoi rimuovere definitivamente %1? MessageDelegate %1 is offline %1 nickname %1 non è in linea Copy ID Copia ID Copy Link Copia link Open with Browser Apri nel il browser Add as Contact Aggiungi ai tuoi contatti Copy Message Copia messaggio Copy Selection Copia selezione MessageDialogWrapper Remove %1 Rimuovi %1 Do you want to permanently remove %1? %1 nickname Vuoi rimuovere %1 definitivamente? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Questo contatto non potrà più inviarti messaggi. Riceverà un avviso per informarlo di questo. Potrebbe comunque inviarti una nuova richiesta di inserimento fra i contatti. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. La connessione a Internet di questo computer è libera da ostacoli. Voglio connettermi direttamente alla rete Tor. Connect Connetti This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. La connessione a Internet di questo computer è soggetta a censure, filtri o proxy. Devo configurare le impostazioni di rete. Configure Configura OfflineStateItem Configure Configura Details Dettagli Connection failed Connessione fallita Connecting… \u2026 is ellipsis Connessione in corso... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Attenzione!</b> Aprire un link nel browser di default metterà a repentaglio la tua sicurezza e anonimia.<br><br>Puoi invece <a href='.'>copiarlo negli appunti</a>. Don't ask again for links from %1 Non chiedere più per i link inviati da %1 Don't ask again for any links (not recommended!) Non chiedere più per ogni link (scelta non consigliata) Open Browser Apri browser Cancel Annulla PreferencesDialog Ricochet Preferences Preferenze Ricochet General Generale Language Lingua Contacts Contatti Tor Tor About Info QCocoaMenuItem Preference Preferenza StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Tor non si è avviato correttamente. Potrebbe trattarsi di un errore di sistema o di installazione. Quit Esci TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Collegamento alla rete Tor... Back Indietro Hide details Nascondi dettagli Show details Mostra dettagli Done Fatto TorConfigurationPage Does this computer need a proxy to access the internet? Questo computer necessita di un proxy per accedere a internet? Proxy type: Tipo di proxy: None Nessuno Address: Indirizzo: IP address or hostname Indirizzo IP o nome host Port: Porta: Username: Nome utente: Optional Facoltativo Password: Password: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? La connessione ad Internet di questo computer passa attraverso un firewall che consente la connessione solo su determinate porte? Allowed ports: Porte consentite: Example: 80,443 Esempio: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Se la connessione a Internet di questo computer è censurata dovrai ottenere ed usare dei bridge relay Enter one or more bridge relays (one per line): Aggiungi uno o più bridge relay (uno per linea): Back Indietro Connect Connetti TorPreferences Running: In esecuzione: Yes Si No No External Esterno Control connected: Control connesso: Circuits established: Circuiti stabiliti: Hidden service: Hidden service: Online Connesso Offline Disconnesso Version: Versione: Error: <b>%1</b> %1 is error message Errore: <b>%1</b> Configure Configura TorStateWidget Connection failed Connessione fallita Connecting… \u2026 is ellipsis Connessione in corso... Connecting… (%1%) %1 is progress percentage, e.g. 100 Connessione in corso... (%1%) Online Connesso Connected Connesso ricochet-1.1.4/translation/ricochet_nb.ts000066400000000000000000000665711300720305500205140ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Del din Ricochet-ID for å tillate tilkoblingsforespørsler Cancel Avbryt Add Legg til ContactActions Open Window Åpne vindu Details... Detaljer... Rename Endre navn Remove Fjern ContactIDField <b>%1</b> is already your contact <b>%1</b> er allerede din kontakt You can't add yourself as a contact Du kan ikke legge til deg selv som en kontakt Enter an ID starting with <b>ricochet:</b> Skriv inn en ID som starter med <b>ricochet:</b> Copied to clipboard Kopiert til utklippstavlen Copy Kopier ContactList Online Tilkoblet Offline Frakoblet Requests Forespørsler Rejected Avvist Outdated Utdatert ContactPreferences Date added: Dato lagt til: Last seen: Sist sett: Request: Forespørsel: Pending connection I påvente av forbindelse Delivered Levert Accepted Godtatt Error Feil Rejected Avvist %1 (Connected) %1 status, e.g. "Accepted" %1 (tilkoblet) Response: Respons: Rename Endre navn Remove Fjern ContactRequestDialog Someone new is asking to connect to you Noen nye spør om de kan å få koble til Reject Avvist Accept Godta ContactRequestFields ID: ID: Name: Navn: Message: Melding: GeneralPreferences Use a single window for conversations Bruk ètt vindu for samtaler Open links in default browser without prompting Åpne lenker i standardnettleser uten å spørre Play audio notifications Spill av lydvarsler Volume Volum Language Språk Restart Ricochet to apply changes Ta en omstart av Ricochet for å bruke endringene LanguagePreferences Select Language Velg språk Restart Ricochet to apply changes Ta en omstart av Ricochet for å bruke endringene LanguagesModel System default Systemstandard Main Ricochet Error Ricochet-feil MainToolBar Add Contact Legg til kontakt Preferences Preferanser Click to add contacts Trykk for å legge til kontakter MainWindow Remove %1 Fjern %1 Do you want to permanently remove %1? Ønsker du å slette %1 for alltid? MessageDelegate %1 is offline %1 nickname %1 er frakoblet Copy ID Kopier ID Copy Link Kopier lenke Open with Browser Åpne i nettleser Add as Contact Legg til som kontakt Copy Message Kopier melding Copy Selection Kopier utvalg MessageDialogWrapper Remove %1 Fjern %1 Do you want to permanently remove %1? %1 nickname Ønsker du å permanent fjerne %1? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Denne kontakten vil ikke lengre være i stand til å kunne kontakte deg og er blitt varslet om dette. Vedkommende kan velge å sende ny tilkoblingsforespørsel. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. Denne datamaskinens internettforbindelse er fri for hindre. Jeg ønsker å koble direkte til Tor-nettverket. Connect Koble til This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. Denne datamaskinens internettforbindelse er sensurert, filtrert, eller bak proxy. Jeg trenger å konfigurere nettverksinnstillinger. Configure Konfigurer OfflineStateItem Configure Konfigurer Details Detaljer Connection failed Tilkoblingsfeil Connecting… \u2026 is ellipsis Kobler til... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Advarsel!</b> Åpning av lenker i din standardnettleser vil svekke sikkerheten og minske anonymiteten din.<br><br>Du kan <a href='.'>kopiere til utklippstavlen</a> i stedet for. Don't ask again for links from %1 Ikke spør igjen for lenker fra %1 Don't ask again for any links (not recommended!) Ikke spør igjen for noen lenker som helst (ikke anbefalt!) Open Browser Åpne nettleser Cancel Avbryt PreferencesDialog Ricochet Preferences Ricochet-preferanser General Generelt Language Språk Contacts Kontakter Tor Tor About Om QCocoaMenuItem Preference Preferanse StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Tor-prosessen ble ikke startet vellykket. Dette er mest sannsynlig en installasjons- eller systemfeil. Quit Avslutt TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Kobler til Tor-nettverket Back Tilbake Hide details Gjem detaljer Show details Vis detaljer Done Ferdig TorConfigurationPage Does this computer need a proxy to access the internet? Trenger denne datamaskinen proxy for å få tilgang til Internett? Proxy type: Proxytype: None Ingen Address: Adresse: IP address or hostname IP-adresse eller vertsnavn Port: Port: Username: Brukernavn: Optional Valgfri Password: Passord: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? Går denne datamaskinens internettforbindelse gjennom en brannmur som kun tillater forbindelser til bestemte porter? Allowed ports: Tillatte porter: Example: 80,443 Eksempel: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Hvis denne datamaskinens internettforbindelse er sensurert må du få tak i og bruke broreléer. Enter one or more bridge relays (one per line): Skriv inn et eller flere broreléer (et relé per linje): Back Tilbake Connect Koble til TorPreferences Running: Kjører: Yes Ja No Nei External Ekstern Control connected: Kontroll tilkoblet: Circuits established: Etablerte kretser: Hidden service: Skjult tjeneste: Online Pålogget Offline Frakoblet Version: Versjon: Error: <b>%1</b> %1 is error message Feil: <b>%1</b> Configure Konfigurer TorStateWidget Connection failed Tilkoblingsfeil Connecting… \u2026 is ellipsis Kobler til... Connecting… (%1%) %1 is progress percentage, e.g. 100 Kobler til... (%1%) Online Pålogget Connected Tilkoblet ricochet-1.1.4/translation/ricochet_nl_NL.ts000066400000000000000000000666311300720305500211140ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Deel je Ricochet ID om verbindingsverzoeken mogelijk te maken Cancel Annuleren Add Toevoegen ContactActions Open Window Open venster Details... Details... Rename Hernoemen Remove Verwijderen ContactIDField <b>%1</b> is already your contact <b>%1</b> is al een contact You can't add yourself as a contact Je kunt jezelf niet als contact toevoegen Enter an ID starting with <b>ricochet:</b> Geef een ID op dat begint met <b>ricochet:</b> Copied to clipboard Gekopieerd naar klembord Copy Kopieer ContactList Online Online Offline Offline Requests Aanvragen Rejected Afgewezen Outdated Verouderd ContactPreferences Date added: Datum toegevoegd: Last seen: Laatst gezien: Request: Aanvraag: Pending connection Verbinding wachtend Delivered Afgeleverd Accepted Geaccepteerd Error Fout Rejected Afgewezen %1 (Connected) %1 status, e.g. "Accepted" %1 (verbonden) Response: Antwoord: Rename Hernoemen Remove Verwijder ContactRequestDialog Someone new is asking to connect to you Een nieuwe contactpersoon wil met je verbinden Reject Afwijzen Accept Accepteren ContactRequestFields ID: ID: Name: Naam: Message: Bericht: GeneralPreferences Use a single window for conversations Gebruik een enkel venster voor gesprekken Open links in default browser without prompting Open links in standaardbrowser zonder te vragen Play audio notifications Afspelen audio meldingen Volume Volume Language Taal Restart Ricochet to apply changes Herstart Ricochet om wijzigingen door te voeren LanguagePreferences Select Language Kies taal Restart Ricochet to apply changes Herstart Ricochet om wijzigingen door te voeren LanguagesModel System default Systeemstandaard Main Ricochet Error Ricochet fout MainToolBar Add Contact Toevoegen contact Preferences Voorkeuren Click to add contacts Klikken om contacten toe te voegen MainWindow Remove %1 Verwijder %1 Do you want to permanently remove %1? Wil je %1 permanent verwijderen? MessageDelegate %1 is offline %1 nickname %1 is offline Copy ID Kopieer ID Copy Link Kopieer link Open with Browser Open met browser Add as Contact Toevoegen als contact Copy Message Kopiëren bericht Copy Selection Kopiëren selectie MessageDialogWrapper Remove %1 Verwijder %1 Do you want to permanently remove %1? %1 nickname Wil je %1 permanent verwijderen? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Deze contactpersoon kan je niet langer berichten sturen en wordt geïnformeerd over het verwijderen. Hij/zij kan een nieuwe aanvraag doen. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. De internetverbinding van deze computer kent geen belemmeringen. Ik wil rechtstreeks verbinden met het TOR netwerk. Connect Verbinden This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. De internetverbinding van deze computer is gecensureerd, gefilterd of geproxied. Ik moet de netwerkinstellingen configureren. Configure Configureren OfflineStateItem Configure Configureren Details Details Connection failed Verbinding mislukt Connecting… \u2026 is ellipsis Verbinden... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Waarschuwing!</b> Het openen van links met je standaard browser schaadt je beveliging en anonimiteit.<br><br>Je kunt in plaats daarvan <a href='.'>kopiëren maar het klembord</a>. Don't ask again for links from %1 Vraag niet opnieuw om links van %1 Don't ask again for any links (not recommended!) Vraag niet meer naar links (niet aanbevolen!) Open Browser Open Browser Cancel Annuleren PreferencesDialog Ricochet Preferences Ricochet voorkeuren General Algemeen Language Taal Contacts Contacten Tor Tor About Over QCocoaMenuItem Preference Voorkeur StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Het TOR proces is niet succesvol gestart. Dit komt waarschijnlijk door een installatie- of systeemfout. Quit Afsluiten TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Verbinden met het TOR netwerk... Back Terug Hide details Verberg details Show details Toon details Done Gedaan TorConfigurationPage Does this computer need a proxy to access the internet? Heeft deze computer een proxy nodig om te verbinden met het internet? Proxy type: Proxy type: None Geen Address: Adres: IP address or hostname IP adres of servernaam Port: Poort: Username: Gebruikersnaam: Optional Optioneel Password: Wachtwoord: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? Gaat de internetverbinding van deze computer door een firewall die alleen verbindingen naar bepaalde poorten toestaat? Allowed ports: Toegestane poorten: Example: 80,443 Voorbeeld: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Als de internetverbinding van deze computer is gecensureerd, moet je bridge relays vinden en gebruiken. Enter one or more bridge relays (one per line): Geen een of meer bridge relays op (een per regel): Back Terug Connect Verbinden TorPreferences Running: Draaiend: Yes Ja No Nee External Extern Control connected: Control verbonden: Circuits established: Circuits aangelegd: Hidden service: Verborgen service: Online Online Offline Offline Version: Versie: Error: <b>%1</b> %1 is error message Fout: <b>%1</b> Configure Configureren TorStateWidget Connection failed Verbinding mislukt Connecting… \u2026 is ellipsis Verbinden... Connecting… (%1%) %1 is progress percentage, e.g. 100 Verbinden… (%1%) Online Online Connected Verbonden ricochet-1.1.4/translation/ricochet_pl.ts000066400000000000000000000673021300720305500205210ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Udostępnij swój identyfikator Ricochet, aby umożliwić łączenie się z Tobą. Cancel Anuluj Add Dodaj ContactActions Open Window Otwórz okno Details... Szczegóły... Rename Zmień nazwę Remove Usuń ContactIDField <b>%1</b> is already your contact <b>%1</b> jest już w Twoich kontaktach You can't add yourself as a contact Nie możesz dodać siebie do kontaktów Enter an ID starting with <b>ricochet:</b> Wpisz ID zaczynające się od <b>ricochet:</b> Copied to clipboard Skopiowano do schowka Copy Kopiuj ContactList Online Połączeni Offline Rozłączeni Requests Prośby o dodanie Rejected Odrzucone Outdated Przeterminowane ContactPreferences Date added: Dołączył: Last seen: Ostatnio widziany: Request: Prośba o dodanie: Pending connection Oczekuje na połączenie Delivered Dostarczony Accepted Zaakceptowany Error Błąd Rejected Odrzucony %1 (Connected) %1 status, e.g. "Accepted" %1 (Połączony) Response: Odpowiedź: Rename Zmień nazwę Remove Usuń ContactRequestDialog Someone new is asking to connect to you Ktoś chce się z Tobą skontaktować Reject Odrzuć Accept Akceptuj ContactRequestFields ID: ID: Name: Nazwa: Message: Wiadomość: GeneralPreferences Use a single window for conversations Używaj pojedynczego okno do rozmów Open links in default browser without prompting Otwórz linki w domyślnej przeglądarce bez pytania Play audio notifications Włącz powiadomienia dźwiękowe Volume Głośność Language Język Restart Ricochet to apply changes Aby zaakceptować zmiany, uruchom Ricochet ponownie LanguagePreferences Select Language Wybierz język Restart Ricochet to apply changes Aby zaakceptować zmiany, uruchom Ricochet ponownie LanguagesModel System default Domyślny w systemie Main Ricochet Error Błąd programu Ricochet MainToolBar Add Contact Dodaj kontakt Preferences Ustawienia Click to add contacts Kliknij, aby dodać do kontaktów MainWindow Remove %1 Usuń %1 Do you want to permanently remove %1? Czy chcesz na stałe usunąć %1? MessageDelegate %1 is offline %1 nickname %1 jest rozłączony Copy ID Skopiuj ID Copy Link Skopiuj link Open with Browser Otwórz w przeglądarce Add as Contact Dodaj jako kontakt Copy Message Kopiuj wiadomość Copy Selection Kopiuj zaznaczenie MessageDialogWrapper Remove %1 Usuń %1 Do you want to permanently remove %1? %1 nickname Czy chcesz na stałe usunąć %1? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Ten kontakt nie będzie mógł więcej wysyłać do Ciebie wiadomości i zostanie poinformowany o usunięciu. Może wysłać Ci nową prośbę o połączenie. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. Połączenie internetowe tego kompuera jest wolne od przeszkód. Chcę połączyć się bezpośrednio z siecią Tor. Connect Połącz This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. Połączenie internetowe tego komputera jest cenzurowane, filtrowane lub trasowane przez serwer proxy. Muszę skonfigurować ustawienia sieciowe. Configure Konfiguruj OfflineStateItem Configure Konfiguruj Details Szczegóły Connection failed Połączenie nie powiodło się Connecting… \u2026 is ellipsis Łączenie… OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Uwaga!</b> Otwieranie linków za pomocą domyślnej przeglądarki spowoduje utratę bezpieczeństwa i anonimowości. Zamiast tego możesz <a href='.'>skopiować go do schowka</a> Don't ask again for links from %1 Nie pytaj ponownie dla linków od %1 Don't ask again for any links (not recommended!) Nigdy nie pytaj ponownie (niezalecane!) Open Browser Otwórz przeglądarkę Cancel Anuluj PreferencesDialog Ricochet Preferences Ustawienia Ricocheta General Główne Language Język Contacts Kontakty Tor Tor About O programie QCocoaMenuItem Preference Ustawienie StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Tor nie został uruchomiony poprawnie. Najprawdopodobniej oznacza to błąd instalacji lub systemu. Quit Wyjdź TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Łączenie z siecią Tor… Back Wróć Hide details Ukryj szczegóły Show details Pokaż szczegóły Done Zrobione TorConfigurationPage Does this computer need a proxy to access the internet? Czy ten komputer potrzebuje serwera proxy, aby łączyć się z Internetem? Proxy type: Typ proxy: None Brak Address: Adres: IP address or hostname Adres IP lub nazwa hosta Port: Port: Username: Nazwa użytkownika: Optional Opcjonalny Password: Hasło: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? Czy połączenie internetowe tego komputera jest kierowane przez firewall, który pozwala na łączenie się tylko z wybranymi portami? Allowed ports: Dozwolone porty: Example: 80,443 Na przykład: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Jeżeli połączenie internetowe tego komputera jest cenzurowane, będziesz musiał uzyskać dostęp do węzła pośredniego (<i>bridge relay</i>). Enter one or more bridge relays (one per line): Podaj jeden lub więcej węzłów pośrednich (jeden w linii): Back Wróć Connect Połącz TorPreferences Running: Uruchomione: Yes Tak No Nie External Zewnętrzny Control connected: Połączony z portem kontrolnym: Circuits established: Obwody utworzone: Hidden service: Ukryte usługi: Online Połączony Offline Rozłączony Version: Wersja: Error: <b>%1</b> %1 is error message Błąd: <b>%1</b> Configure Konfiguruj TorStateWidget Connection failed Połączenie nie powiodło się Connecting… \u2026 is ellipsis Łączenie… Connecting… (%1%) %1 is progress percentage, e.g. 100 Łączenie… (%1%) Online Połączony Connected Połączony ricochet-1.1.4/translation/ricochet_pt_BR.ts000066400000000000000000000667051300720305500211220ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Compartilhe seu ID Ricochet para permitir pedidos de conexão Cancel Cancelar Add Adicionar ContactActions Open Window Abrir Janela Details... Detalhes... Rename Renomear Remove Remover ContactIDField <b>%1</b> is already your contact <b>%1</b> já é seu contato You can't add yourself as a contact Você não pode adicionar a si mesmo como um contato Enter an ID starting with <b>ricochet:</b> Entre uma ID começando com <b>ricochet:</b> Copied to clipboard Copiado para a área de transferência Copy Copiar ContactList Online Online Offline Offline Requests Pedidos Rejected Rejeitado Outdated Desatualizado ContactPreferences Date added: Adicionado em: Last seen: Visto pela última vez: Request: Pedido: Pending connection Conexão pendente Delivered Entregue Accepted Aceito Error Erro Rejected Rejeitado %1 (Connected) %1 status, e.g. "Accepted" %1 (Conectado) Response: Resposta: Rename Renomear Remove Remover ContactRequestDialog Someone new is asking to connect to you Alguém novo está pedindo para se conectar com você Reject Rejeitar Accept Aceitar ContactRequestFields ID: ID: Name: Nome: Message: Mensagem: GeneralPreferences Use a single window for conversations Usar somente uma janela para conversas Open links in default browser without prompting Abrir links no navegador padrão sem pedir confirmação Play audio notifications Tocar notificações de áudio Volume Volume Language Idioma Restart Ricochet to apply changes Reiniciar Ricochet para aplicar mudanças LanguagePreferences Select Language Selecionar Linguagem Restart Ricochet to apply changes Reiniciar Ricochet para aplicar mudanças LanguagesModel System default Padrão do sistema Main Ricochet Error Erro do Ricochet MainToolBar Add Contact Adicionar Contato Preferences Preferências Click to add contacts Clique para adicionar contatos MainWindow Remove %1 Remover %1 Do you want to permanently remove %1? Você deseja remover %1 permanentemente? MessageDelegate %1 is offline %1 nickname %1 está offline Copy ID Copiar ID Copy Link Copiar Link Open with Browser Abrir no Navegador Add as Contact Adicionar como Contato Copy Message Copiar Mensagem Copy Selection Copiar Seleção MessageDialogWrapper Remove %1 Remover %1 Do you want to permanently remove %1? %1 nickname Você deseja remover %1 permanentemente? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Este contato não poderá mais te enviar mensagens, e será notificado sobre a remoção. Ele pode escolher enviar novo pedido de conexão. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. A conexão à internet deste computador está livre de obstáculos. Quero me conectar diretamente à rede Tor. Connect Conectar This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. A conexão à internet deste computador é censurada, filtrada, ou passa por proxy. Preciso configurar a rede. Configure Configurar OfflineStateItem Configure Configurar Details Detalhes Connection failed A conexão falhou Connecting… \u2026 is ellipsis Conectando... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Atenção!</b> Abrir links com o seu navegador pode danificar sua segurança e anonimidade.<br><br>Ao invés disso, você pode <a href='.'>copiar para a área de transferência</a>. Don't ask again for links from %1 Não perguntar de novo para links de %1 Don't ask again for any links (not recommended!) Não perguntar de novo para quaisquer links (não recomendado!) Open Browser Abrir Navegador Cancel Cancelar PreferencesDialog Ricochet Preferences Preferências do Ricochet General Geral Language Idioma Contacts Contatos Tor Tor About Sobre QCocoaMenuItem Preference Preferência StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. O processo do Tor não foi iniciado com sucesso. Muito provavelmente isso é um erro de instalação ou de sistema. Quit Sair TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Conectando à rede Tor... Back Voltar Hide details Esconder detalhes Show details Mostrar detalhes Done Feito TorConfigurationPage Does this computer need a proxy to access the internet? Este computador precisa de um proxy para acessar a internet? Proxy type: Tipo de proxy: None Nenhum Address: Endereço: IP address or hostname Endereço IP ou nome do host Port: Porta: Username: Usuário: Optional Opcional Password: Senha: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? A conexão à internet deste computador passa por um firewall que só permite conexões a certas portas? Allowed ports: Portas permitidas: Example: 80,443 Exemplo: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Se a conexão à internet deste computador é censurada, você precisará obter e usar bridge relays. Enter one or more bridge relays (one per line): Entre um ou mais bridge relays (um por linha): Back Voltar Connect Conectar TorPreferences Running: Executando: Yes Sim No Não External Externo Control connected: Controle conectado: Circuits established: Circuitos estabelecidos: Hidden service: Serviço escondido: Online Online Offline Offline Version: Versão: Error: <b>%1</b> %1 is error message Erro: <b>%1</b> Configure Configurar TorStateWidget Connection failed A conexão falhou Connecting… \u2026 is ellipsis Conectando... Connecting… (%1%) %1 is progress percentage, e.g. 100 Conectando... (%1) Online Online Connected Conectado ricochet-1.1.4/translation/ricochet_pt_PT.ts000066400000000000000000000670121300720305500211320ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Partilhe o seu ID Ricochet para permitir pedidos de ligação Cancel Cancelar Add Adicionar ContactActions Open Window Abrir Janela Details... Detalhes... Rename Mudar o nome Remove Remover ContactIDField <b>%1</b> is already your contact <b>%1</b> já é um contacto seu You can't add yourself as a contact Não pode adicionar-se a si mesmo como contacto Enter an ID starting with <b>ricochet:</b> Insira um ID que comece com <b>ricochet:</b> Copied to clipboard Copiado para a área de transferência Copy Copiar ContactList Online Online Offline Offline Requests Pedidos Rejected Rejeitados Outdated Desatualizado ContactPreferences Date added: Adicionado a: Last seen: Visto pela última vez: Request: Pedido: Pending connection Ligação pendente Delivered Enviado Accepted Aceite Error Erro Rejected Rejeitado %1 (Connected) %1 status, e.g. "Accepted" %1 (Ligado) Response: Resposta: Rename Mudar o nome Remove Remover ContactRequestDialog Someone new is asking to connect to you Alguém novo está a pedir para se ligar a si Reject Rejeitar Accept Aceitar ContactRequestFields ID: ID: Name: Nome: Message: Mensagem: GeneralPreferences Use a single window for conversations Utilizar uma única janela para conversas Open links in default browser without prompting Abrir links no navegador padrão sem pedir confirmação Play audio notifications Ativar notificações de áudio Volume Volume Language Idioma Restart Ricochet to apply changes Reiniciar o Ricochet para aplicar as alterações LanguagePreferences Select Language Selecionar Idioma Restart Ricochet to apply changes Reiniciar o Ricochet para aplicar as alterações LanguagesModel System default Padrão do sistema Main Ricochet Error Erro do Ricochet MainToolBar Add Contact Adicionar Contacto Preferences Preferências Click to add contacts Clique para adicionar contactos MainWindow Remove %1 Remover %1 Do you want to permanently remove %1? Deseja remover %1 permanentemente? MessageDelegate %1 is offline %1 nickname %1 está offline Copy ID Copiar ID Copy Link Copiar Link Open with Browser Abrir com o Navegador Add as Contact Adicionar como Contacto Copy Message Copiar Mensagem Copy Selection Copiar Seleção MessageDialogWrapper Remove %1 Remover %1 Do you want to permanently remove %1? %1 nickname Deseja remover %1 permanentemente? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Este contacto deixará de lhe poder enviar mensagens, e será ainda notificado sobre a remoção. No entanto, este poderá enviar um novo pedido de ligação. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. A ligação à Internet deste computador está livre de obstáculos. Pretendo ligar-me diretamente à rede Tor. Connect Ligar This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. A ligação à Internet deste computador é censurada, filtrada, ou passa por um proxy. Necessito de configurar as definições da rede. Configure Configurar OfflineStateItem Configure Configurar Details Detalhes Connection failed Ligação falhou Connecting… \u2026 is ellipsis Ligando... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Atenção!</b> Abrir links com o seu navegador padrão irá diminuir a sua segurança e anonimidade.<br><br>Em vez disso, poderá <a href='.'>copiar para a área de transferência</a>. Don't ask again for links from %1 Não perguntar de novo para links de %1 Don't ask again for any links (not recommended!) Não perguntar de novo para quaisquer links (não recomendado!) Open Browser Abrir Navegador Cancel Cancelar PreferencesDialog Ricochet Preferences Preferências do Ricochet General Geral Language Idioma Contacts Contactos Tor Tor About Sobre QCocoaMenuItem Preference Preferência StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. O processo do Tor não foi iniciado com sucesso. Tal deve-se, provavelmente, a um erro de instalação ou de sistema. Quit Sair TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis A ligar à rede Tor... Back Voltar Hide details Esconder detalhes Show details Mostrar detalhes Done Terminado TorConfigurationPage Does this computer need a proxy to access the internet? Este computador necessita de um proxy para aceder à internet? Proxy type: Tipo de proxy: None Nenhum Address: Endereço: IP address or hostname Endereço de IP ou nome do host Port: Porta: Username: Nome de utilizador: Optional Opcional Password: Palavra-passe: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? A ligação à Internet deste computador passa por uma firewall que apenas permite ligações por certas portas? Allowed ports: Portas permitidas: Example: 80,443 Exemplo: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Se a ligação à Internet deste computador é censurada, necessitará de obter e utilizar bridge relays. Enter one or more bridge relays (one per line): Insira um ou mais bridge relays (um por linha): Back Voltar Connect Ligar TorPreferences Running: A correr: Yes Sim No Não External Externo Control connected: Controlo ligado: Circuits established: Circuitos estabelecidos: Hidden service: Serviço oculto: Online Online Offline Offline Version: Versão: Error: <b>%1</b> %1 is error message Erro: <b>%1</b> Configure Configurar TorStateWidget Connection failed Ligação falhou Connecting… \u2026 is ellipsis Ligando... Connecting… (%1%) %1 is progress percentage, e.g. 100 Ligando... (%1%) Online Online Connected Ligado ricochet-1.1.4/translation/ricochet_ru.ts000066400000000000000000000717111300720305500205330ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Сообщите свой ID, чтобы собеседник узнал Вас Cancel Отмена Add Добавить ContactActions Open Window Открыть окно Details... Подробнее... Rename Переименовать Remove Удалить ContactIDField <b>%1</b> is already your contact <b>%1</b> уже в Вашем списке контактов You can't add yourself as a contact Вы не можете добавить себя в свой же список контактов Enter an ID starting with <b>ricochet:</b> Введите ID, начинающийся с <b>ricochet:</b> Copied to clipboard Скопировано в буфер Copy Копировать ContactList Online В сети Offline Не в сети Requests Запросы Rejected Отказано Outdated ContactPreferences Date added: Добавлен: Last seen: Последний раз замечен: Request: Запрос: Pending connection Ожидаем соединение Delivered Доставлено Accepted Принято Error Ошибка Rejected Отказано %1 (Connected) %1 status, e.g. "Accepted" %1 (Соединено) Response: Ответ: Rename Переименовать Remove Удалить ContactRequestDialog Someone new is asking to connect to you Новый человек хочет пообщаться с Вами Reject Отказать Accept Принять ContactRequestFields ID: ID: Name: Имя: Message: Сообщение: GeneralPreferences Use a single window for conversations Использовать одно окно для разговоров Open links in default browser without prompting Открывать ссылки в браузере по умолчанию без запроса Play audio notifications Volume Language Restart Ricochet to apply changes LanguagesModel System default Main Ricochet Error Ошибка Ricochet MainToolBar Add Contact Добавить контакт Preferences Настройки Click to add contacts Нажмите, чтобы добавить контакты MainWindow Remove %1 Удалить %1 Do you want to permanently remove %1? Вы действительно хотите удалить %1 навсегда? MessageDelegate %1 is offline %1 nickname %1 не в сети Copy ID Копировать ID Copy Link Копировать ссылку Open with Browser Открыть в браузере Add as Contact Добавить в список контактов Copy Message Copy Selection MessageDialogWrapper Remove %1 Удалить %1 Do you want to permanently remove %1? %1 nickname Вы действительно хотите удалить %1 навсегда? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Этот контакт больше не сможет писать Вам и будет уведомлен об удалении. Тем не менее, он сможет послать Вам новый запрос. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. Соединение на этом компьютере свободно от цензуры и фильтров. Я хочу подключиться к сети Tor напрямую. Connect Соединиться This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. Соединение на этом компьютере подвергается цензуре, фильтрации либо проксируется. Мне нужно настроить дополнительные параметры соединения. Configure Настроить OfflineStateItem Configure Настроить Details Подробности Connection failed Не удалось соединиться Connecting… \u2026 is ellipsis Соединение... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Внимание!</b> Открывая ссылки браузером по умолчанию Вы можете нарушить Вашу безопасность и анонимность.<br><br>Вместо этого Вы можете <a href='.'>скопировать ссылку</a>. Don't ask again for links from %1 Не спрашивать при открытии последующих ссылок от %1 Don't ask again for any links (not recommended!) Не спрашивать при открытии всех последующих ссылок (не рекомендуется!) Open Browser Открыть браузер Cancel Отмена PreferencesDialog Ricochet Preferences Настройки Ricochet General Основные Contacts Контакты Tor Tor About О программе QCocoaMenuItem Preference Настройки StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Процесс Tor не был запущен успешно. Скорее всего, причина в неправильной установке или системной ошибке. Quit Выход TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Подсоединяемся к сети Tor… Back Назад Hide details Скрыть подробности Show details Показать подробности Done Готово TorConfigurationPage Does this computer need a proxy to access the internet? Этот компьютер использует прокси для доступа в Интернет? Proxy type: Тип прокси: None Нет Address: Адрес: IP address or hostname IP адрес или имя хоста Port: Порт: Username: Имя пользователя: Optional Необязательно Password: Пароль: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? Соединение данного компьютера фильтруется фаерволом, который разрешает доступ только к определенным портам? Allowed ports: Открытые порты: Example: 80,443 Например: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Если соединение этого компьютера подвергается цензуре, Вам необходимо получить и использовать ретрансляторы Tor. Enter one or more bridge relays (one per line): Введите один ретранслятор или более (по одному на строку): Back Назад Connect Соединить TorPreferences Running: Запущен: Yes Да No Нет External Внешний Control connected: Контроль присоединенных: Circuits established: Цепи установлены: Hidden service: Скрытый сервис: Online В сети Offline Не в сети Version: Версия: Error: <b>%1</b> %1 is error message Ошибка: <b>%1</b> Configure Настроить TorStateWidget Connection failed Не удалось соединиться Connecting… \u2026 is ellipsis Соединение… Connecting… (%1%) %1 is progress percentage, e.g. 100 Соединение… (%1%) Online В сети Connected Соединено ricochet-1.1.4/translation/ricochet_sl.ts000066400000000000000000000665751300720305500205370ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Za povezavo z drugimi posredujte svoj Ricochet ID. Cancel Prekliči Add Dodaj ContactActions Open Window Odpri Okno Details... Podrobnosti... Rename Preimenuj Remove Odstrani ContactIDField <b>%1</b> is already your contact <b>%1</b> je že med stiki You can't add yourself as a contact Sebe ne moreš dodati med stike Enter an ID starting with <b>ricochet:</b> Vnesi ID, tako da začneš vnos z <b>ricochet:</b> Copied to clipboard Kopirano v odložišče Copy Kopiraj ContactList Online Povezan Offline Brez povezave Requests Zahteve Rejected Zavrnjeno Outdated Zastarelo ContactPreferences Date added: Dodan datum: Last seen: Nazadnje viden: Request: Zahteva: Pending connection Povezovanje je v teku Delivered Dostavljeno Accepted Sprejeto Error Napaka Rejected Zavrnjeno %1 (Connected) %1 status, e.g. "Accepted" %1 (Povezan) Response: Odgovor: Rename Preimenuj Remove Odstrani ContactRequestDialog Someone new is asking to connect to you Nekdo nov se želi povezati z vami Reject Zavrni Accept Sprejmi ContactRequestFields ID: ID: Name: Ime: Message: Sporočilo: GeneralPreferences Use a single window for conversations Uporabljaj enotno okno za pogovore Open links in default browser without prompting Brez spraševanja odpri povezave v privzetem brskalniku Play audio notifications Predvajaj zvočna obvestila Volume Glasnost Language Jezik Restart Ricochet to apply changes Za uveljavitev sprememb ponovno zaženite Ricochet LanguagePreferences Select Language Izberi jezik Restart Ricochet to apply changes Za uveljavitev sprememb ponovno zaženite Ricochet LanguagesModel System default Sistemsko privzeto Main Ricochet Error Napaka Ricochet MainToolBar Add Contact Dodaj stik Preferences Nastavitve Click to add contacts Klikni za dodajanje kontaktov MainWindow Remove %1 Odstrani %1 Do you want to permanently remove %1? Ali želte za vselej odstraniti %1? MessageDelegate %1 is offline %1 nickname %1 ni povezan Copy ID Kopiraj ID Copy Link Kopiraj povezavo Open with Browser Odpri z brskalnikom Add as Contact Dodaj kot stik Copy Message Kopiraj sporočilo Copy Selection Kopiraj izbor MessageDialogWrapper Remove %1 Odstrani %1 Do you want to permanently remove %1? %1 nickname Ali želte za vselej odstraniti %1? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Ta oseba vam ne bo mogla več pošiljati sporočil in bo obveščena o odstranitvi z vašega seznama. Lahko vam pošlje novo zahtevo za povezavo. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. Povezava tega računalnika z medmrežjem je brez ovir. Želim se neposredno povezati z omrežjem Tor. Connect Poveži se This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. Povezava tega računalnika z medmrežjem je cenzurirana, filtrirana ali preusmerjena. Potrebne so ročne nastavitve. Configure Nastavi OfflineStateItem Configure Nastavi Details Podrobnosti Connection failed Povezovanje ni uspelo Connecting… \u2026 is ellipsis Povezujem... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Pozor!</b> Odpiranje povezav s privzetim brskalnikom škoduje vaši zasebnosti in varnosti.<br><br>Lahko pa jih namesto tega<a href='.'>prekopirate v odložišče</a>. Don't ask again for links from %1 Ne sprašuj več za povezavo z %1 Don't ask again for any links (not recommended!) Ne sprašuj več za nobeno povezavo (ni priporočljivo!) Open Browser Odpri brskalnik Cancel Prekliči PreferencesDialog Ricochet Preferences Nastavitve Ricochet General Splošno Language Jezik Contacts Stiki Tor Tor About Več o QCocoaMenuItem Preference Izbira StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Proces Tor se ni uspel uspešno zagnati. Najverjetneje gre za problem pri instalaciji ali sistemsko napako. Quit Končaj TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Povezujem se z omrežjem Tor... Back Nazaj Hide details Skrij podrobnosti Show details Prikaži podrobnosti Done Izvršeno TorConfigurationPage Does this computer need a proxy to access the internet? Potrebuje ta računalnik proksi za dostop do medmrežja? Proxy type: Vrsta proksija: None Brez Address: Naslov IP address or hostname Naslov IP ali ime gostitelja Port: Vrata: Username: Uporabniško ime: Optional Izbirno Password: Geslo: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? Ali poteka povezava tega računalnika z medmrežjem čez požarni zid, ki dovoljuje povezavo le skozi določena vrata? Allowed ports: Dovoljena vrata: Example: 80,443 Primer: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Če je povezava tega računalnika z medmrežjem cenzurirana, morate pridobiti posredniške mostove - bridge relays. Enter one or more bridge relays (one per line): Vnesite enega ali več posredniških mostov (enega na vrstico): Back Nazaj Connect Poveži se TorPreferences Running: Teče: Yes Da No Ne External Zunanje Control connected: Nadzorno povezan: Circuits established: Vzpostavljen krog: Hidden service: Skrite storitve: Online Povezan Offline Brez povezave Version: Različica: Error: <b>%1</b> %1 is error message Napaka: <b>%1</b> Configure Nastavi TorStateWidget Connection failed Povezava ni uspela Connecting… \u2026 is ellipsis Povezujem... Connecting… (%1%) %1 is progress percentage, e.g. 100 Povezujem… (%1%) Online Povezan Connected Povezan ricochet-1.1.4/translation/ricochet_sq.ts000066400000000000000000000672451300720305500205370ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Ndani me të tjerët ID-në tuaj Ricochet që të lejoni kërkesa lidhjesh Cancel Anulojeni Add Shtoje ContactActions Open Window Hap Dritare Details... Hollësi… Rename Riemërtojeni Remove Hiqe ContactIDField <b>%1</b> is already your contact <b>%1</b> është tashmë kontakt i juaji You can't add yourself as a contact S’mund të shtoni veten si kontakt Enter an ID starting with <b>ricochet:</b> Jepni një ID që fillon me <b>ricochet:</b> Copied to clipboard U kopjua në të papastër Copy Kopjoje ContactList Online Në linjë Offline Jo në linjë Requests Kërkesa Rejected E hedhur tej Outdated E vjetruar ContactPreferences Date added: Datë shtimi: Last seen: Parë së fundi më: Request: Kërkesë: Pending connection Lidhje në pritje të pranimit Delivered Të dërguar Accepted Të pranuara Error Gabim Rejected E hedhur tej %1 (Connected) %1 status, e.g. "Accepted" %1 (I lidhur) Response: Përgjigje: Rename Riemërtojeni Remove Hiqe ContactRequestDialog Someone new is asking to connect to you Dikush i ri po kërkon të lidhet me ju Reject Hidheni tej Accept Pranojeni ContactRequestFields ID: ID: Name: Emër: Message: Mesazh: GeneralPreferences Use a single window for conversations Përdor një dritare të vetme për bisedat Open links in default browser without prompting Hapi lidhjet në shfletuesin parazgjedhje, pa pyetur Play audio notifications Luaji njoftimet audion Volume Volum Language Gjuhë Restart Ricochet to apply changes Riniseni Ricochet-in që të zbatohen ndryshimet LanguagePreferences Select Language Përzgjidhni Gjuhë Restart Ricochet to apply changes Riniseni Ricochet-in që të zbatohen ndryshimet LanguagesModel System default Parazgjedhje sistemi Main Ricochet Error Gabim Ricochet-i MainToolBar Add Contact Shtoni Kontakt Preferences Parapëlqime Click to add contacts Klikoni që të shtoni kontakte MainWindow Remove %1 Hiqe Do you want to permanently remove %1? Doni të hiqet %1 përgjithmonë? MessageDelegate %1 is offline %1 nickname %1 s’është në linjë Copy ID Kopjo ID-në Copy Link Kopjo Lidhjen Open with Browser Hape me Shfletues Add as Contact Shtojeni si Kontakt Copy Message Kopjo Mesazhin Copy Selection Kopjo Përzgjedhjen MessageDialogWrapper Remove %1 Hiqe %1 Do you want to permanently remove %1? %1 nickname Doni të hiqet %1 përgjithmonë? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Ky kontakt s’do të jetë më në gjendje t’ju dërgojë mesazhe, dhe do të njoftohet për heqjen. Ai mund të zgjedhë t’ju dërgojë një kërkesë të re lidhjeje. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. Lidhja në Internet e këtij kompjuteri është pa pengesa. Do të doja të lidhem drejtpërsëdrejti me rrjetin Tor. Connect Lidhu This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. Lidhja në Internet e këtij kompjuteri është e censuruar, e filtruar ose bëhet përmes një ndërmjetësi. Kam nevojë të formësoj rregullime rrjeti. Configure Formësojeni OfflineStateItem Configure Formësojeni Details Hollësi Connection failed Lidhja dështoi Connecting… \u2026 is ellipsis Po lidhet… OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Kujdes!</b> Hapja e lidhjeve me shfletuesin tuaj parazgjedhje do të rrezikojë sigurinë dhe anonimitetin tuaj.<br><br>Më mirë mund <a href='.'>ta kopjoni në të papastër</a>. Don't ask again for links from %1 Mos pyet sërish për lidhje nga %1 Don't ask again for any links (not recommended!) Mos pyet më, për çfarëdo lidhje (e pakëshillueshme!) Open Browser Hap Shfletues Cancel Anulojeni PreferencesDialog Ricochet Preferences Parapëlqime për Ricochet-in General Të përgjithshme Language Gjuhë Contacts Kontakte Tor Tor About Mbi QCocoaMenuItem Preference Parapëlqim StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Procesi Tor s’u nis me sukses. Ka shumë të ngjarë që ky të jetë një gabim instalimi ose sistemi. Quit Dilni TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Po lidhet me rrjetin TOR… Back Mbrapsht Hide details Fshihi hollësitë Show details Shfaqi hollësitë Done U bë TorConfigurationPage Does this computer need a proxy to access the internet? Ka nevojë për ndërmjetës ky kompjuter, që të lidhet në internet? Proxy type: Lloj ndërmjetësi: None Asnjë Address: Adresë: IP address or hostname Adresë IP ose emër strehe Port: Portë: Username: Emër përdoruesi: Optional Opsionale Password: Fjalëkalim: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? A kalon përmes një firewall-i lidhja internet e këtij kompjuteri, i cili lejon lidhje vetëm te disa prej portave? Allowed ports: Porta të lejuara: Example: 80,443 Shembull: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Nëse lidhja në Internet e këtij kompjuteri është e censuruar, do t’ju duhet të merrni dhe përdorni <i>bridge relays</i>. Enter one or more bridge relays (one per line): Jepni një ose më tepër <i>bridge relays</i> (një për rresht): Back Mbrapsht Connect Lidhu TorPreferences Running: Xhiron: Yes Po No Jo External I jashtëm Control connected: I lidhur me kontrollin: Circuits established: Qarqe të vendosur: Hidden service: Shërbim i fshehur: Online Në linjë Offline Jo në linjë Version: Version: Error: <b>%1</b> %1 is error message Gabim: <b>%1</b> Configure Formësojeni TorStateWidget Connection failed Lidhja dështoi Connecting… \u2026 is ellipsis Po lidhet… Connecting… (%1%) %1 is progress percentage, e.g. 100 Po lidhet… (%1%) Online Në linjë Connected I lidhur ricochet-1.1.4/translation/ricochet_sv.ts000066400000000000000000000667571300720305500205530ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Dela ditt Ricochet-ID så att du kan ta emot förfrågningar Cancel Avbryt Add Lägg till ContactActions Open Window Öppna fönster Details... Detaljer... Rename Ändra namn Remove Ta bort ContactIDField <b>%1</b> is already your contact <b>%1</b> finns redan bland dina kontakter You can't add yourself as a contact Du kan inte lägga till dig själv som kontakt Enter an ID starting with <b>ricochet:</b> Fyll i ett ID som startar med <b>ricochet:</b> Copied to clipboard Kopierat till Urklipp Copy Kopiera ContactList Online Online Offline Offline Requests Förfrågningar Rejected Nekad Outdated Inaktuell ContactPreferences Date added: Tillagd datum: Last seen: Sågs senast: Request: Förfrågan: Pending connection Inväntar anslutning Delivered Levererad Accepted Godkänd Error Fel Rejected Nekad %1 (Connected) %1 status, e.g. "Accepted" %1 (Ansluten) Response: Svar: Rename Ändra namn Remove Ta bort ContactRequestDialog Someone new is asking to connect to you Någon ber att få kontakt med dig Reject Avböj Accept Acceptera ContactRequestFields ID: ID: Name: Namn: Message: Meddelande: GeneralPreferences Use a single window for conversations Använd bara ett fönster för konversationer Open links in default browser without prompting Öppna länkar i standardwebbläsaren utan att fråga Play audio notifications Spela ljudnotiser Volume Volym Language Språk Restart Ricochet to apply changes Starta om Ricochet för att aktivera ändringarna LanguagePreferences Select Language Välj språk Restart Ricochet to apply changes Starta om Ricochet för att aktivera ändringarna LanguagesModel System default Systemets standardval Main Ricochet Error Ricochet fel MainToolBar Add Contact Lägg till kontakt Preferences Inställningar Click to add contacts Klicka för att lägga till kontakter MainWindow Remove %1 Ta bort %1 Do you want to permanently remove %1? Vill du ta bort %1 permanent? MessageDelegate %1 is offline %1 nickname %1 är offline Copy ID Kopiera ID Copy Link Kopiera länk Open with Browser Öppna med webbläsare Add as Contact Lägg till som kontakt Copy Message Kopiera meddelande Copy Selection Kopiera markering MessageDialogWrapper Remove %1 Ta bort %1 Do you want to permanently remove %1? %1 nickname Vill du ta bort %1 permanent? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Den här kontakten kommer inte längre att kunna skicka meddelanden till dig, och kommer att bli informerad om borttagningen. Kontakten kan välja att skicka en ny förfrågan. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. Den här datorn kan ansluta till Internet obehindrat. Jag vill ansluta direkt till Tor-nätverket. Connect Anslut This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. Den här datorns Internetanslutning är censurerad, filtrerad eller kräver användning av en proxy. Jag behöver konfigurera nätverksinställningarna. Configure Konfigurera OfflineStateItem Configure Konfigurera Details Detaljer Connection failed Anslutning misslyckades Connecting… \u2026 is ellipsis Ansluter... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Varning!</b> Att öppna länkar med din standard&shy;webbläsare skadar din säkerhet och anonymitet.<br><br>Du kan <a href='.'>kopiera till Urklipp</a> istället. Don't ask again for links from %1 Fråga inte igen för länkar från %1 Don't ask again for any links (not recommended!) Fråga inte igen för några länkar (rekommenderas inte!) Open Browser Öppna webbläsaren Cancel Avbryt PreferencesDialog Ricochet Preferences Inställningar för Ricochet General Allmänt Language Språk Contacts Kontakter Tor Tor About Om QCocoaMenuItem Preference Inställning StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Tor-processen startade inte korrekt. Det beror förmodligen på ett installations- eller systemfel. Quit Avsluta TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Ansluter till Tor-nätverket... Back Tillbaka Hide details Göm detaljer Show details Visa detaljer Done Klar TorConfigurationPage Does this computer need a proxy to access the internet? Behöver den här datorn använda en proxy för att ansluta till Internet? Proxy type: Typ av proxy: None Ingen Address: Adress: IP address or hostname IP-adress eller värdnamn Port: Port: Username: Användarnamn: Optional Krävs ej Password: Lösenord: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? Går den här datorns Internetanslutning genom en brandvägg som bara tillåter anslutningar på vissa portar? Allowed ports: Tillåtna portar: Example: 80,443 Exempel: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Om den här datorns Internetanslutning är censurerad måste du skaffa och använda en brygga till Tor, en så kallad ”bridge relay”. Enter one or more bridge relays (one per line): Ange en eller flera bryggor (en per rad): Back Tillbaka Connect Anslut TorPreferences Running: Körs: Yes Ja No Nej External Extern Control connected: Kontroll ansluten: Circuits established: Kretsar etablerade: Hidden service: Dold tjänst: Online Online Offline Offline Version: Version: Error: <b>%1</b> %1 is error message Fel: <b>%1</b> Configure Konfigurera TorStateWidget Connection failed Anslutning misslyckades Connecting… \u2026 is ellipsis Ansluter... Connecting… (%1%) %1 is progress percentage, e.g. 100 Ansluter… (%1%) Online Online Connected Ansluten ricochet-1.1.4/translation/ricochet_tr.ts000066400000000000000000000674301300720305500205350ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Bağlantı isteklerine izin vermek için Ricochet kimliğinizi paylaşın Cancel İptal Add Ekle ContactActions Open Window Yeni Pencere Details... Ayrıntılar... Rename Yeniden adlandır Remove Çıkar ContactIDField <b>%1</b> is already your contact <b>%1</b> kişi listenize zaten eklenmiş You can't add yourself as a contact Kendinizi kişi listesine ekleyemezsiniz Enter an ID starting with <b>ricochet:</b> <b>ricochet:</b> ile başlayan bir kimlik girin Copied to clipboard Panoya kopyalandı Copy Kopyala ContactList Online Çevrimiçi Offline Çevrimdışı Requests İstekler Rejected Reddedildi Outdated Güncel Değil ContactPreferences Date added: Eklenme tarihi: Last seen: Son görülme tarihi: Request: İstek: Pending connection Beklemedeki bağlantı Delivered Ulaştırıldı Accepted Kabul edildi Error Hata Rejected Reddedildi %1 (Connected) %1 status, e.g. "Accepted" %1 (Bağlandı) Response: Cevap: Rename Yeniden adlandır Remove Çıkar ContactRequestDialog Someone new is asking to connect to you Yeni bir kişi sizinle bağlantı kurmak istiyor Reject Reddet Accept Kabul et ContactRequestFields ID: Kimlik: Name: Ad: Message: Mesaj: GeneralPreferences Use a single window for conversations Görüşmeler için tek pencere kullan Open links in default browser without prompting Bağlantıları bana sormadan varsayılan tarayıcıda aç Play audio notifications Ses bildirimlerini oynat Volume Ses Language Dil Restart Ricochet to apply changes Değişikliklerin uygulanması için Ricochet'i yeniden başlatın. LanguagePreferences Select Language Dil Seçin Restart Ricochet to apply changes Değişikliklerin uygulanması için Ricochet'i yeniden başlatın. LanguagesModel System default Sistem varsayılanı Main Ricochet Error Ricochet Hatası MainToolBar Add Contact Kişi Ekle Preferences Ayarlar Click to add contacts Kişi eklemek için tıklayın MainWindow Remove %1 %1 isimli kullanıcıyı kişi listesinden çıkar Do you want to permanently remove %1? %1 isimli kullanıcıyı kalıcı olarak silmek istiyor musunuz? MessageDelegate %1 is offline %1 nickname %1 çevrimdışı Copy ID Kimliği kopyala Copy Link Bağlantı Kopyala Open with Browser Tarayıcı ile Aç Add as Contact Kişi olarak Ekle Copy Message Mesajı Kopyala Copy Selection Seçileni Kopyala MessageDialogWrapper Remove %1 %1 isimli kullanıcıyı sil Do you want to permanently remove %1? %1 nickname %1 isimli kullanıcıyı kalıcı olarak silmek istiyor musunuz? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Bu kişi artık size mesaj gönderemeyecek ve silinme işlemiyle ilgili bilgilendirilecek. Kişi isterse size yeni bir bağlantı isteği gönderebilir. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. Bu bilgisayarın internete bağlanmasında bir engel yoktur. Doğrudan Tor ağına bağlanmak istiyorum. Connect Bağlan This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. Bu bilgisayarın internet bağlantısı sansürlenmiş, filtrelenmiş ya da proxy kullanmaktadır. Ağ ayarlarını yapılandırmam gerekiyor. Configure Yapılandır OfflineStateItem Configure Yapılandır Details Ayrıntılar Connection failed Bağlantı başarısız Connecting… \u2026 is ellipsis Bağlanıyor... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Uyarı!</b> Varsayılan tarayıcı ile bağlantıları açmak, güvenlik ve anonimliğinize zarar verecektir.<br><br>Bunun yerine <a href='.'>panoya kopyala</a>yabilirsiniz. Don't ask again for links from %1 %1 isimli kullanıcıdan gelen bağlantılar için tekrar sorma Don't ask again for any links (not recommended!) Bundan sonra bağlantılar için bir daha sorma (tavsiye edilmez!) Open Browser Tarayıcıyı Aç Cancel İptal PreferencesDialog Ricochet Preferences Ricochet Ayarları General Genel Language Dil Contacts Kişiler Tor Tor About Hakkında QCocoaMenuItem Preference Tercih StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Tor başlatılamadı. Büyük olasılıkla kurulum veya sistem hatası. Quit Çık TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis Tor ağına bağlanıyor... Back Geri Hide details Ayrıntıları gizle Show details Ayrıntıları göster Done Tamamla TorConfigurationPage Does this computer need a proxy to access the internet? Bu bilgisayar internete erişmek için bir proxy'e ihtiyaç duyuyor mu? Proxy type: Proxy türü: None Hiçbiri Address: Adres: IP address or hostname IP adresi veya host adı Port: Port: Username: Kullanıcı adı: Optional İsteğe bağlı Password: Parola: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? Bu bilgisayarın internet bağlantısı sadece belli portlara bağlantı izni veren bir güvenlik duvarı üzerinden gidiyor mu? Allowed ports: İzin verilen portlar: Example: 80,443 Örnek: 80.443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Bu bilgisayarın internet bağlantısı sansürlenmişse, köprü aktarımlarını edinmeniz ve kullanmanız gerekir. Enter one or more bridge relays (one per line): Bir veya birden fazla köprü aktarım adresi girin (satır başına bir tane): Back Geri Connect Bağlan TorPreferences Running: Çalışıyor: Yes Evet No Hayır External Harici Control connected: Kontrol bağlantısı: Circuits established: Devrelere bağlanıldı: Hidden service: Gizli servis: Online Çevrimiçi Offline Çevrimdışı Version: Sürüm: Error: <b>%1</b> %1 is error message Hata: <b>%1</b> Configure Yapılandır TorStateWidget Connection failed Bağlantı başarısız Connecting… \u2026 is ellipsis Bağlanıyor... Connecting… (%1%) %1 is progress percentage, e.g. 100 Bağlanıyor... (%1%) Online Çevrimiçi Connected Bağlı ricochet-1.1.4/translation/ricochet_uk.ts000066400000000000000000000731031300720305500205210ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests Поділитись вашим Ricochet ID для запитів на підключення Cancel Відмінити Add Додати ContactActions Open Window Відкрити вікно Details... Детальніше... Rename Перейменувати Remove Видалити ContactIDField <b>%1</b> is already your contact <b>%1</b> це вже ваш контакт You can't add yourself as a contact Ви не можете додати самі себе як контакт Enter an ID starting with <b>ricochet:</b> Введіть ID який починається з <b>ricochet:</b> Copied to clipboard Скопійовано до буферу обміну Copy Копіювати ContactList Online В мережі Offline Не в мережі Requests Запити Rejected Відхилено Outdated Застарілий ContactPreferences Date added: Дата додавання: Last seen: Останній раз бачили: Request: Запит: Pending connection Встановлення з'єднання Delivered Доставлено Accepted Прийнято Error Помилка Rejected Відхилено %1 (Connected) %1 status, e.g. "Accepted" %1 (Підключено) Response: Відповідь: Rename Перейменувати Remove Видалити ContactRequestDialog Someone new is asking to connect to you Хтось новий запрошує підключення до вас Reject Відхилити Accept Прийняти ContactRequestFields ID: ID: Name: Ім'я: Message: Повідомлення: GeneralPreferences Use a single window for conversations Використовувати одне вікно для розмов Open links in default browser without prompting Відкривати посилання в браузері без запитань Play audio notifications Програти аудіо повідомлення Volume Гучність Language Мова Restart Ricochet to apply changes Перезавантажити Ricochet для збереження змін LanguagePreferences Select Language Вибір мови Restart Ricochet to apply changes Перезавантажити Ricochet для збереження змін LanguagesModel System default Системні налаштування Main Ricochet Error Помилка Ricochet MainToolBar Add Contact Додати контакт Preferences Налаштування Click to add contacts Натисніть щоб додати контакти MainWindow Remove %1 Видалити %1 Do you want to permanently remove %1? Бажаєте назавжди видалити %1? MessageDelegate %1 is offline %1 nickname %1 не в мережі Copy ID Копіювати ID Copy Link Копіювати посилання Open with Browser Відкрити браузер Add as Contact Додати як контакт Copy Message Скопіювати повідомлення Copy Selection Скопіювати вибране MessageDialogWrapper Remove %1 Видалити %1 Do you want to permanently remove %1? %1 nickname Бажаєте назавжди видалити %1? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. Цей контакт більше не зможе вам писати і буде проінформований про видалення. Він може послати запит на нове з'єднання. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. Це інтернет з'єднання не блокується провайдером. Я б хотів підключитись напряму до мережі Tor. Connect Підключення This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. Це інтернет з'єднання цензурується провайдером або фільтрується. Мені треба налаштувати параметри мережі. Configure Налаштувати OfflineStateItem Configure Налаштувати Details Детальніше Connection failed З'єднання не вдалося. Connecting… \u2026 is ellipsis З'єднання... OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>Попередження!</b> Відкриття посилань у браузері за замовчуванням може нашкодити вашій безпеці та анонімності. <br><br>Замість цього ви можете їх <a href='.'>скопіювати у буфер обміну</a> . Don't ask again for links from %1 Не запитувати знову про посилання від %1 Don't ask again for any links (not recommended!) Не запитувати знову про будь-які посилання (не рекомендовано!) Open Browser Відкрити браузер Cancel Відмінити PreferencesDialog Ricochet Preferences Налаштування Ricochet General Основні Language Мова Contacts Контакти Tor Tor About Про QCocoaMenuItem Preference Налаштування StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Процес Tor не був вдало запущений. Скоріш за все це помилка при установці або системна. Quit Вийти TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis З'єднання з мережею Tor... Back Назад Hide details Сховати деталі Show details Показати деталі Done Виконано TorConfigurationPage Does this computer need a proxy to access the internet? Чи потребує цей комп'ютер проксі для доступу до інтернету? Proxy type: Тип проксі: None Ніякого Address: Адреса: IP address or hostname IP адреса чи ім'я сервера Port: Порт: Username: Ім'я: Optional Опції Password: Пароль: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? З'єднання цього комп'ютеру проходять через firewall який дозволяй з'єднання тільки з певними портами? Allowed ports: Дозволені порти: Example: 80,443 Наприклад: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Якщо інтернет з'єднання цього комп'ютеру цензуруються, то вам потрібно отримати і використовувати ретранслятори. Enter one or more bridge relays (one per line): Введіть один або декілька адрес ретрансляторів (один на лінію) Back Назад Connect Підключення TorPreferences Running: Запущено: Yes Так No Ні External Зовнішній Control connected: Контрольне з'єднання: Circuits established: Схема побудована: Hidden service: Прихований сервіс: Online В мережі Offline Не в мережі Version: Версія: Error: <b>%1</b> %1 is error message Помилка: <b>%1</b> Configure Налаштувати TorStateWidget Connection failed З'єднання не вдалося. Connecting… \u2026 is ellipsis З'єднання... Connecting… (%1%) %1 is progress percentage, e.g. 100 З'єднання... (%1%) Online В мережі Connected Підключено ricochet-1.1.4/translation/ricochet_zh.ts000066400000000000000000000647571300720305500205420ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests 分享您的Ricochet ID给你的好友 Cancel 取消 Add 添加 ContactActions Open Window 打开窗口 Details... 细节…… Rename 重命名 Remove 移除 ContactIDField <b>%1</b> is already your contact <b>%1</b>已经是你的联系人 You can't add yourself as a contact 不能添加自己为联系人 Enter an ID starting with <b>ricochet:</b> 输入一个ID并以<b>ricochet:</b>打头 Copied to clipboard 复制到剪贴板 Copy 复制 ContactList Online 在线 Offline 离线 Requests 请求 Rejected 已拒绝 Outdated ContactPreferences Date added: 添加日期: Last seen: 最后上线: Request: 请求: Pending connection 挂起的连接 Delivered 送达 Accepted 已接受 Error 错误 Rejected 已拒绝 %1 (Connected) %1 status, e.g. "Accepted" %1 (已连接) Response: 回复: Rename 重命名 Remove 移除 ContactRequestDialog Someone new is asking to connect to you 有陌生人希望连接你 Reject 拒绝 Accept 接受 ContactRequestFields ID: 帐号: Name: 名字: Message: 消息: GeneralPreferences Use a single window for conversations Open links in default browser without prompting 无需提示,直接在默认浏览器中打开链接 Play audio notifications 使用声音提醒 Volume 音量 Language 语言 Restart Ricochet to apply changes 应用并重启 LanguagePreferences Select Language 选择语言 Restart Ricochet to apply changes 应用并重启 LanguagesModel System default 系统默认 Main Ricochet Error Ricochet错误 MainToolBar Add Contact 添加联系人 Preferences 选项 Click to add contacts 点击添加联系人 MainWindow Remove %1 移除%1 Do you want to permanently remove %1? 确定永久移除%1? MessageDelegate %1 is offline %1 nickname %1离线中 Copy ID 复制帐号 Copy Link 复制链接 Open with Browser 用浏览器打开 Add as Contact 添加为联系人 Copy Message 复制消息 Copy Selection 复制选择内容 MessageDialogWrapper Remove %1 移除%1 Do you want to permanently remove %1? %1 nickname 确定永久移除%1? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. 这台电脑的网络连接没有任何障碍,我希望直接连到Tor网络。 Connect 连接 This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. 这台电脑的网络连接是经过审查、过滤或代理的,我希望配置网络设置。 Configure 配置 OfflineStateItem Configure 配置 Details 细节 Connection failed 连接失败 Connecting… \u2026 is ellipsis 连接中…… OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. Don't ask again for links from %1 不再询问来自%1的链接 Don't ask again for any links (not recommended!) 不再询问任何链接(不推荐) Open Browser 打开浏览器 Cancel 取消 PreferencesDialog Ricochet Preferences Ricochet选项 General 通用 Language 语言 Contacts 联系人 Tor Tor About 关于 QCocoaMenuItem Preference 选项 StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Tor 没有启动成功。可能需要重新安装。 Quit 退出 TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis 连接到Tor网络中…… Back 返回 Hide details 隐藏细节 Show details 显示细节 Done 已完成 TorConfigurationPage Does this computer need a proxy to access the internet? 这台电脑需要通过代理来连接网络吗? Proxy type: 代理类型: None None Address: 地址: IP address or hostname IP地址或主机名 Port: 端口: Username: 用户名: Optional 可选 Password: 密码: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? Allowed ports: 允许的端口: Example: 80,443 例如:80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. Enter one or more bridge relays (one per line): Back 返回 Connect 连接 TorPreferences Running: 运行中 Yes No External 外部 Control connected: 联系人已连接: Circuits established: 线路已经建立: Hidden service: 隐藏的服务: Online 在线 Offline 离线 Version: 版本: Error: <b>%1</b> %1 is error message 错误: <b>%1</b> Configure 配置 TorStateWidget Connection failed 连接失败 Connecting… \u2026 is ellipsis 连接中…… Connecting… (%1%) %1 is progress percentage, e.g. 100 连接中……(%1%) Online 在线 Connected 已连接 ricochet-1.1.4/translation/ricochet_zh_HK.ts000066400000000000000000000656001300720305500211100ustar00rootroot00000000000000 AboutPreferences Ricochet %1 %1 version, e.g. 1.0.0 Ricochet %1 AddContactDialog Share your Ricochet ID to allow connection requests 分享你的Ricochet ID 以允許連線的請求 Cancel 取消 Add 新增 ContactActions Open Window 開啟視窗 Details... 細節 Rename 重新命名 Remove 移除 ContactIDField <b>%1</b> is already your contact <b>%1</b> 已是你的聯絡人 You can't add yourself as a contact 你不能新增自己為聯絡人 Enter an ID starting with <b>ricochet:</b> 輸入ID開始使用 <b>ricochet:</b> Copied to clipboard 複製到剪貼簿 Copy 複製 ContactList Online 上線 Offline 離線 Requests 請求 Rejected 拒絕 Outdated 過時 ContactPreferences Date added: 新增之日期: Last seen: 上回瀏覧: Request: 請求: Pending connection 連線擱置中 Delivered 已傳送 Accepted 已接受 Error 錯誤 Rejected 拒絕 %1 (Connected) %1 status, e.g. "Accepted" %1 (己連結) Response: 回應: Rename 重新命名 Remove 移除 ContactRequestDialog Someone new is asking to connect to you 某新人要求與你連線 Reject 拒絕 Accept 接受 ContactRequestFields ID: ID: Name: 名稱: Message: 訊息: GeneralPreferences Use a single window for conversations 使用單一視窗來對話 Open links in default browser without prompting 在原瀏覧器中開啟連結而不要跳出新視窗 Play audio notifications 開啟通知音訊 Volume 音量 Language 語言 Restart Ricochet to apply changes 重啟Ricochet 以更新改變 LanguagePreferences Select Language 選取語言 Restart Ricochet to apply changes 重啟Ricochet 以更新改變 LanguagesModel System default 系統預設 Main Ricochet Error Ricochet 出錯 MainToolBar Add Contact 新增聯絡人 Preferences 偏好 Click to add contacts 點擊來新增聯絡人 MainWindow Remove %1 移除%1 Do you want to permanently remove %1? 你是否要永遠移除%1? MessageDelegate %1 is offline %1 nickname %1 已離線 Copy ID 複製ID Copy Link 複製連結 Open with Browser 開啟瀏覧器 Add as Contact 新增聯絡人 Copy Message 複製訊息 Copy Selection 複製選項 MessageDialogWrapper Remove %1 移除%1 Do you want to permanently remove %1? %1 nickname 你是否要永遠移除%1? This contact will no longer be able to message you, and will be notified about the removal. They may choose to send a new connection request. 這名聯絡人不再傳訊給你,也將接到遭移除的通知。他們可以選擇重新送出連結請求 NetworkSetupWizard This computer's Internet connection is free of obstacles. I would like to connect directly to the Tor network. 這台電腦的網路連結未有任何阻礙,可以直接連結上Tor網路 Connect 連線 This computer's Internet connection is censored, filtered, or proxied. I need to configure network settings. 如果這台電腦的網路連線遭到監控,我必須修改網路設定 Configure 設定 OfflineStateItem Configure 設定 Details 細節 Connection failed 連線失敗 Connecting… \u2026 is ellipsis 連線中 OpenBrowserDialog <b>Warning!</b> Opening links with your default browser will harm your security and anonymity.<br><br>You can <a href='.'>copy to the clipboard</a> instead. <b>警告!</b> 用你的瀏覧器直接開啟連結可能會對安全與匿名性造成威脅.<br><br>你可另以<a href='.'>複製到剪貼簿</a>取代 . Don't ask again for links from %1 別再請求來自%1的連結 Don't ask again for any links (not recommended!) 別再請求任何連結(不建議!) Open Browser 開啟瀏覧器 Cancel 取消 PreferencesDialog Ricochet Preferences Ricochet 偏好 General 一般 Language 語言 Contacts 聯絡人 Tor Tor About 關於 QCocoaMenuItem Preference 偏好 StartupStatusPage The Tor process was not started successfully. This is most likely an installation or system error. Tor連結過程並未成功啟動,這大部份是由於安裝或系統出錯 Quit 中止 TorBootstrapStatus Connecting to the Tor network… \u2026 is ellipsis 連線到Tor網路 Back 退後 Hide details 隱藏細節 Show details 顯示細節 Done 完成 TorConfigurationPage Does this computer need a proxy to access the internet? 這台電腦是否需要透過代理主機來連網? Proxy type: Proxy 類型: None Address: 地址: IP address or hostname I位置或主機名稱 Port: 埠部: Username: 使用者名稱: Optional 選項 Password: 密碼: Does this computer's Internet connection go through a firewall that only allows connections to certain ports? 這台電腦的網路連線是否透過防火牆,只能充許某些埠號來連網? Allowed ports: 允許埠部: Example: 80,443 示範: 80,443 If this computer's Internet connection is censored, you will need to obtain and use bridge relays. 如果這台電腦的網路連線遭到監控,你必須改用中續撟接服務 Enter one or more bridge relays (one per line): 輸入一個以上的中續橋接(一行一個): Back 退後 Connect 連線 TorPreferences Running: 執行中: Yes No External 外部 Control connected: 連線控制: Circuits established: 已建立巡迴: Hidden service: 隱藏服務: Online 上線 Offline 離線 Version: 版本: Error: <b>%1</b> %1 is error message <b>%1</b>出錯 Configure 設定 TorStateWidget Connection failed 連線失敗 Connecting… \u2026 is ellipsis 連線中 Connecting… (%1%) %1 is progress percentage, e.g. 100 (%1%)連線中… Online 上線 Connected 已連結