pax_global_header00006660000000000000000000000064146507153660014527gustar00rootroot0000000000000052 comment=5d7b581db641c96b690d1f735e462291d68b8716 rest-server-0.13.0/000077500000000000000000000000001465071536600140715ustar00rootroot00000000000000rest-server-0.13.0/.github/000077500000000000000000000000001465071536600154315ustar00rootroot00000000000000rest-server-0.13.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001465071536600176145ustar00rootroot00000000000000rest-server-0.13.0/.github/ISSUE_TEMPLATE/BUG.md000066400000000000000000000053631465071536600205620ustar00rootroot00000000000000--- name: Bug report about: Report a problem with rest-server to help us resolve it and improve --- Output of `rest-server --version` --------------------------------- How did you run rest-server exactly? ------------------------------------ What backend/server/service did you use to store the repository? ---------------------------------------------------------------- Expected behavior ----------------- Actual behavior --------------- Steps to reproduce the behavior ------------------------------- Do you have any idea what may have caused this? ----------------------------------------------- Do you have an idea how to solve the issue? ------------------------------------------- Did rest-server help you today? Did it make you happy in any way? ----------------------------------------------------------------- rest-server-0.13.0/.github/ISSUE_TEMPLATE/FEATURE.md000066400000000000000000000034771465071536600212440ustar00rootroot00000000000000--- name: Feature request/enhancement about: Suggest a new feature or enhancement for rest-server --- Output of `rest-server --version` --------------------------------- What should rest-server do differently? --------------------------------------- What are you trying to do? What is your use case? ------------------------------------------------- Did rest-server help you today? Did it make you happy in any way? ----------------------------------------------------------------- rest-server-0.13.0/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000002451465071536600216050ustar00rootroot00000000000000contact_links: - name: restic forum url: https://forum.restic.net about: Please ask questions about using restic here, do not open an issue for questions. rest-server-0.13.0/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000033321465071536600212330ustar00rootroot00000000000000 What is the purpose of this change? What does it change? -------------------------------------------------------- Was the change discussed in an issue or in the forum before? ------------------------------------------------------------ Checklist --------- - [ ] I have enabled [maintainer edits for this PR](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) - [ ] I have added tests for all changes in this PR - [ ] I have added documentation for the changes (in the manual) - [ ] There's a new file in `changelog/unreleased/` that describes the changes for our users (template [here](https://github.com/restic/rest-server/blob/master/changelog/TEMPLATE)) - [ ] I have run `gofmt` on the code in all commits - [ ] All commit messages are formatted in the same style as [the other commits in the repo](https://github.com/restic/rest-server/commits/master) - [ ] I'm done, this Pull Request is ready for review rest-server-0.13.0/.github/dependabot.yml000066400000000000000000000005051465071536600202610ustar00rootroot00000000000000version: 2 updates: # Dependencies listed in go.mod - package-ecosystem: "gomod" directory: "/" # Location of package manifests schedule: interval: "weekly" # Dependencies listed in .github/workflows/*.yml - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" rest-server-0.13.0/.github/workflows/000077500000000000000000000000001465071536600174665ustar00rootroot00000000000000rest-server-0.13.0/.github/workflows/tests.yml000066400000000000000000000043641465071536600213620ustar00rootroot00000000000000name: test on: # run tests on push to master, but not when other branches are pushed to push: branches: - master # run tests for all pull requests pull_request: env: latest_go: "1.21.x" GO111MODULE: on jobs: test: strategy: matrix: go: - 1.18.x - 1.19.x - 1.20.x - 1.21.x runs-on: ubuntu-latest name: Go ${{ matrix.go }} env: GOPROXY: https://proxy.golang.org steps: - name: Set up Go ${{ matrix.go }} uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} - name: Check out code uses: actions/checkout@v4 - name: Build run: | go build ./cmd/rest-server - name: Build with build.go run: | go run build.go --goos linux go run build.go --goos windows go run build.go --goos darwin - name: Run tests run: | go test ./... - name: Check changelog files with calens run: | echo "install calens" go install github.com/restic/calens@latest echo "check changelog files" calens lint: name: lint runs-on: ubuntu-latest steps: - name: Set up Go ${{ env.latest_go }} uses: actions/setup-go@v5 with: go-version: ${{ env.latest_go }} - name: Check out code uses: actions/checkout@v4 - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. version: v1.51 # Optional: show only new issues if it's a pull request. The default value is `false`. only-new-issues: true args: --verbose --timeout 10m # only run golangci-lint for pull requests, otherwise ALL hints get # reported. We need to slowly address all issues until we can enable # linting the master branch :) if: github.event_name == 'pull_request' - name: Check go.mod/go.sum run: | echo "check if go.mod and go.sum are up to date" go mod tidy git diff --exit-code go.mod go.sum rest-server-0.13.0/.gitignore000066400000000000000000000000151465071536600160550ustar00rootroot00000000000000/rest-server rest-server-0.13.0/.golangci.yml000066400000000000000000000030031465071536600164510ustar00rootroot00000000000000# This is the configuration for golangci-lint for the restic project. # # A sample config with all settings is here: # https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml linters: # only enable the linters listed below disable-all: true enable: # make sure all errors returned by functions are handled - errcheck # find unused code - deadcode # show how code can be simplified - gosimple # # make sure code is formatted - gofmt # examine code and report suspicious constructs, such as Printf calls whose # arguments do not align with the format string - govet # make sure names and comments are used according to the conventions - revive # detect when assignments to existing variables are not used - ineffassign # run static analysis and find errors - staticcheck # find unused variables, functions, structs, types, etc. - unused # find unused struct fields - structcheck # find unused global variables - varcheck # parse and typecheck code - typecheck issues: # don't use the default exclude rules, this hides (among others) ignored # errors from Close() calls exclude-use-default: false # list of things to not warn about exclude: # revive: do not warn about missing comments for exported stuff - exported (function|method|var|type|const) .* should have comment or be unexported # revive: ignore constants in all caps - don't use ALL_CAPS in Go names; use CamelCase rest-server-0.13.0/.goreleaser.yml000066400000000000000000000164371465071536600170350ustar00rootroot00000000000000--- version: 2 before: # Run a few commands to check the state of things. When anything is changed # in files commited to the repo, goreleaser will abort before building # anything because the git checkout is dirty. hooks: # make sure all modules are available - go mod download # make sure all generated code is up to date - go generate ./... # check that $VERSION is set - test -n "{{ .Env.VERSION }}" # make sure the file VERSION contains the latest version (used for build.go) - bash -c 'echo "{{ .Env.VERSION }}" > VERSION' # make sure that main.go contains the latest version - echo sed -i 's/var version = "[^"]*"/var version = "{{ .Env.VERSION }}"/' cmd/rest-server/main.go # make sure the file CHANGELOG.md is up to date - calens --output CHANGELOG.md # build a single binary builds: - # make sure everything is statically linked by disabling cgo altogether env: - CGO_ENABLED=0 # set the package for the main binary main: ./cmd/rest-server flags: # don't include any paths to source files in the resulting binary - -trimpath mod_timestamp: '{{ .CommitTimestamp }}' ldflags: # set the version variable in the main package - "-s -w -X main.version={{ .Version }}" # list all operating systems and architectures we build binaries for goos: - linux - darwin - windows - freebsd - netbsd - openbsd - dragonfly - solaris goarch: - amd64 - 386 - arm - arm64 - mips - mips64 - mips64le - ppc64 - ppc64le goarm: - 6 - 7 # configure the resulting archives to create archives: - # package a directory which contains the source file wrap_in_directory: true builds_info: &archive_file_info owner: root group: root mtime: '{{ .CommitDate }}' mode: 0644 # add these files to all archives files: - src: LICENSE dst: . info: *archive_file_info - src: README.md dst: . info: *archive_file_info - src: CHANGELOG.md dst: . info: *archive_file_info # also build an archive of the source code source: enabled: true # build a file containing the SHA256 hashes checksum: name_template: 'SHA256SUMS' # sign the checksum file signs: - artifacts: checksum signature: "${artifact}.asc" args: - "--armor" - "--output" - "${signature}" - "--detach-sign" - "${artifact}" # configure building the rest-server docker image dockers: - image_templates: - restic/rest-server:{{ .Version }}-amd64 build_flag_templates: - "--platform=linux/amd64" - "--pull" - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.source=https://github.com/restic/{{ .ProjectName }}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.licenses=BSD-2-Clause" use: buildx dockerfile: "Dockerfile.goreleaser" extra_files: &extra_files - docker/create_user - docker/delete_user - docker/entrypoint.sh - image_templates: - restic/rest-server:{{ .Version }}-i386 goarch: 386 build_flag_templates: - "--platform=linux/386" - "--pull" - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.source=https://github.com/restic/{{ .ProjectName }}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.licenses=BSD-2-Clause" use: buildx dockerfile: "Dockerfile.goreleaser" extra_files: *extra_files - image_templates: - restic/rest-server:{{ .Version }}-arm32v6 goarch: arm goarm: 6 build_flag_templates: - "--platform=linux/arm/v6" - "--pull" - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.source=https://github.com/restic/{{ .ProjectName }}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.licenses=BSD-2-Clause" use: buildx dockerfile: "Dockerfile.goreleaser" extra_files: *extra_files - image_templates: - restic/rest-server:{{ .Version }}-arm32v7 goarch: arm goarm: 7 build_flag_templates: - "--platform=linux/arm/v7" - "--pull" - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.source=https://github.com/restic/{{ .ProjectName }}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.licenses=BSD-2-Clause" use: buildx dockerfile: "Dockerfile.goreleaser" extra_files: *extra_files - image_templates: - restic/rest-server:{{ .Version }}-arm64v8 goarch: arm64 build_flag_templates: - "--platform=linux/arm64/v8" - "--pull" - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.source=https://github.com/restic/{{ .ProjectName }}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.licenses=BSD-2-Clause" use: buildx dockerfile: "Dockerfile.goreleaser" extra_files: *extra_files - image_templates: - restic/rest-server:{{ .Version }}-ppc64le goarch: ppc64le build_flag_templates: - "--platform=linux/ppc64le" - "--pull" - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.source=https://github.com/restic/{{ .ProjectName }}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.licenses=BSD-2-Clause" use: buildx dockerfile: "Dockerfile.goreleaser" extra_files: *extra_files docker_manifests: - name_template: "restic/rest-server:{{ .Version }}" image_templates: - "restic/rest-server:{{ .Version }}-amd64" - "restic/rest-server:{{ .Version }}-i386" - "restic/rest-server:{{ .Version }}-arm32v6" - "restic/rest-server:{{ .Version }}-arm32v7" - "restic/rest-server:{{ .Version }}-arm64v8" - "restic/rest-server:{{ .Version }}-ppc64le" - name_template: "restic/rest-server:latest" image_templates: - "restic/rest-server:{{ .Version }}-amd64" - "restic/rest-server:{{ .Version }}-i386" - "restic/rest-server:{{ .Version }}-arm32v6" - "restic/rest-server:{{ .Version }}-arm32v7" - "restic/rest-server:{{ .Version }}-arm64v8" - "restic/rest-server:{{ .Version }}-ppc64le" rest-server-0.13.0/AUTHORS000066400000000000000000000011111465071536600151330ustar00rootroot00000000000000# This is the official list of Rest Server authors for copyright purposes. Aaron Bieber Alexander Neumann Bertil Chapuis Brice Waegeneire Bruno Clermont Chapuis Bertil Kenny Keslar Konrad Wojas Matthew Holt Mebus Wayne Scott Zlatko Čalušić cgonzalez n0npax rest-server-0.13.0/CHANGELOG.md000066400000000000000000000415571465071536600157160ustar00rootroot00000000000000Changelog for rest-server 0.13.0 (2024-07-26) ============================================ The following sections list the changes in rest-server 0.13.0 relevant to users. The changes are ordered by importance. Summary ------- * Chg #267: Update dependencies and require Go 1.18 or newer * Chg #273: Shut down cleanly on TERM and INT signals * Enh #271: Print listening address after start-up * Enh #272: Support listening on a unix socket Details ------- * Change #267: Update dependencies and require Go 1.18 or newer Most dependencies have been updated. Since some libraries require newer language features, support for Go 1.17 has been dropped, which means that rest-server now requires at least Go 1.18 to build. https://github.com/restic/rest-server/pull/267 * Change #273: Shut down cleanly on TERM and INT signals Rest-server now listens for TERM and INT signals and cleanly closes down the http.Server and listener when receiving either of them. This is particularly useful when listening on a unix socket, as the server will now remove the socket file when it shuts down. https://github.com/restic/rest-server/pull/273 * Enhancement #271: Print listening address after start-up When started with `--listen :0`, rest-server would print `start server on :0` The message now also includes the actual address listened on, for example `start server on 0.0.0.0:37333`. This is useful when starting a server with an auto-allocated free port number (port 0). https://github.com/restic/rest-server/pull/271 * Enhancement #272: Support listening on a unix socket It is now possible to make rest-server listen on a unix socket by prefixing the socket filename with `unix:` and passing it to the `--listen` option, for example `--listen unix:/tmp/foo`. This is useful in combination with remote port forwarding to enable a remote server to backup locally, e.g.: ``` rest-server --listen unix:/tmp/foo & ssh -R /tmp/foo:/tmp/foo user@host restic -r rest:http+unix:///tmp/foo:/repo backup ``` https://github.com/restic/rest-server/pull/272 Changelog for rest-server 0.12.1 (2023-07-09) ============================================ The following sections list the changes in rest-server 0.12.1 relevant to users. The changes are ordered by importance. Summary ------- * Fix #230: Fix erroneous warnings about unsupported fsync * Fix #238: API: Return empty array when listing empty folders * Enh #217: Log to stdout using the `--log -` option Details ------- * Bugfix #230: Fix erroneous warnings about unsupported fsync Due to a regression in rest-server 0.12.0, it continuously printed `WARNING: fsync is not supported by the data storage. This can lead to data loss, if the system crashes or the storage is unexpectedly disconnected.` for systems that support fsync. We have fixed the warning. https://github.com/restic/rest-server/issues/230 https://github.com/restic/rest-server/pull/231 * Bugfix #238: API: Return empty array when listing empty folders Rest-server returned `null` when listing an empty folder. This has been changed to returning an empty array in accordance with the REST protocol specification. This change has no impact on restic users. https://github.com/restic/rest-server/issues/238 https://github.com/restic/rest-server/pull/239 * Enhancement #217: Log to stdout using the `--log -` option Logging to stdout was possible using `--log /dev/stdout`. However, when the rest server is run as a different user, for example, using `sudo -u restic rest-server [...] --log /dev/stdout` This did not work due to permission issues. For logging to stdout, the `--log` option now supports the special filename `-` which also works in these cases. https://github.com/restic/rest-server/pull/217 Changelog for rest-server 0.12.0 (2023-04-24) ============================================ The following sections list the changes in rest-server 0.12.0 relevant to users. The changes are ordered by importance. Summary ------- * Fix #183: Allow usernames containing underscore and more * Fix #219: Ignore unexpected files in the data/ folder * Fix #1871: Return 500 "Internal server error" if files cannot be read * Chg #207: Return error if command-line arguments are specified * Chg #208: Update dependencies and require Go 1.17 or newer * Enh #133: Cache basic authentication credentials * Enh #187: Allow configurable location for `.htpasswd` file Details ------- * Bugfix #183: Allow usernames containing underscore and more The security fix in rest-server 0.11.0 (#131) disallowed usernames containing and underscore "_". The list of allowed characters has now been changed to include Unicode characters, numbers, "_", "-", "." and "@". https://github.com/restic/rest-server/issues/183 https://github.com/restic/rest-server/pull/184 * Bugfix #219: Ignore unexpected files in the data/ folder If the data folder of a repository contained files, this would prevent restic from retrieving a list of file data files. This has been fixed. As a workaround remove the files that are directly contained in the data folder (e.g., `.DS_Store` files). https://github.com/restic/rest-server/issues/219 https://github.com/restic/rest-server/pull/221 * Bugfix #1871: Return 500 "Internal server error" if files cannot be read When files in a repository cannot be read by rest-server, for example after running `restic prune` directly on the server hosting the repositories in a way that causes filesystem permissions to be wrong, rest-server previously returned 404 "Not Found" as status code. This was causing confusing for users. The error handling has now been fixed to only return 404 "Not Found" if the file actually does not exist. Otherwise a 500 "Internal server error" is reported to the client and the underlying error is logged at the server side. https://github.com/restic/rest-server/issues/1871 https://github.com/restic/rest-server/pull/195 * Change #207: Return error if command-line arguments are specified Command line arguments are ignored by rest-server, but there was previously no indication of this when they were supplied anyway. To prevent usage errors an error is now printed when command line arguments are supplied, instead of them being silently ignored. https://github.com/restic/rest-server/pull/207 * Change #208: Update dependencies and require Go 1.17 or newer Most dependencies have been updated. Since some libraries require newer language features, support for Go 1.15-1.16 has been dropped, which means that rest-server now requires at least Go 1.17 to build. https://github.com/restic/rest-server/pull/208 * Enhancement #133: Cache basic authentication credentials To speed up the verification of basic auth credentials, rest-server now caches passwords for a minute in memory. That way the expensive verification of basic auth credentials can be skipped for most requests issued by a single restic run. The password is kept in memory in a hashed form and not as plaintext. https://github.com/restic/rest-server/issues/133 https://github.com/restic/rest-server/pull/138 * Enhancement #187: Allow configurable location for `.htpasswd` file It is now possible to specify the location of the `.htpasswd` file using the `--htpasswd-file` option. https://github.com/restic/rest-server/issues/187 https://github.com/restic/rest-server/pull/188 Changelog for rest-server 0.11.0 (2022-02-10) ============================================ The following sections list the changes in rest-server 0.11.0 relevant to users. The changes are ordered by importance. Summary ------- * Sec #131: Prevent loading of usernames containing a slash * Fix #119: Fix Docker configuration for `DISABLE_AUTHENTICATION` * Fix #142: Fix possible data loss due to interrupted network connections * Fix #155: Reply "insufficient storage" on disk full or over-quota * Fix #157: Use platform-specific temporary directory as default data directory * Chg #112: Add subrepo support and refactor server code * Chg #146: Build rest-server at docker container build time * Enh #122: Verify uploaded files * Enh #126: Allow running rest-server via systemd socket activation * Enh #148: Expand use of security features in example systemd unit file Details ------- * Security #131: Prevent loading of usernames containing a slash "/" is valid char in HTTP authorization headers, but is also used in rest-server to map usernames to private repos. This commit prevents loading maliciously composed usernames like "/foo/config" by restricting the allowed characters to the unicode character class, numbers, "-", "." and "@". This prevents requests to other users files like: Curl -v -X DELETE -u foo/config:attack http://localhost:8000/foo/config https://github.com/restic/rest-server/issues/131 https://github.com/restic/rest-server/pull/132 https://github.com/restic/rest-server/pull/137 * Bugfix #119: Fix Docker configuration for `DISABLE_AUTHENTICATION` Rest-server 0.10.0 introduced a regression which caused the `DISABLE_AUTHENTICATION` environment variable to stop working for the Docker container. This has been fixed by automatically setting the option `--no-auth` to disable authentication. https://github.com/restic/rest-server/issues/119 https://github.com/restic/rest-server/pull/124 * Bugfix #142: Fix possible data loss due to interrupted network connections When rest-server was run without `--append-only` it was possible to lose uploaded files in a specific scenario in which a network connection was interrupted. For the data loss to occur a file upload by restic would have to be interrupted such that restic notices the interrupted network connection before the rest-server. Then restic would have to retry the file upload and finish it before the rest-server notices that the initial upload has failed. Then the uploaded file would be accidentally removed by rest-server when trying to cleanup the failed upload. This has been fixed by always uploading to a temporary file first which is moved in position only once it was uploaded completely. https://github.com/restic/rest-server/pull/142 * Bugfix #155: Reply "insufficient storage" on disk full or over-quota When there was no space left on disk, or any other write-related error occurred, rest-server replied with HTTP status code 400 (Bad request). This is misleading (restic client will dump the status code to the user). Rest-server now replies with two different status codes in these situations: * HTTP 507 "Insufficient storage" is the status on disk full or repository over-quota * HTTP 500 "Internal server error" is used for other disk-related errors https://github.com/restic/rest-server/issues/155 https://github.com/restic/rest-server/pull/160 * Bugfix #157: Use platform-specific temporary directory as default data directory If no data directory is specificed, then rest-server now uses the Go standard library functions to retrieve the standard temporary directory path for the current platform. https://github.com/restic/rest-server/issues/157 https://github.com/restic/rest-server/pull/158 * Change #112: Add subrepo support and refactor server code Support for multi-level repositories has been added, so now each user can have its own subrepositories. This feature is always enabled. Authentication for the Prometheus /metrics endpoint can now be disabled with the new `--prometheus-no-auth` flag. We have split out all HTTP handling to a separate `repo` subpackage to cleanly separate the server code from the code that handles a single repository. The new RepoHandler also makes it easier to reuse rest-server as a Go component in any other HTTP server. The refactoring makes the code significantly easier to follow and understand, which in turn makes it easier to add new features, audit for security and debug issues. https://github.com/restic/rest-server/issues/109 https://github.com/restic/rest-server/issues/107 https://github.com/restic/rest-server/pull/112 * Change #146: Build rest-server at docker container build time The Dockerfile now includes a build stage such that the latest rest-server is always built and packaged. This is done in a standard golang container to ensure a clean build environment and only the final binary is shipped rather than the whole build environment. https://github.com/restic/rest-server/issues/146 https://github.com/restic/rest-server/pull/145 * Enhancement #122: Verify uploaded files The rest-server now by default verifies that the hash of content of uploaded files matches their filename. This ensures that transmission errors are detected and forces restic to retry the upload. On low-power devices it can make sense to disable this check by passing the `--no-verify-upload` flag. https://github.com/restic/rest-server/issues/122 https://github.com/restic/rest-server/pull/130 * Enhancement #126: Allow running rest-server via systemd socket activation We've added the option to have systemd create the listening socket and start the rest-server on demand. https://github.com/restic/rest-server/issues/126 https://github.com/restic/rest-server/pull/151 https://github.com/restic/rest-server/pull/127 * Enhancement #148: Expand use of security features in example systemd unit file The example systemd unit file now enables additional systemd features to mitigate potential security vulnerabilities in rest-server and the various packages and operating system components which it relies upon. https://github.com/restic/rest-server/issues/148 https://github.com/restic/rest-server/pull/149 Changelog for rest-server 0.10.0 (2020-09-13) ============================================ The following sections list the changes in rest-server 0.10.0 relevant to users. The changes are ordered by importance. Summary ------- * Sec #60: Require auth by default, add --no-auth flag * Sec #64: Refuse overwriting config file in append-only mode * Sec #117: Stricter path sanitization * Chg #102: Remove vendored dependencies * Enh #44: Add changelog file Details ------- * Security #60: Require auth by default, add --no-auth flag In order to prevent users from accidentally exposing rest-server without authentication, rest-server now defaults to requiring a .htpasswd. If you want to disable authentication, you need to explicitly pass the new --no-auth flag. https://github.com/restic/rest-server/issues/60 https://github.com/restic/rest-server/pull/61 * Security #64: Refuse overwriting config file in append-only mode While working on the `rclone serve restic` command we noticed that is currently possible to overwrite the config file in a repo even if `--append-only` is specified. The first commit adds proper tests, and the second commit fixes the issue. https://github.com/restic/rest-server/pull/64 * Security #117: Stricter path sanitization The framework we're using in rest-server to decode paths to repositories allowed specifying URL-encoded characters in paths, including sensitive characters such as `/` (encoded as `%2F`). We've changed this unintended behavior, such that rest-server now rejects such paths. In particular, it is no longer possible to specify sub-repositories for users by encoding the path with `%2F`, such as `http://localhost:8000/foo%2Fbar`, which means that this will unfortunately be a breaking change in that case. If using sub-repositories for users is important to you, please let us know in the forum, so we can learn about your use case and implement this properly. As it currently stands, the ability to use sub-repositories was an unintentional feature made possible by the URL decoding framework used, and hence never meant to be supported in the first place. If we wish to have this feature in rest-server, we'd like to have it implemented properly and intentionally. https://github.com/restic/rest-server/issues/117 * Change #102: Remove vendored dependencies We've removed the vendored dependencies (in the subdir `vendor/`) similar to what we did for `restic` itself. When building restic, the Go compiler automatically fetches the dependencies. It will also cryptographically verify that the correct code has been fetched by using the hashes in `go.sum` (see the link to the documentation below). Building the rest-server now requires Go 1.11 or newer, since we're using Go Modules for dependency management. Older Go versions are not supported any more. https://github.com/restic/rest-server/issues/102 https://golang.org/cmd/go/#hdr-Module_downloading_and_verification * Enhancement #44: Add changelog file https://github.com/restic/rest-server/issues/44 https://github.com/restic/rest-server/pull/62 rest-server-0.13.0/Dockerfile000066400000000000000000000007121465071536600160630ustar00rootroot00000000000000FROM golang:alpine AS builder ENV CGO_ENABLED 0 COPY . /build WORKDIR /build RUN go build -o rest-server ./cmd/rest-server FROM alpine ENV DATA_DIRECTORY /data ENV PASSWORD_FILE /data/.htpasswd RUN apk add --no-cache --update apache2-utils COPY docker/create_user /usr/bin/ COPY docker/delete_user /usr/bin/ COPY docker/entrypoint.sh /entrypoint.sh COPY --from=builder /build/rest-server /usr/bin VOLUME /data EXPOSE 8000 CMD [ "/entrypoint.sh" ] rest-server-0.13.0/Dockerfile.goreleaser000066400000000000000000000004631465071536600202150ustar00rootroot00000000000000FROM alpine ENV DATA_DIRECTORY /data ENV PASSWORD_FILE /data/.htpasswd RUN apk add --no-cache --update apache2-utils COPY docker/create_user /usr/bin/ COPY docker/delete_user /usr/bin/ COPY docker/entrypoint.sh /entrypoint.sh COPY rest-server /usr/bin VOLUME /data EXPOSE 8000 CMD [ "/entrypoint.sh" ] rest-server-0.13.0/LICENSE000066400000000000000000000026151465071536600151020ustar00rootroot00000000000000The BSD 2-Clause License Copyright © 2015, Bertil Chapuis Copyright © 2016, Zlatko Čalušić, Alexander Neumann Copyright © 2017, The Rest Server Authors All rights reserved. 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. 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 HOLDER 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. rest-server-0.13.0/README.md000066400000000000000000000240301465071536600153470ustar00rootroot00000000000000# Rest Server [![Status badge for CI tests](https://github.com/restic/rest-server/workflows/test/badge.svg)](https://github.com/restic/rest-server/actions?query=workflow%3Atest) [![Go Report Card](https://goreportcard.com/badge/github.com/restic/rest-server)](https://goreportcard.com/report/github.com/restic/rest-server) [![GoDoc](https://godoc.org/github.com/restic/rest-server?status.svg)](https://godoc.org/github.com/restic/rest-server) [![License](https://img.shields.io/badge/license-BSD%20%282--Clause%29-003262.svg?maxAge=2592000)](https://github.com/restic/rest-server/blob/master/LICENSE) [![Powered by](https://img.shields.io/badge/powered_by-Go-5272b4.svg?maxAge=2592000)](https://golang.org/) Rest Server is a high performance HTTP server that implements restic's [REST backend API](https://restic.readthedocs.io/en/latest/100_references.html#rest-backend). It provides secure and efficient way to backup data remotely, using [restic](https://github.com/restic/restic) backup client via the [rest: URL](https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#rest-server). ## Requirements Rest Server requires Go 1.18 or higher to build. The only tested compiler is the official Go compiler. Building server with `gccgo` may work, but is not supported. The required version of restic backup client to use with `rest-server` is [v0.7.1](https://github.com/restic/restic/releases/tag/v0.7.1) or higher. ## Build For building the `rest-server` binary run `CGO_ENABLED=0 go build -o rest-server ./cmd/rest-server` ## Usage To learn how to use restic backup client with REST backend, please consult [restic manual](https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#rest-server). ```console $ rest-server --help Run a REST server for use with restic Usage: rest-server [flags] Flags: --append-only enable append only mode --cpu-profile string write CPU profile to file --debug output debug messages -h, --help help for rest-server --htpasswd-file string location of .htpasswd file (default: "/.htpasswd") --listen string listen address (default ":8000") --log filename write HTTP requests in the combined log format to the specified filename --max-size int the maximum size of the repository in bytes --no-auth disable .htpasswd authentication --no-verify-upload do not verify the integrity of uploaded data. DO NOT enable unless the rest-server runs on a very low-power device --path string data directory (default "/tmp/restic") --private-repos users can only access their private repo --prometheus enable Prometheus metrics --prometheus-no-auth disable auth for Prometheus /metrics endpoint --tls turn on TLS support --tls-cert string TLS certificate path --tls-key string TLS key path -v, --version version for rest-server ``` By default the server persists backup data in the OS temporary directory (`/tmp/restic` on Linux/BSD and others, in `%TEMP%\\restic` in Windows, etc). **If `rest-server` is launched using the default path, all backups will be lost**. To start the server with a custom persistence directory and with authentication disabled: ```sh rest-server --path /user/home/backup --no-auth ``` To authenticate users (for access to the rest-server), the server supports using a `.htpasswd` file to specify users. By default, the server looks for this file at the root of the persistence directory, but this can be changed using the `--htpasswd-file` option. You can create such a file by executing the following command (note that you need the `htpasswd` program from Apache's http-tools). In order to append new user to the file, just omit the `-c` argument. Only bcrypt and SHA encryption methods are supported, so use -B (very secure) or -s (insecure by today's standards) when adding/changing passwords. ```sh htpasswd -B -c .htpasswd username ``` If you want to disable authentication, you must add the `--no-auth` flag. If this flag is not specified and the `.htpasswd` cannot be opened, rest-server will refuse to start. NOTE: In older versions of rest-server (up to 0.9.7), this flag does not exist and the server disables authentication if `.htpasswd` is missing or cannot be opened. By default the server uses HTTP protocol. This is not very secure since with Basic Authentication, user name and passwords will be sent in clear text in every request. In order to enable TLS support just add the `--tls` argument and add a private and public key at the root of your persistence directory. You may also specify private and public keys by `--tls-cert` and `--tls-key`. Signed certificate is normally required by the restic backend, but if you just want to test the feature you can generate password-less unsigned keys with the following command: ```sh openssl req -newkey rsa:2048 -nodes -x509 -keyout private_key -out public_key -days 365 -addext "subjectAltName = IP:127.0.0.1,DNS:yourdomain.com" ``` Omit the `IP:127.0.0.1` if you don't need your server be accessed via SSH Tunnels. No need to change default values in the openssl dialog, hitting enter every time is sufficient. To access this server via restic use `--cacert public_key`, meaning with a self-signed certificate you have to distribute your `public_key` file to every restic client. The `--append-only` mode allows creation of new backups but prevents deletion and modification of existing backups. This can be useful when backing up systems that have a potential of being hacked. To prevent your users from accessing each others' repositories, you may use the `--private-repos` flag which grants access only when a subdirectory with the same name as the user is specified in the repository URL. For example, user "foo" using the repository URLs `rest:https://foo:pass@host:8000/foo` or `rest:https://foo:pass@host:8000/foo/` would be granted access, but the same user using repository URLs `rest:https://foo:pass@host:8000/` or `rest:https://foo:pass@host:8000/foobar/` would be denied access. Users can also create their own subrepositories, like `/foo/bar/`. Rest Server uses exactly the same directory structure as local backend, so you should be able to access it both locally and via HTTP, even simultaneously. ### Systemd There's an example [systemd service file](https://github.com/restic/rest-server/blob/master/examples/systemd/rest-server.service) included with the source, so you can get Rest Server up & running as a proper Systemd service in no time. Before installing, adapt paths and options to your environment. ### Docker Rest Server works well inside a container, images are [published to Docker Hub](https://hub.docker.com/r/restic/rest-server). #### Start server You can run the server with any container runtime, like Docker: ```sh docker pull restic/rest-server:latest docker run -p 8000:8000 -v /my/data:/data --name rest_server restic/rest-server ``` Note that: - **contrary to the defaults** of `rest-server`, the persistent data volume is located to `/data`. - By default, the image uses authentication. To turn it off, set environment variable `DISABLE_AUTHENTICATION` to any value. - By default, the image loads the `.htpasswd` file from the persistent data volume (i.e. from `/data/.htpasswd`). To change the location of this file, set the environment variable `PASSWORD_FILE` to the path of the `.htpasswd` file. Please note that this path must be accessible from inside the container and should be persisted. This is normally done by bind-mounting a path into the container or with another docker volume. - It's suggested to set a container name to more easily manage users (`--name` parameter to `docker run`). - You can set environment variable `OPTIONS` to any extra flags you'd like to pass to rest-server. #### Customize the image The [published image](https://hub.docker.com/r/restic/rest-server) is built from the `Dockerfile` available on this repository, which you may use as a basis for building your own customized images. ```sh git clone https://github.com/restic/rest-server.git cd rest-server docker build -t restic/rest-server:latest . ``` #### Manage users ##### Add user ```sh docker exec -it rest_server create_user myuser ``` or ```sh docker exec -it rest_server create_user myuser mypassword ``` ##### Delete user ```sh docker exec -it rest_server delete_user myuser ``` ## Prometheus support and Grafana dashboard The server can be started with `--prometheus` to expose [Prometheus](https://prometheus.io/) metrics at `/metrics`. If authentication is enabled, this endpoint requires authentication for the 'metrics' user, but this can be overridden with the `--prometheus-no-auth` flag. This repository contains an example full stack Docker Compose setup with a Grafana dashboard in [examples/compose-with-grafana/](examples/compose-with-grafana/). ## Why use Rest Server? Compared to the SFTP backend, the REST backend has better performance, especially so if you can skip additional crypto overhead by using plain HTTP transport (restic already properly encrypts all data it sends, so using HTTPS is mostly about authentication). But, even if you use HTTPS transport, the REST protocol should be faster and more scalable, due to some inefficiencies of the SFTP protocol (everything needs to be transferred in chunks of 32 KiB at most, each packet needs to be acknowledged by the server). One important safety feature that Rest Server adds is the optional ability to run in append-only mode. This prevents an attacker from wiping your server backups when access is gained to the server being backed up. Finally, the Rest Server implementation is really simple and as such could be used on the low-end devices, no problem. Also, in some cases, for example behind corporate firewalls, HTTP/S might be the only protocol allowed. Here too REST backend might be the perfect option for your backup needs. ## Contributors Contributors are welcome, just open a new issue / pull request. rest-server-0.13.0/Release.md000066400000000000000000000030771465071536600160020ustar00rootroot000000000000001. Export `$VERSION`: export VERSION=0.10.0 2. Add new version to file `VERSION` and `main.go` and commit the result: echo "${VERSION}" | tee VERSION sed -i "s/var version = \"[^\"]*\"/var version = \"${VERSION}\"/" cmd/rest-server/main.go git commit -m "Update VERSION files for ${VERSION}" VERSION cmd/rest-server/main.go 3. Move changelog files for `calens`: mv changelog/unreleased "changelog/${VERSION}_$(date +%Y-%m-%d)" rm -f "changelog/${VERSION}_$(date +%Y-%m-%d)/.gitkeep" git add "changelog/${VERSION}"* git rm -r changelog/unreleased mkdir changelog/unreleased touch changelog/unreleased/.gitkeep git add changelog/unreleased/.gitkeep git commit -m "Move changelog files for ${VERSION}" changelog/{unreleased,"${VERSION}"*} 4. Generate changelog: calens > CHANGELOG.md git add CHANGELOG.md git commit -m "Generate CHANGELOG.md for ${VERSION}" CHANGELOG.md 5. Tag new version and push the tag: git tag -a -s -m "v${VERSION}" "v${VERSION}" git push --tags 6. Build the project (use `--snapshot` for testing, or pass `--config` to use another config file): goreleaser \ release \ --release-notes <(calens --template changelog/CHANGELOG-GitHub.tmpl --version "${VERSION}") 7. Set a new version in `main.go` and commit the result: sed -i "s/var version = \"[^\"]*\"/var version = \"${VERSION}-dev\"/" cmd/rest-server/main.go git commit -m "Update version for development" cmd/rest-server/main.go rest-server-0.13.0/VERSION000066400000000000000000000000071465071536600151360ustar00rootroot000000000000000.13.0 rest-server-0.13.0/build.go000066400000000000000000000272011465071536600155210ustar00rootroot00000000000000// Description // // This program aims to make building Go programs for end users easier by just // calling it with `go run`, without having to setup a GOPATH. // // This program needs Go >= 1.12. It'll use Go modules for compilation. It // builds the package configured as Main in the Config struct. // BSD 2-Clause License // // Copyright (c) 2016-2018, Alexander Neumann // All rights reserved. // // This file has been derived from the repository at: // https://github.com/fd0/build-go // // 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. // // 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 HOLDER 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. //go:build ignore_build_go // +build ignore_build_go package main import ( "fmt" "io" "io/ioutil" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" ) // config contains the configuration for the program to build. var config = Config{ Name: "rest-server", // name of the program executable and directory Namespace: "github.com/restic/rest-server", // subdir of GOPATH, e.g. "github.com/foo/bar" Main: "github.com/restic/rest-server/cmd/rest-server", // package name for the main package Tests: []string{"./..."}, // tests to run MinVersion: GoVersion{Major: 1, Minor: 15, Patch: 0}, // minimum Go version supported } // Config configures the build. type Config struct { Name string Namespace string Main string DefaultBuildTags []string Tests []string MinVersion GoVersion } var ( verbose bool runTests bool enableCGO bool enablePIE bool goVersion = ParseGoVersion(runtime.Version()) ) // die prints the message with fmt.Fprintf() to stderr and exits with an error // code. func die(message string, args ...interface{}) { fmt.Fprintf(os.Stderr, message, args...) os.Exit(1) } func showUsage(output io.Writer) { fmt.Fprintf(output, "USAGE: go run build.go OPTIONS\n") fmt.Fprintf(output, "\n") fmt.Fprintf(output, "OPTIONS:\n") fmt.Fprintf(output, " -v --verbose output more messages\n") fmt.Fprintf(output, " -t --tags specify additional build tags\n") fmt.Fprintf(output, " -T --test run tests\n") fmt.Fprintf(output, " -o --output set output file name\n") fmt.Fprintf(output, " --enable-cgo use CGO to link against libc\n") fmt.Fprintf(output, " --enable-pie use PIE buildmode\n") fmt.Fprintf(output, " --goos value set GOOS for cross-compilation\n") fmt.Fprintf(output, " --goarch value set GOARCH for cross-compilation\n") fmt.Fprintf(output, " --goarm value set GOARM for cross-compilation\n") } func verbosePrintf(message string, args ...interface{}) { if !verbose { return } fmt.Printf("build: "+message, args...) } // printEnv prints Go-relevant environment variables in a nice way using verbosePrintf. func printEnv(env []string) { verbosePrintf("environment (GO*):\n") for _, v := range env { // ignore environment variables which do not start with GO*. if !strings.HasPrefix(v, "GO") { continue } verbosePrintf(" %s\n", v) } } // build runs "go build args..." with GOPATH set to gopath. func build(cwd string, env map[string]string, args ...string) error { // -trimpath removes all absolute paths from the binary. a := []string{"build", "-trimpath"} if enablePIE { a = append(a, "-buildmode=pie") } a = append(a, args...) cmd := exec.Command("go", a...) cmd.Env = os.Environ() for k, v := range env { cmd.Env = append(cmd.Env, k+"="+v) } if !enableCGO { cmd.Env = append(cmd.Env, "CGO_ENABLED=0") } printEnv(cmd.Env) cmd.Dir = cwd cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr verbosePrintf("chdir %q\n", cwd) verbosePrintf("go %q\n", a) return cmd.Run() } // test runs "go test args..." with GOPATH set to gopath. func test(cwd string, env map[string]string, args ...string) error { args = append([]string{"test", "-count", "1"}, args...) cmd := exec.Command("go", args...) cmd.Env = os.Environ() for k, v := range env { cmd.Env = append(cmd.Env, k+"="+v) } if !enableCGO { cmd.Env = append(cmd.Env, "CGO_ENABLED=0") } cmd.Dir = cwd cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr printEnv(cmd.Env) verbosePrintf("chdir %q\n", cwd) verbosePrintf("go %q\n", args) return cmd.Run() } // getVersion returns the version string from the file VERSION in the current // directory. func getVersionFromFile() string { buf, err := ioutil.ReadFile("VERSION") if err != nil { verbosePrintf("error reading file VERSION: %v\n", err) return "" } return strings.TrimSpace(string(buf)) } // getVersion returns a version string which is a combination of the contents // of the file VERSION in the current directory and the version from git (if // available). func getVersion() string { versionFile := getVersionFromFile() versionGit := getVersionFromGit() verbosePrintf("version from file 'VERSION' is %q, version from git %q\n", versionFile, versionGit) switch { case versionFile == "": return versionGit case versionGit == "": return versionFile } return fmt.Sprintf("%s (%s)", versionFile, versionGit) } // getVersionFromGit returns a version string that identifies the currently // checked out git commit. func getVersionFromGit() string { cmd := exec.Command("git", "describe", "--long", "--tags", "--dirty", "--always") out, err := cmd.Output() if err != nil { verbosePrintf("git describe returned error: %v\n", err) return "" } version := strings.TrimSpace(string(out)) verbosePrintf("git version is %s\n", version) return version } // Constants represents a set of constants that are set in the final binary to // the given value via compiler flags. type Constants map[string]string // LDFlags returns the string that can be passed to go build's `-ldflags`. func (cs Constants) LDFlags() string { l := make([]string, 0, len(cs)) for k, v := range cs { l = append(l, fmt.Sprintf(`-X "%s=%s"`, k, v)) } return strings.Join(l, " ") } // GoVersion is the version of Go used to compile the project. type GoVersion struct { Major int Minor int Patch int } // ParseGoVersion parses the Go version s. If s cannot be parsed, the returned GoVersion is null. func ParseGoVersion(s string) (v GoVersion) { if !strings.HasPrefix(s, "go") { return } s = s[2:] data := strings.Split(s, ".") if len(data) < 2 || len(data) > 3 { // invalid version return GoVersion{} } var err error v.Major, err = strconv.Atoi(data[0]) if err != nil { return GoVersion{} } // try to parse the minor version while removing an eventual suffix (like // "rc2" or so) for s := data[1]; s != ""; s = s[:len(s)-1] { v.Minor, err = strconv.Atoi(s) if err == nil { break } } if v.Minor == 0 { // no minor version found return GoVersion{} } if len(data) >= 3 { v.Patch, err = strconv.Atoi(data[2]) if err != nil { return GoVersion{} } } return } // AtLeast returns true if v is at least as new as other. If v is empty, true is returned. func (v GoVersion) AtLeast(other GoVersion) bool { var empty GoVersion // the empty version satisfies all versions if v == empty { return true } if v.Major < other.Major { return false } if v.Minor < other.Minor { return false } if v.Patch < other.Patch { return false } return true } func (v GoVersion) String() string { return fmt.Sprintf("Go %d.%d.%d", v.Major, v.Minor, v.Patch) } func main() { if !goVersion.AtLeast(GoVersion{1, 12, 0}) { die("Go version (%v) is too old, restic requires Go >= 1.12\n", goVersion) } if !goVersion.AtLeast(config.MinVersion) { fmt.Fprintf(os.Stderr, "%s detected, this program requires at least %s\n", goVersion, config.MinVersion) os.Exit(1) } buildTags := config.DefaultBuildTags skipNext := false params := os.Args[1:] env := map[string]string{ "GO111MODULE": "on", // make sure we build in Module mode "GOOS": runtime.GOOS, "GOARCH": runtime.GOARCH, "GOARM": "", } var outputFilename string for i, arg := range params { if skipNext { skipNext = false continue } switch arg { case "-v", "--verbose": verbose = true case "-t", "-tags", "--tags": if i+1 >= len(params) { die("-t given but no tag specified") } skipNext = true buildTags = append(buildTags, strings.Split(params[i+1], " ")...) case "-o", "--output": skipNext = true outputFilename = params[i+1] case "-T", "--test": runTests = true case "--enable-cgo": enableCGO = true case "--enable-pie": enablePIE = true case "--goos": skipNext = true env["GOOS"] = params[i+1] case "--goarch": skipNext = true env["GOARCH"] = params[i+1] case "--goarm": skipNext = true env["GOARM"] = params[i+1] case "-h": showUsage(os.Stdout) return default: fmt.Fprintf(os.Stderr, "Error: unknown option %q\n\n", arg) showUsage(os.Stderr) os.Exit(1) } } verbosePrintf("detected Go version %v\n", goVersion) preserveSymbols := false for i := range buildTags { buildTags[i] = strings.TrimSpace(buildTags[i]) if buildTags[i] == "debug" || buildTags[i] == "profile" { preserveSymbols = true } } verbosePrintf("build tags: %s\n", buildTags) root, err := os.Getwd() if err != nil { die("Getwd(): %v\n", err) } if outputFilename == "" { outputFilename = config.Name if env["GOOS"] == "windows" { outputFilename += ".exe" } } output := outputFilename if !filepath.IsAbs(output) { output = filepath.Join(root, output) } version := getVersion() constants := Constants{} if version != "" { constants["main.version"] = version } ldflags := constants.LDFlags() if !preserveSymbols { // Strip debug symbols. ldflags = "-s -w " + ldflags } verbosePrintf("ldflags: %s\n", ldflags) var ( buildArgs []string testArgs []string ) mainPackage := config.Main if strings.HasPrefix(mainPackage, config.Namespace) { mainPackage = strings.Replace(mainPackage, config.Namespace, "./", 1) } buildTarget := filepath.FromSlash(mainPackage) buildCWD, err := os.Getwd() if err != nil { die("unable to determine current working directory: %v\n", err) } buildArgs = append(buildArgs, "-tags", strings.Join(buildTags, " "), "-ldflags", ldflags, "-o", output, buildTarget, ) err = build(buildCWD, env, buildArgs...) if err != nil { die("build failed: %v\n", err) } if runTests { verbosePrintf("running tests\n") testArgs = append(testArgs, config.Tests...) err = test(buildCWD, env, testArgs...) if err != nil { die("running tests failed: %v\n", err) } } } rest-server-0.13.0/changelog/000077500000000000000000000000001465071536600160205ustar00rootroot00000000000000rest-server-0.13.0/changelog/0.10.0_2020-09-13/000077500000000000000000000000001465071536600176465ustar00rootroot00000000000000rest-server-0.13.0/changelog/0.10.0_2020-09-13/issue-102000066400000000000000000000012171465071536600212220ustar00rootroot00000000000000Change: Remove vendored dependencies We've removed the vendored dependencies (in the subdir `vendor/`) similar to what we did for `restic` itself. When building restic, the Go compiler automatically fetches the dependencies. It will also cryptographically verify that the correct code has been fetched by using the hashes in `go.sum` (see the link to the documentation below). Building the rest-server now requires Go 1.11 or newer, since we're using Go Modules for dependency management. Older Go versions are not supported any more. https://github.com/restic/rest-server/issues/102 https://golang.org/cmd/go/#hdr-Module_downloading_and_verification rest-server-0.13.0/changelog/0.10.0_2020-09-13/issue-117000066400000000000000000000020371465071536600212310ustar00rootroot00000000000000Security: Stricter path sanitization The framework we're using in rest-server to decode paths to repositories allowed specifying URL-encoded characters in paths, including sensitive characters such as `/` (encoded as `%2F`). We've changed this unintended behavior, such that rest-server now rejects such paths. In particular, it is no longer possible to specify sub-repositories for users by encoding the path with `%2F`, such as `http://localhost:8000/foo%2Fbar`, which means that this will unfortunately be a breaking change in that case. If using sub-repositories for users is important to you, please let us know in the forum, so we can learn about your use case and implement this properly. As it currently stands, the ability to use sub-repositories was an unintentional feature made possible by the URL decoding framework used, and hence never meant to be supported in the first place. If we wish to have this feature in rest-server, we'd like to have it implemented properly and intentionally. https://github.com/restic/rest-server/issues/117 rest-server-0.13.0/changelog/0.10.0_2020-09-13/issue-44000066400000000000000000000001771465071536600211530ustar00rootroot00000000000000Enhancement: Add changelog file https://github.com/restic/rest-server/issues/44 https://github.com/restic/rest-server/pull/62 rest-server-0.13.0/changelog/0.10.0_2020-09-13/issue-60000066400000000000000000000005751465071536600211530ustar00rootroot00000000000000Security: Require auth by default, add --no-auth flag In order to prevent users from accidentally exposing rest-server without authentication, rest-server now defaults to requiring a .htpasswd. If you want to disable authentication, you need to explicitly pass the new --no-auth flag. https://github.com/restic/rest-server/issues/60 https://github.com/restic/rest-server/pull/61 rest-server-0.13.0/changelog/0.10.0_2020-09-13/pull-64000066400000000000000000000005361465071536600210000ustar00rootroot00000000000000Security: Refuse overwriting config file in append-only mode While working on the `rclone serve restic` command we noticed that is currently possible to overwrite the config file in a repo even if `--append-only` is specified. The first commit adds proper tests, and the second commit fixes the issue. https://github.com/restic/rest-server/pull/64 rest-server-0.13.0/changelog/0.11.0_2022-02-10/000077500000000000000000000000001465071536600176375ustar00rootroot00000000000000rest-server-0.13.0/changelog/0.11.0_2022-02-10/issue-119000066400000000000000000000006231465071536600212230ustar00rootroot00000000000000Bugfix: Fix Docker configuration for `DISABLE_AUTHENTICATION` rest-server 0.10.0 introduced a regression which caused the `DISABLE_AUTHENTICATION` environment variable to stop working for the Docker container. This has been fixed by automatically setting the option `--no-auth` to disable authentication. https://github.com/restic/rest-server/issues/119 https://github.com/restic/rest-server/pull/124 rest-server-0.13.0/changelog/0.11.0_2022-02-10/issue-122000066400000000000000000000006611465071536600212170ustar00rootroot00000000000000Enhancement: Verify uploaded files The rest-server now by default verifies that the hash of content of uploaded files matches their filename. This ensures that transmission errors are detected and forces restic to retry the upload. On low-power devices it can make sense to disable this check by passing the `--no-verify-upload` flag. https://github.com/restic/rest-server/issues/122 https://github.com/restic/rest-server/pull/130 rest-server-0.13.0/changelog/0.11.0_2022-02-10/issue-126000066400000000000000000000004761465071536600212270ustar00rootroot00000000000000Enhancement: Allow running rest-server via systemd socket activation We've added the option to have systemd create the listening socket and start the rest-server on demand. https://github.com/restic/rest-server/issues/126 https://github.com/restic/rest-server/pull/151 https://github.com/restic/rest-server/pull/127 rest-server-0.13.0/changelog/0.11.0_2022-02-10/issue-131000066400000000000000000000011561465071536600212170ustar00rootroot00000000000000Security: Prevent loading of usernames containing a slash "/" is valid char in HTTP authorization headers, but is also used in rest-server to map usernames to private repos. This commit prevents loading maliciously composed usernames like "/foo/config" by restricting the allowed characters to the unicode character class, numbers, "-", "." and "@". This prevents requests to other users files like: curl -v -X DELETE -u foo/config:attack http://localhost:8000/foo/config https://github.com/restic/rest-server/issues/131 https://github.com/restic/rest-server/pull/132 https://github.com/restic/rest-server/pull/137 rest-server-0.13.0/changelog/0.11.0_2022-02-10/issue-146000066400000000000000000000006421465071536600212240ustar00rootroot00000000000000Change: Build rest-server at docker container build time The Dockerfile now includes a build stage such that the latest rest-server is always built and packaged. This is done in a standard golang container to ensure a clean build environment and only the final binary is shipped rather than the whole build environment. https://github.com/restic/rest-server/issues/146 https://github.com/restic/rest-server/pull/145 rest-server-0.13.0/changelog/0.11.0_2022-02-10/issue-148000066400000000000000000000005771465071536600212350ustar00rootroot00000000000000Enhancement: Expand use of security features in example systemd unit file The example systemd unit file now enables additional systemd features to mitigate potential security vulnerabilities in rest-server and the various packages and operating system components which it relies upon. https://github.com/restic/rest-server/issues/148 https://github.com/restic/rest-server/pull/149 rest-server-0.13.0/changelog/0.11.0_2022-02-10/pull-112000066400000000000000000000015361465071536600210440ustar00rootroot00000000000000Change: Add subrepo support and refactor server code Support for multi-level repositories has been added, so now each user can have its own subrepositories. This feature is always enabled. Authentication for the Prometheus /metrics endpoint can now be disabled with the new `--prometheus-no-auth` flag. We have split out all HTTP handling to a separate `repo` subpackage to cleanly separate the server code from the code that handles a single repository. The new RepoHandler also makes it easier to reuse rest-server as a Go component in any other HTTP server. The refactoring makes the code significantly easier to follow and understand, which in turn makes it easier to add new features, audit for security and debug issues. https://github.com/restic/restic/pull/112 https://github.com/restic/restic/issues/109 https://github.com/restic/restic/issues/107 rest-server-0.13.0/changelog/0.11.0_2022-02-10/pull-142000066400000000000000000000014551465071536600210470ustar00rootroot00000000000000Bugfix: Fix possible data loss due to interrupted network connections When rest-server was run without `--append-only` it was possible to lose uploaded files in a specific scenario in which a network connection was interrupted. For the data loss to occur a file upload by restic would have to be interrupted such that restic notices the interrupted network connection before the rest-server. Then restic would have to retry the file upload and finish it before the rest-server notices that the initial upload has failed. Then the uploaded file would be accidentally removed by rest-server when trying to cleanup the failed upload. This has been fixed by always uploading to a temporary file first which is moved in position only once it was uploaded completely. https://github.com/restic/rest-server/pull/142 rest-server-0.13.0/changelog/0.11.0_2022-02-10/pull-158000066400000000000000000000005321465071536600210510ustar00rootroot00000000000000Bugfix: Use platform-specific temporary directory as default data directory If no data directory is specificed, then rest-server now uses the Go standard library functions to retrieve the standard temporary directory path for the current platform. https://github.com/restic/rest-server/issues/157 https://github.com/restic/rest-server/pull/158 rest-server-0.13.0/changelog/0.11.0_2022-02-10/pull-160000066400000000000000000000011511465071536600210400ustar00rootroot00000000000000Bugfix: Reply "insufficient storage" on disk full or over-quota When there was no space left on disk, or any other write-related error occurred, rest-server replied with HTTP status code 400 (Bad request). This is misleading (restic client will dump the status code to the user). rest-server now replies with two different status codes in these situations: * HTTP 507 "Insufficient storage" is the status on disk full or repository over-quota * HTTP 500 "Internal server error" is used for other disk-related errors https://github.com/restic/rest-server/issues/155 https://github.com/restic/rest-server/pull/160 rest-server-0.13.0/changelog/0.12.0_2023-04-24/000077500000000000000000000000001465071536600176505ustar00rootroot00000000000000rest-server-0.13.0/changelog/0.12.0_2023-04-24/issue-133000066400000000000000000000007131465071536600212300ustar00rootroot00000000000000Enhancement: Cache basic authentication credentials To speed up the verification of basic auth credentials, rest-server now caches passwords for a minute in memory. That way the expensive verification of basic auth credentials can be skipped for most requests issued by a single restic run. The password is kept in memory in a hashed form and not as plaintext. https://github.com/restic/rest-server/issues/133 https://github.com/restic/rest-server/pull/138 rest-server-0.13.0/changelog/0.12.0_2023-04-24/issue-182000066400000000000000000000005441465071536600212360ustar00rootroot00000000000000Bugfix: Allow usernames containing underscore and more The security fix in rest-server 0.11.0 (#131) disallowed usernames containing and underscore "_". The list of allowed characters has now been changed to include Unicode characters, numbers, "_", "-", "." and "@". https://github.com/restic/restic/issues/183 https://github.com/restic/restic/pull/184 rest-server-0.13.0/changelog/0.12.0_2023-04-24/issue-187000066400000000000000000000003751465071536600212450ustar00rootroot00000000000000Enhancement: Allow configurable location for `.htpasswd` file It is now possible to specify the location of the `.htpasswd` file using the `--htpasswd-file` option. https://github.com/restic/restic/issues/187 https://github.com/restic/restic/pull/188 rest-server-0.13.0/changelog/0.12.0_2023-04-24/issue-219000066400000000000000000000006171465071536600212400ustar00rootroot00000000000000Bugfix: Ignore unexpected files in the data/ folder If the data folder of a repository contained files, this would prevent restic from retrieving a list of file data files. This has been fixed. As a workaround remove the files that are directly contained in the data folder (e.g., `.DS_Store` files). https://github.com/restic/rest-server/issues/219 https://github.com/restic/rest-server/pull/221 rest-server-0.13.0/changelog/0.12.0_2023-04-24/pull-194000066400000000000000000000012671465071536600210700ustar00rootroot00000000000000Bugfix: Return 500 "Internal server error" if files cannot be read When files in a repository cannot be read by rest-server, for example after running `restic prune` directly on the server hosting the repositories in a way that causes filesystem permissions to be wrong, rest-server previously returned 404 "Not Found" as status code. This was causing confusing for users. The error handling has now been fixed to only return 404 "Not Found" if the file actually does not exist. Otherwise a 500 "Internal server error" is reported to the client and the underlying error is logged at the server side. https://github.com/restic/restic/issues/1871 https://github.com/restic/rest-server/pull/195 rest-server-0.13.0/changelog/0.12.0_2023-04-24/pull-207000066400000000000000000000005631465071536600210610ustar00rootroot00000000000000Change: Return error if command-line arguments are specified Command line arguments are ignored by rest-server, but there was previously no indication of this when they were supplied anyway. To prevent usage errors an error is now printed when command line arguments are supplied, instead of them being silently ignored. https://github.com/restic/rest-server/pull/207 rest-server-0.13.0/changelog/0.12.0_2023-04-24/pull-208000066400000000000000000000004651465071536600210630ustar00rootroot00000000000000Change: Update dependencies and require Go 1.17 or newer Most dependencies have been updated. Since some libraries require newer language features, support for Go 1.15-1.16 has been dropped, which means that rest-server now requires at least Go 1.17 to build. https://github.com/restic/rest-server/pull/208 rest-server-0.13.0/changelog/0.12.1_2023-07-09/000077500000000000000000000000001465071536600176575ustar00rootroot00000000000000rest-server-0.13.0/changelog/0.12.1_2023-07-09/issue-230000066400000000000000000000006551465071536600212420ustar00rootroot00000000000000Bugfix: Fix erroneous warnings about unsupported fsync Due to a regression in rest-server 0.12.0, it continuously printed `WARNING: fsync is not supported by the data storage. This can lead to data loss, if the system crashes or the storage is unexpectedly disconnected.` for systems that support fsync. We have fixed the warning. https://github.com/restic/rest-server/issues/230 https://github.com/restic/rest-server/pull/231 rest-server-0.13.0/changelog/0.12.1_2023-07-09/issue-238000066400000000000000000000005501465071536600212440ustar00rootroot00000000000000Bugfix: API: Return empty array when listing empty folders Rest-server returned `null` when listing an empty folder. This has been changed to returning an empty array in accordance with the REST protocol specification. This change has no impact on restic users. https://github.com/restic/rest-server/issues/238 https://github.com/restic/rest-server/pull/239 rest-server-0.13.0/changelog/0.12.1_2023-07-09/pull-217000066400000000000000000000007041465071536600210660ustar00rootroot00000000000000Enhancement: Log to stdout using the `--log -` option Logging to stdout was possible using `--log /dev/stdout`. However, when the rest server is run as a different user, for example, using `sudo -u restic rest-server [...] --log /dev/stdout` this did not work due to permission issues. For logging to stdout, the `--log` option now supports the special filename `-` which also works in these cases. https://github.com/restic/rest-server/pull/217 rest-server-0.13.0/changelog/0.13.0_2024-07-26/000077500000000000000000000000001465071536600176575ustar00rootroot00000000000000rest-server-0.13.0/changelog/0.13.0_2024-07-26/pull-267000066400000000000000000000004601465071536600210720ustar00rootroot00000000000000Change: Update dependencies and require Go 1.18 or newer Most dependencies have been updated. Since some libraries require newer language features, support for Go 1.17 has been dropped, which means that rest-server now requires at least Go 1.18 to build. https://github.com/restic/rest-server/pull/267 rest-server-0.13.0/changelog/0.13.0_2024-07-26/pull-271000066400000000000000000000005671465071536600210750ustar00rootroot00000000000000Enhancement: Print listening address after start-up When started with `--listen :0`, rest-server would print `start server on :0` The message now also includes the actual address listened on, for example `start server on 0.0.0.0:37333`. This is useful when starting a server with an auto-allocated free port number (port 0). https://github.com/restic/rest-server/pull/271 rest-server-0.13.0/changelog/0.13.0_2024-07-26/pull-272000066400000000000000000000010151465071536600210630ustar00rootroot00000000000000Enhancement: Support listening on a unix socket It is now possible to make rest-server listen on a unix socket by prefixing the socket filename with `unix:` and passing it to the `--listen` option, for example `--listen unix:/tmp/foo`. This is useful in combination with remote port forwarding to enable a remote server to backup locally, e.g.: ``` rest-server --listen unix:/tmp/foo & ssh -R /tmp/foo:/tmp/foo user@host restic -r rest:http+unix:///tmp/foo:/repo backup ``` https://github.com/restic/rest-server/pull/272 rest-server-0.13.0/changelog/0.13.0_2024-07-26/pull-273000066400000000000000000000005501465071536600210670ustar00rootroot00000000000000Change: Shut down cleanly on TERM and INT signals Rest-server now listens for TERM and INT signals and cleanly closes down the http.Server and listener when receiving either of them. This is particularly useful when listening on a unix socket, as the server will now remove the socket file when it shuts down. https://github.com/restic/rest-server/pull/273 rest-server-0.13.0/changelog/CHANGELOG-GitHub.tmpl000066400000000000000000000016531465071536600213720ustar00rootroot00000000000000{{- range $changes := . }}{{ with $changes -}} Changelog for rest-server {{ .Version }} ({{ .Date }}) ========================================= The following sections list the changes in rest-server {{ .Version }} relevant to users. The changes are ordered by importance. Summary ------- {{ range $entry := .Entries }}{{ with $entry }} * {{ .TypeShort }} [#{{ .PrimaryID }}]({{ .PrimaryURL }}): {{ .Title }} {{- end }}{{ end }} Details ------- {{ range $entry := .Entries }}{{ with $entry }} * {{ .Type }} #{{ .PrimaryID }}: {{ .Title }} {{ range $par := .Paragraphs }} {{ $par }} {{ end }} {{ range $id := .Issues -}} {{ ` ` }}[#{{ $id }}](https://github.com/restic/rest-server/issues/{{ $id -}}) {{- end -}} {{ range $id := .PRs -}} {{ ` ` }}[#{{ $id }}](https://github.com/restic/rest-server/pull/{{ $id -}}) {{- end -}} {{ ` ` }}{{ range $url := .OtherURLs -}} {{ $url -}} {{- end }} {{ end }}{{ end }} {{ end }}{{ end -}} rest-server-0.13.0/changelog/CHANGELOG.tmpl000066400000000000000000000015631465071536600202120ustar00rootroot00000000000000{{- range $changes := . }}{{ with $changes -}} Changelog for rest-server {{ .Version }} ({{ .Date }}) ============================================ The following sections list the changes in rest-server {{ .Version }} relevant to users. The changes are ordered by importance. Summary ------- {{ range $entry := .Entries }}{{ with $entry }} * {{ .TypeShort }} #{{ .PrimaryID }}: {{ .Title }} {{- end }}{{ end }} Details ------- {{ range $entry := .Entries }}{{ with $entry }} * {{ .Type }} #{{ .PrimaryID }}: {{ .Title }} {{ range $par := .Paragraphs }} {{ wrapIndent $par 80 3 }} {{ end -}} {{ range $id := .Issues }} https://github.com/restic/rest-server/issues/{{ $id -}} {{ end -}} {{ range $id := .PRs }} https://github.com/restic/rest-server/pull/{{ $id -}} {{ end -}} {{ range $url := .OtherURLs }} {{ $url -}} {{ end }} {{ end }}{{ end }} {{ end }}{{ end -}} rest-server-0.13.0/changelog/TEMPLATE000066400000000000000000000015731465071536600171640ustar00rootroot00000000000000# The first line must start with Bugfix:, Enhancement: or Change:, # including the colon. Use present tense. Remove lines starting with '#' # from this template. Bugfix: Fix behavior for foobar (in present tense) # Describe the problem in the past tense, the new behavior in the present # tense. Mention the affected commands, backends, operating systems, etc. # Focus on user-facing behavior, not the implementation. We've fixed the behavior for foobar, a long-standing annoyance for rest-server users. # The last section is a list of issue, PR and forum URLs. # The first issue ID determines the filename for the changelog entry: # changelog/unreleased/issue-1234. If there are no relevant issue links, # use the PR ID and call the file pull-55555. https://github.com/restic/rest-server/issues/1234 https://github.com/restic/rest-server/pull/55555 https://forum.restic.net/foo/bar/baz rest-server-0.13.0/changelog/unreleased/000077500000000000000000000000001465071536600201475ustar00rootroot00000000000000rest-server-0.13.0/changelog/unreleased/.gitkeep000066400000000000000000000000001465071536600215660ustar00rootroot00000000000000rest-server-0.13.0/cmd/000077500000000000000000000000001465071536600146345ustar00rootroot00000000000000rest-server-0.13.0/cmd/rest-server/000077500000000000000000000000001465071536600171155ustar00rootroot00000000000000rest-server-0.13.0/cmd/rest-server/listener_unix.go000066400000000000000000000030231465071536600223320ustar00rootroot00000000000000//go:build !windows // +build !windows package main import ( "fmt" "log" "net" "strings" "github.com/coreos/go-systemd/v22/activation" ) // findListener tries to find a listener via systemd socket activation. If that // fails, it tries to create a listener on addr. func findListener(addr string) (listener net.Listener, err error) { // try systemd socket activation listeners, err := activation.Listeners() if err != nil { panic(err) } switch len(listeners) { case 0: // no listeners found, listen manually if strings.HasPrefix(addr, "unix:") { // if we want to listen on a unix socket unixAddr, err := net.ResolveUnixAddr("unix", strings.TrimPrefix(addr, "unix:")) if err != nil { return nil, fmt.Errorf("unable to understand unix address %s: %w", addr, err) } listener, err = net.ListenUnix("unix", unixAddr) if err != nil { return nil, fmt.Errorf("listen on %v failed: %w", addr, err) } } else { // assume tcp listener, err = net.Listen("tcp", addr) if err != nil { return nil, fmt.Errorf("listen on %v failed: %w", addr, err) } } log.Printf("start server on %v", listener.Addr()) return listener, nil case 1: // one listener supplied by systemd, use that one // // for testing, run rest-server with systemd-socket-activate as follows: // // systemd-socket-activate -l 8080 ./rest-server log.Printf("systemd socket activation mode") return listeners[0], nil default: return nil, fmt.Errorf("got %d listeners from systemd, expected one", len(listeners)) } } rest-server-0.13.0/cmd/rest-server/listener_unix_test.go000066400000000000000000000035451465071536600234020ustar00rootroot00000000000000//go:build !windows // +build !windows package main import ( "context" "fmt" "net" "net/http" "os" "path/filepath" "testing" "time" ) func TestUnixSocket(t *testing.T) { td := t.TempDir() // this is the socket we'll listen on and connect to tempSocket := filepath.Join(td, "sock") // create some content and parent dirs if err := os.MkdirAll(filepath.Join(td, "data", "repo1"), 0700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(td, "data", "repo1", "config"), []byte("foo"), 0700); err != nil { t.Fatal(err) } // run the following twice, to test that the server will // cleanup its socket file when quitting, which won't happen // if it doesn't exit gracefully for i := 0; i < 2; i++ { err := testServerWithArgs([]string{ "--no-auth", "--path", filepath.Join(td, "data"), "--listen", fmt.Sprintf("unix:%s", tempSocket), }, time.Second, func(ctx context.Context, _ *restServerApp) error { // custom client that will talk HTTP to unix socket client := http.Client{ Transport: &http.Transport{ DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { return net.Dial("unix", tempSocket) }, }, } for _, test := range []struct { Path string StatusCode int }{ {"/repo1/", http.StatusMethodNotAllowed}, {"/repo1/config", http.StatusOK}, {"/repo2/config", http.StatusNotFound}, } { req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://ignored"+test.Path, nil) if err != nil { return err } resp, err := client.Do(req) if err != nil { return err } resp.Body.Close() if resp.StatusCode != test.StatusCode { return fmt.Errorf("expected %d from server, instead got %d (path %s)", test.StatusCode, resp.StatusCode, test.Path) } } return nil }) if err != nil { t.Fatal(err) } } } rest-server-0.13.0/cmd/rest-server/listener_windows.go000066400000000000000000000005631465071536600230470ustar00rootroot00000000000000package main import ( "fmt" "log" "net" ) // findListener creates a listener. func findListener(addr string) (listener net.Listener, err error) { // listen manually listener, err = net.Listen("tcp", addr) if err != nil { return nil, fmt.Errorf("listen on %v failed: %w", addr, err) } log.Printf("start server on %v", listener.Addr()) return listener, nil } rest-server-0.13.0/cmd/rest-server/main.go000066400000000000000000000137231465071536600203760ustar00rootroot00000000000000package main import ( "context" "errors" "fmt" "log" "net" "net/http" "os" "os/signal" "path/filepath" "runtime" "runtime/pprof" "sync" "syscall" restserver "github.com/restic/rest-server" "github.com/spf13/cobra" ) type restServerApp struct { CmdRoot *cobra.Command Server restserver.Server CpuProfile string listenerAddressMu sync.Mutex listenerAddress net.Addr // set after startup } // cmdRoot is the base command when no other command has been specified. func newRestServerApp() *restServerApp { rv := &restServerApp{ CmdRoot: &cobra.Command{ Use: "rest-server", Short: "Run a REST server for use with restic", SilenceErrors: true, SilenceUsage: true, Args: func(cmd *cobra.Command, args []string) error { if len(args) != 0 { return fmt.Errorf("rest-server expects no arguments - unknown argument: %s", args[0]) } return nil }, Version: fmt.Sprintf("rest-server %s compiled with %v on %v/%v\n", version, runtime.Version(), runtime.GOOS, runtime.GOARCH), }, Server: restserver.Server{ Path: filepath.Join(os.TempDir(), "restic"), Listen: ":8000", }, } rv.CmdRoot.RunE = rv.runRoot flags := rv.CmdRoot.Flags() flags.StringVar(&rv.CpuProfile, "cpu-profile", rv.CpuProfile, "write CPU profile to file") flags.BoolVar(&rv.Server.Debug, "debug", rv.Server.Debug, "output debug messages") flags.StringVar(&rv.Server.Listen, "listen", rv.Server.Listen, "listen address") flags.StringVar(&rv.Server.Log, "log", rv.Server.Log, "write HTTP requests in the combined log format to the specified `filename` (use \"-\" for logging to stdout)") flags.Int64Var(&rv.Server.MaxRepoSize, "max-size", rv.Server.MaxRepoSize, "the maximum size of the repository in bytes") flags.StringVar(&rv.Server.Path, "path", rv.Server.Path, "data directory") flags.BoolVar(&rv.Server.TLS, "tls", rv.Server.TLS, "turn on TLS support") flags.StringVar(&rv.Server.TLSCert, "tls-cert", rv.Server.TLSCert, "TLS certificate path") flags.StringVar(&rv.Server.TLSKey, "tls-key", rv.Server.TLSKey, "TLS key path") flags.BoolVar(&rv.Server.NoAuth, "no-auth", rv.Server.NoAuth, "disable .htpasswd authentication") flags.StringVar(&rv.Server.HtpasswdPath, "htpasswd-file", rv.Server.HtpasswdPath, "location of .htpasswd file (default: \"/.htpasswd)\"") flags.BoolVar(&rv.Server.NoVerifyUpload, "no-verify-upload", rv.Server.NoVerifyUpload, "do not verify the integrity of uploaded data. DO NOT enable unless the rest-server runs on a very low-power device") flags.BoolVar(&rv.Server.AppendOnly, "append-only", rv.Server.AppendOnly, "enable append only mode") flags.BoolVar(&rv.Server.PrivateRepos, "private-repos", rv.Server.PrivateRepos, "users can only access their private repo") flags.BoolVar(&rv.Server.Prometheus, "prometheus", rv.Server.Prometheus, "enable Prometheus metrics") flags.BoolVar(&rv.Server.PrometheusNoAuth, "prometheus-no-auth", rv.Server.PrometheusNoAuth, "disable auth for Prometheus /metrics endpoint") return rv } var version = "0.13.0" func (app *restServerApp) tlsSettings() (bool, string, string, error) { var key, cert string if !app.Server.TLS && (app.Server.TLSKey != "" || app.Server.TLSCert != "") { return false, "", "", errors.New("requires enabled TLS") } else if !app.Server.TLS { return false, "", "", nil } if app.Server.TLSKey != "" { key = app.Server.TLSKey } else { key = filepath.Join(app.Server.Path, "private_key") } if app.Server.TLSCert != "" { cert = app.Server.TLSCert } else { cert = filepath.Join(app.Server.Path, "public_key") } return app.Server.TLS, key, cert, nil } // returns the address that the app is listening on. // returns nil if the application hasn't finished starting yet func (app *restServerApp) ListenerAddress() net.Addr { app.listenerAddressMu.Lock() defer app.listenerAddressMu.Unlock() return app.listenerAddress } func (app *restServerApp) runRoot(cmd *cobra.Command, args []string) error { log.SetFlags(0) log.Printf("Data directory: %s", app.Server.Path) if app.CpuProfile != "" { f, err := os.Create(app.CpuProfile) if err != nil { return err } defer f.Close() if err := pprof.StartCPUProfile(f); err != nil { return err } defer pprof.StopCPUProfile() log.Println("CPU profiling enabled") defer log.Println("Stopped CPU profiling") } if app.Server.NoAuth { log.Println("Authentication disabled") } else { log.Println("Authentication enabled") } handler, err := restserver.NewHandler(&app.Server) if err != nil { log.Fatalf("error: %v", err) } if app.Server.PrivateRepos { log.Println("Private repositories enabled") } else { log.Println("Private repositories disabled") } enabledTLS, privateKey, publicKey, err := app.tlsSettings() if err != nil { return err } listener, err := findListener(app.Server.Listen) if err != nil { return fmt.Errorf("unable to listen: %w", err) } // set listener address, this is useful for tests app.listenerAddressMu.Lock() app.listenerAddress = listener.Addr() app.listenerAddressMu.Unlock() srv := &http.Server{ Handler: handler, } // run server in background go func() { if !enabledTLS { err = srv.Serve(listener) } else { log.Printf("TLS enabled, private key %s, pubkey %v", privateKey, publicKey) err = srv.ServeTLS(listener, publicKey, privateKey) } if err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("listen and serve returned err: %v", err) } }() // wait until done <-app.CmdRoot.Context().Done() // gracefully shutdown server if err := srv.Shutdown(context.Background()); err != nil { return fmt.Errorf("server shutdown returned an err: %w", err) } log.Println("shutdown cleanly") return nil } func main() { // create context to be notified on interrupt or term signal so that we can shutdown cleanly ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() if err := newRestServerApp().CmdRoot.ExecuteContext(ctx); err != nil { log.Fatalf("error: %v", err) } } rest-server-0.13.0/cmd/rest-server/main_test.go000066400000000000000000000165741465071536600214440ustar00rootroot00000000000000package main import ( "context" "errors" "fmt" "io/ioutil" "net/http" "net/url" "os" "path/filepath" "strings" "sync" "testing" "time" restserver "github.com/restic/rest-server" ) func TestTLSSettings(t *testing.T) { type expected struct { TLSKey string TLSCert string Error bool } type passed struct { Path string TLS bool TLSKey string TLSCert string } var tests = []struct { passed passed expected expected }{ {passed{TLS: false}, expected{"", "", false}}, {passed{TLS: true}, expected{ filepath.Join(os.TempDir(), "restic/private_key"), filepath.Join(os.TempDir(), "restic/public_key"), false, }}, {passed{ Path: os.TempDir(), TLS: true, }, expected{ filepath.Join(os.TempDir(), "private_key"), filepath.Join(os.TempDir(), "public_key"), false, }}, {passed{Path: os.TempDir(), TLS: true, TLSKey: "/etc/restic/key", TLSCert: "/etc/restic/cert"}, expected{"/etc/restic/key", "/etc/restic/cert", false}}, {passed{Path: os.TempDir(), TLS: false, TLSKey: "/etc/restic/key", TLSCert: "/etc/restic/cert"}, expected{"", "", true}}, {passed{Path: os.TempDir(), TLS: false, TLSKey: "/etc/restic/key"}, expected{"", "", true}}, {passed{Path: os.TempDir(), TLS: false, TLSCert: "/etc/restic/cert"}, expected{"", "", true}}, } for _, test := range tests { app := newRestServerApp() t.Run("", func(t *testing.T) { // defer func() { restserver.Server = defaultConfig }() if test.passed.Path != "" { app.Server.Path = test.passed.Path } app.Server.TLS = test.passed.TLS app.Server.TLSKey = test.passed.TLSKey app.Server.TLSCert = test.passed.TLSCert gotTLS, gotKey, gotCert, err := app.tlsSettings() if err != nil && !test.expected.Error { t.Fatalf("tls_settings returned err (%v)", err) } if test.expected.Error { if err == nil { t.Fatalf("Error not returned properly (%v)", test) } else { return } } if gotTLS != test.passed.TLS { t.Errorf("TLS enabled, want (%v), got (%v)", test.passed.TLS, gotTLS) } wantKey := test.expected.TLSKey if gotKey != wantKey { t.Errorf("wrong TLSPrivPath path, want (%v), got (%v)", wantKey, gotKey) } wantCert := test.expected.TLSCert if gotCert != wantCert { t.Errorf("wrong TLSCertPath path, want (%v), got (%v)", wantCert, gotCert) } }) } } func TestGetHandler(t *testing.T) { dir, err := ioutil.TempDir("", "rest-server-test") if err != nil { t.Fatal(err) } defer func() { err := os.Remove(dir) if err != nil { t.Fatal(err) } }() getHandler := restserver.NewHandler // With NoAuth = false and no .htpasswd _, err = getHandler(&restserver.Server{Path: dir}) if err == nil { t.Errorf("NoAuth=false: expected error, got nil") } // With NoAuth = true and no .htpasswd _, err = getHandler(&restserver.Server{NoAuth: true, Path: dir}) if err != nil { t.Errorf("NoAuth=true: expected no error, got %v", err) } // With NoAuth = false and custom .htpasswd htpFile, err := ioutil.TempFile(dir, "custom") if err != nil { t.Fatal(err) } defer func() { err := os.Remove(htpFile.Name()) if err != nil { t.Fatal(err) } }() _, err = getHandler(&restserver.Server{HtpasswdPath: htpFile.Name()}) if err != nil { t.Errorf("NoAuth=false with custom htpasswd: expected no error, got %v", err) } // Create .htpasswd htpasswd := filepath.Join(dir, ".htpasswd") err = ioutil.WriteFile(htpasswd, []byte(""), 0644) if err != nil { t.Fatal(err) } defer func() { err := os.Remove(htpasswd) if err != nil { t.Fatal(err) } }() // With NoAuth = false and with .htpasswd _, err = getHandler(&restserver.Server{Path: dir}) if err != nil { t.Errorf("NoAuth=false with .htpasswd: expected no error, got %v", err) } } // helper method to test the app. Starts app with passed arguments, // then will call the callback function which can make requests against // the application. If the callback function fails due to errors returned // by http.Do() (i.e. *url.Error), then it will be retried until successful, // or the passed timeout passes. func testServerWithArgs(args []string, timeout time.Duration, cb func(context.Context, *restServerApp) error) error { // create the app with passed args app := newRestServerApp() app.CmdRoot.SetArgs(args) // create context that will timeout ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() // wait group for our client and server tasks jobs := &sync.WaitGroup{} jobs.Add(2) // run the server, saving the error var serverErr error go func() { defer jobs.Done() defer cancel() // if the server is stopped, no point keep the client alive serverErr = app.CmdRoot.ExecuteContext(ctx) }() // run the client, saving the error var clientErr error go func() { defer jobs.Done() defer cancel() // once the client is done, stop the server var urlError *url.Error // execute in loop, as we will retry for network errors // (such as the server hasn't started yet) for { clientErr = cb(ctx, app) switch { case clientErr == nil: return // success, we're done case errors.As(clientErr, &urlError): // if a network error (url.Error), then wait and retry // as server may not be ready yet select { case <-time.After(time.Millisecond * 100): continue case <-ctx.Done(): // unless we run out of time first clientErr = context.Canceled return } default: return // other error type, we're done } } }() // wait for both to complete jobs.Wait() // report back if either failed if clientErr != nil || serverErr != nil { return fmt.Errorf("client or server error, client: %v, server: %v", clientErr, serverErr) } return nil } func TestHttpListen(t *testing.T) { td := t.TempDir() // create some content and parent dirs if err := os.MkdirAll(filepath.Join(td, "data", "repo1"), 0700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(td, "data", "repo1", "config"), []byte("foo"), 0700); err != nil { t.Fatal(err) } for _, args := range [][]string{ {"--no-auth", "--path", filepath.Join(td, "data"), "--listen", "127.0.0.1:0"}, // test emphemeral port {"--no-auth", "--path", filepath.Join(td, "data"), "--listen", "127.0.0.1:9000"}, // test "normal" port {"--no-auth", "--path", filepath.Join(td, "data"), "--listen", "127.0.0.1:9000"}, // test that server was shutdown cleanly and that we can re-use that port } { err := testServerWithArgs(args, time.Second*10, func(ctx context.Context, app *restServerApp) error { for _, test := range []struct { Path string StatusCode int }{ {"/repo1/", http.StatusMethodNotAllowed}, {"/repo1/config", http.StatusOK}, {"/repo2/config", http.StatusNotFound}, } { listenAddr := app.ListenerAddress() if listenAddr == nil { return &url.Error{} // return this type of err, as we know this will retry } port := strings.Split(listenAddr.String(), ":")[1] req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://localhost:%s%s", port, test.Path), nil) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if err != nil { return err } resp.Body.Close() if resp.StatusCode != test.StatusCode { return fmt.Errorf("expected %d from server, instead got %d (path %s)", test.StatusCode, resp.StatusCode, test.Path) } } return nil }) if err != nil { t.Fatal(err) } } } rest-server-0.13.0/docker/000077500000000000000000000000001465071536600153405ustar00rootroot00000000000000rest-server-0.13.0/docker/create_user000077500000000000000000000004641465071536600175730ustar00rootroot00000000000000#!/bin/sh if [ -z "$1" ]; then echo "create_user [username]" echo "or" echo "create_user [username] [password]" exit 1 fi if [ -z "$2" ]; then # password from prompt htpasswd -B $PASSWORD_FILE $1 else # read password from command line htpasswd -B -b $PASSWORD_FILE $1 $2 fi rest-server-0.13.0/docker/delete_user000077500000000000000000000001571465071536600175710ustar00rootroot00000000000000#!/bin/sh if [ -z "$1" ]; then echo "delete_user [username]" exit 1 fi htpasswd -D $PASSWORD_FILE $1 rest-server-0.13.0/docker/entrypoint.sh000077500000000000000000000006601465071536600201140ustar00rootroot00000000000000#!/bin/sh set -e if [ -n "$DISABLE_AUTHENTICATION" ]; then OPTIONS="--no-auth $OPTIONS" else if [ ! -f "$PASSWORD_FILE" ]; then touch "$PASSWORD_FILE" fi if [ ! -s "$PASSWORD_FILE" ]; then echo echo "**WARNING** No user exists, please 'docker exec -it \$CONTAINER_ID create_user'" echo fi fi exec rest-server --path "$DATA_DIRECTORY" --htpasswd-file "$PASSWORD_FILE" $OPTIONS rest-server-0.13.0/examples/000077500000000000000000000000001465071536600157075ustar00rootroot00000000000000rest-server-0.13.0/examples/bsd/000077500000000000000000000000001465071536600164575ustar00rootroot00000000000000rest-server-0.13.0/examples/bsd/freebsd000066400000000000000000000006511465071536600200160ustar00rootroot00000000000000#!/bin/sh . /etc/rc.subr name=restserver rcvar=restserver_enable start_cmd="${name}_start" stop_cmd=":" load_rc_config $name : ${restserver_enable:=no} : ${restserver_msg="Nothing started."} datadir="/backups" restserver_start() { rest-server --path $datadir \ --private-repos \ --tls \ --tls-cert "/etc/ssl/rest-server.crt" \ --tls-key "/etc/ssl/private/rest-server.key" & } run_rc_command "$1" rest-server-0.13.0/examples/bsd/openbsd000066400000000000000000000002601465071536600200320ustar00rootroot00000000000000#!/bin/ksh # # $OpenBSD: $ daemon="/usr/local/bin/rest-server" daemon_flags="--path /var/restic" daemon_user="_restic" . /etc/rc.d/rc.subr rc_bg=YES rc_reload=NO rc_cmd $1 rest-server-0.13.0/examples/compose-with-grafana/000077500000000000000000000000001465071536600217225ustar00rootroot00000000000000rest-server-0.13.0/examples/compose-with-grafana/README.md000066400000000000000000000024361465071536600232060ustar00rootroot00000000000000# Rest Server Grafana Dashboard This is a demo [Docker Compose](https://docs.docker.com/compose/) setup for [Rest Server](https://github.com/restic/rest-server) with [Prometheus](https://prometheus.io/) and [Grafana](https://grafana.com/). ![Grafana dashboard screenshot](screenshot.png) ## Quickstart Build `rest-server` in Docker: cd ../.. make docker_build cd - Bring up the Docker Compose stack: docker-compose build docker-compose up -d Check if everything is up and running: docker-compose ps Grafana will be running on [http://localhost:8030/](http://localhost:8030/) with username "admin" and password "admin". The first time you access it you will be asked to setup a data source. Configure it like this (make sure you name it "prometheus", as this is hardcoded in the example dashboard): ![Add data source](datasource.png) The Rest Server dashboard can be accessed on [http://localhost:8030/dashboard/file/rest-server.json](http://localhost:8030/dashboard/file/rest-server.json). Prometheus can be accessed on [http://localhost:8020/](http://localhost:8020/). If you do a backup like this, some graphs should show up: restic -r rest:http://127.0.0.1:8010/demo1 -p ./demo-passwd init restic -r rest:http://127.0.0.1:8010/demo1 -p ./demo-passwd backup . rest-server-0.13.0/examples/compose-with-grafana/dashboards/000077500000000000000000000000001465071536600240345ustar00rootroot00000000000000rest-server-0.13.0/examples/compose-with-grafana/dashboards/rest-server.json000066400000000000000000000366411465071536600272220ustar00rootroot00000000000000{ "__inputs": [ { "name": "DS_PROMETHEUS-INFRA", "label": "prometheus-infra", "description": "", "type": "datasource", "pluginId": "prometheus", "pluginName": "Prometheus" } ], "__requires": [ { "type": "grafana", "id": "grafana", "name": "Grafana", "version": "4.6.0" }, { "type": "panel", "id": "graph", "name": "Graph", "version": "" }, { "type": "datasource", "id": "prometheus", "name": "Prometheus", "version": "1.0.0" } ], "annotations": { "list": [ { "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" } ] }, "editable": true, "gnetId": null, "graphTooltip": 0, "hideControls": false, "id": null, "links": [], "refresh": "10s", "rows": [ { "collapse": false, "height": 244, "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS-INFRA}", "fill": 1, "id": 1, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "span": 6, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(rest_server_blob_write_bytes_total{instance=\"$instance\"}[15s])) by ($group)", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{$group}}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Blob Write Throughput by $group", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "Bps", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ] }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS-INFRA}", "fill": 1, "id": 4, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "span": 6, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(rest_server_blob_write_total{instance=\"$instance\"}[15s])) by ($group)", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{$group}}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Blob Write Operations by $group", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "ops", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ] } ], "repeat": null, "repeatIteration": null, "repeatRowId": null, "showTitle": false, "title": "Dashboard Row", "titleSize": "h6" }, { "collapse": false, "height": 258, "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS-INFRA}", "fill": 1, "id": 2, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "span": 6, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(rest_server_blob_read_bytes_total{instance=\"$instance\"}[15s])) by ($group)", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{$group}}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Blob Read Throughput by $group", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "Bps", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ] }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS-INFRA}", "fill": 1, "id": 5, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "span": 6, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(rest_server_blob_read_total{instance=\"$instance\"}[15s])) by ($group)", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{$group}}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Blob Read Operations by $group", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "ops", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ] } ], "repeat": null, "repeatIteration": null, "repeatRowId": null, "showTitle": false, "title": "Dashboard Row", "titleSize": "h6" }, { "collapse": false, "height": 250, "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS-INFRA}", "fill": 1, "id": 3, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "span": 6, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(rest_server_blob_delete_bytes_total{instance=\"$instance\"}[15s])) by ($group)", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{$group}}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Blob Delete Throughput by $group", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "Bps", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ] }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "${DS_PROMETHEUS-INFRA}", "fill": 1, "id": 6, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "span": 6, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(rest_server_blob_delete_total{instance=\"$instance\"}[15s])) by ($group)", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{$group}}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeShift": null, "title": "Blob Delete Operations by $group", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "ops", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ] } ], "repeat": null, "repeatIteration": null, "repeatRowId": null, "showTitle": false, "title": "Dashboard Row", "titleSize": "h6" } ], "schemaVersion": 14, "style": "dark", "tags": [], "templating": { "list": [ { "allValue": null, "current": {}, "datasource": "${DS_PROMETHEUS-INFRA}", "hide": 0, "includeAll": false, "label": "Instance", "multi": false, "name": "instance", "options": [], "query": "label_values(process_start_time_seconds{job=\"rest_server\"}, instance)", "refresh": 2, "regex": "", "sort": 1, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": { "tags": [], "text": "type", "value": "type" }, "hide": 0, "includeAll": false, "label": "Group By", "multi": false, "name": "group", "options": [ { "selected": true, "text": "type", "value": "type" }, { "selected": false, "text": "repo", "value": "repo" }, { "selected": false, "text": "user", "value": "user" } ], "query": "type,repo,user", "type": "custom" } ] }, "time": { "from": "now-5m", "to": "now" }, "timepicker": { "refresh_intervals": [ "5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d" ], "time_options": [ "5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d" ] }, "timezone": "", "title": "Restic Rest Server", "version": 8 } rest-server-0.13.0/examples/compose-with-grafana/datasource.png000066400000000000000000001402041465071536600245630ustar00rootroot00000000000000PNG  IHDR5S iCCPICC ProfileHTSiǿH'&CMTB$  *"P cA,X,'ȠQQ,agΗ;}w;K Ȅds~:n& fEFXX@4gw4moMUr( d d\P_7?W0͇V"">i3ɳ,{dKy:=!sx|󺱹,^=ͧ6J8i,bIyy"A&k82s9tE #<2O cgf ssy%18hpbX! OaJ9E9ǩ<_\fbBX8K#5gIJ(iRDq8)>R??ZG)) %OEJE>9Ng B~t 6,ϝn+[BK J3ltkK+~woAJy_^2w9o=C,-l0oև" @9!TBAKpA| Šl@%zpm48.& &!A@Z>d YCAP<A|H 6@PT Sy*݇-FdXր ` fpNsW5a>_<(J 2C9PT*JZ*AjPͨT7JE}Bc44mvAltz z3]nE_DB0:ab0i|L1S9|bJXCMǮnŶ`;}!8S\q8.Wۃ;; >Ix-5~s$J'8B  V!Ba0I#]Qt:bxD"鐜H$TA:JB$}"˓M^D\G$'P(J%@@yB(C1apdTɴ˼%2dȖ!;J%P ^Tu z:@YɅemk*B'o #ϑ/?(A~Ҽhl!%ڰVPPpDWaLQ^V1FqbEJ@U] 40,شyA e5e;ʟU*>**UTUMTUU^RUSPsQcS{GT?ޣ>!أqAcTSIC3]sY-Ok9tE:I_ikkhjODiyKuMݩۥ;JI>AQ[[ `A CeCaa##QQmcq^& פ)ljo3kڷi!a3,Ϭl\<|yk =,,3-Y> ZoaĚm]e}ۆbk֦捭m>{v4`v]v_#zI a8a<:vl|O3 F ,:hUǕzUFwKrMrqx0f33^{Zz =OzNx9{FyyxDTd}(ϱO}j[GSYSS53 J<ȼxv^4;O<;S:b ,`'%"6 GyF!Ql,R2OMCM1_&ۦ">>-a< 17/4 ZiTXtXML:com.adobe.xmp 565 595 #)@IDATx |TCܐ"HlY1K M'mTD,/m![yryJMAb%V`1?D!ղ P!Y5g=ل$M6ol\353t$ $@$@$@$ \ F_   NASt#A$@$@$@Q @ @Q)   wHHHSF \Qn^>8+ sDY[v y FOSK!  .D Ɖ QZW{0,T>qUhW*T@QTWw,Dc*ԯҁ:9uHHQ扚(A[lfണX4*\Q8*4AjuKxHHHM hkj_7IШhV;_mD7"&rCrr2.iVI\q##yL8o*'KbϩֱH+$@$@$SSy;% w+qR'y {y?WY1|~V[lZYB74  lrP3=/[&   .C h1&t]k玌,8qt_;^kq`uME{*!Ɲ(5MLj ;g.nCL%IHHC|7 C\]ŰYezIć!鬇`){UR5 jSS*Yaó[1[Zd͍a0~T3.:P"[EY|@8D;zU"_g4JJ\eVN }85N==gюKD;ex!9O-v{n"x:TEWn"FDx٫P>E(Q㧚aXG{ ;BoS20 ѷAl/ѿZ8+D_h8+,B׫bP)M 1nmUu0SU!Z$g"Q Ǧ=1xfA12 ÂKJt_i v S(q#o5E&A<2 f$SdHrWg[Ĩ#xxPVKkwc0giSz{T}k4ҷKT']d"/A;i>2R)q>]E|^u @–nB^dm:ylC6}C^K9mlChId̴דцL'Cd(_ϴmb&cjQWӥ)X-s4zbH7Q4[ )#D2[JɹwA<(i`˽Ϸ/ e4~T*y׭^Rk3|ndvlj:o <@0oyQYR?Uݮo kR FSpW._"wvH²7d}o$xN$@A$Ou-"<%o]mb]Qi!_-> yp1G>2< 9%8,)=CL"Ǒ/x_wF(rüHh7?/ӷF-ߣuP]v2f{k#sR;Yue%|PRdxbMۛ+yzqba-X38T /Tiwf-HE7 3'1)N&kI-\.}+vd}ڿ" Vf'|̞ 1Iv5>m+|Ğu/,"ޜs=ŝO. @O?׵L 4ky)mH8 ٹ[1.3-t$ hu1M$Ia hmF:/d}gG< Mc{{qnœ3Y;L٪O+,5ʯO?˴~w'?+2! F"kk}=sv"iԥc #Aܭp#BG62АO^͠3lW_OF^RLzd;Fn=KFr˹Eb.IHڞ@{g vt_YGx+(8.7?Nb}w{y-^[gxgd?1<&OEr{39RPZW?gz΂>=u&Z5J9zo#wa)*,\+IËE!cmy*WN'|c뻛M3e[`՞ʬ8y {"lۯ{Įz?ߕw*^ &V]Ss␱^FP'/\" 9c<,-*v$~VHy홉{[Q;( Rj,+J6jŇdzj'NT!掦섲"F UAVy̐pJ~,Hi7mn_?Og_Goy[ňogNO b^wd9Q%k^`߀;&kzǰzof하z& G(7) wuGȵ=_Qm.FjQl2K2]uȫ2dL]eTcn ?JD"׫o/Ja|{Uش>UN($l4 )c5U8![*@X=Q6\ V5u-R(4۫~OBu óBE&d GJ/y6$uOSHً~P 23?zC xfgdϻ/EK,eL 1Iisup%}\vu= $%$2ZZEo v峌Wh^ɑ}uŒcvK^30R =XU X>!LdeHyWaeۥ9mHKٹTȡ")BD{n7[ϭR0mNl+}'AZ,ߋymO7\EKj7y$ "֛~xִ۾ZF,?>=Sו ?ØVhH˞cA[7{"{4ZBy4y s6Ts |1/v+|CkI£ k=Tcako=N4ISzֱ 2}'?D ̕0Mb~8kI1L "nHi-yg$=^Kr1~ŽNJyw} [MEN[*v:`g]p>>Ң 1 @3iFݙHHH 7d =gIHH@&PiHHH PԴ=sH$@$@$5AJ$@$@$@mO홳D    T$  h{5mϜ% EM$ @i{,HHH (j&IHHڞEM3g$@$@$@A @Q4I$@$@$(jڞ9K$   @I   '@QY" @P*M =gIHH@&PiHHH PԴ=sH$@$@$5AJ$@$@$@mO홳D    T$  h{5mϜ% EM$ @i{,HHH (j&IHHڞEM3g$@$@$@A @Q4I$@$@$(jڞ9K$   @I   'peY3{?KKGv;ݺAq;fRWl=L$@$@-"풄lLJ\X?nWj̥._^Ea6-Ȝ$@$@]@O?) N;D^yGHHH.jԔ=4{GcҾN  a/jBF$@$@$qPtbMIHH!@QF t5XS   FP4Q$@$@$@EM+֔HHH5a @!@Qq5%  hEM#pE$@$@$qP4ffT8X2ezt|t5فǒ1+4K2LMNFާ@0/ @:$vʼnjX,V A6XH^E?ic&!YD?#Q?qT`Ũ$/@[߰H:9S*7ϫTuFuX  :)}}1o+ׁdz}T-a{ e^6us"cIߚ(FZb TkmBpW2佰"EbژViWXxfʄB"D ,X hG^TR4a)x$شq ~uꟺ'D$ =Qؔ;^]Hи$f`ZFX*&&c{!gJٵL*(ڸhx~8 s~c xa{b1ggn 2Og".SW#o (<+EJBW*KusP o f=;cStPl;J_c1pK’(=D W2B"Ey׭9g$ @tlQsюܵ%@Dl\y\,upf$ƢyoWh`v l'J_/lSc(@,d= #d*CnrsMOEdHoopSɎ~&Qge0WX PR,_[y/էcQ"hHIhvn5 D'uŇّ'"aejdo$b{>JE\π3)y(E2fVW\\]*Gqxfd"*, ^؀suH5Y1 6#arc^JT@ڵ %X@yV6Y+eM|iyIrsp|ZE[ċ1*CdįW(~D]q5Nad@])ںDTl/uKGϫm쮭Y%Rnń{*ːoF)}o%IDݟM݆ sXhv+LIRW.Feak4_`^yA$@$@~ʯ͹(c>bLSzs=[*bx6RV 4Dʀ/L!8QV:i+*}g a]־$A.ZPrDdEyq.r.%<;c0&eM?%Nsȱ Aw5VFG ;9K k^DM@޲"[([-u.\_J gŘYyLOr+EJe[S`)GsZeU^N b[?i15]%įi! "-jdJJcqT}3 qp~Ye4 nޞM>sTJZJEȱSk̄aZJnIs8c{xC1 CJ7)c'w -KjkKHԝ3Hy&}D ŊŘe39%VX+gasej˽%<s&2D<41X}5ҷS6.{o>C[/\b&vxD;w P(=Č=uH$@$к]к&gW~eI{ 6ZY`ɫ]Xߣ nejnj jp[yIArl /CM];_4\Cb-݄oU!7-Eb&[Wi -]kێ2[,᢬TJ_Ii׌zuE]{-o\kR e8Z?[-UwDu:N*ZXL1HH t QSq n2`oktpӍXT%?:2!'zz2 GǫS5xg5k1S @ t'[d/P)xl_iL r2T}* QE_N; HHH :iB{$fӈھ&'{ٷ K’    $@Q4ʶWa Ij}G7 W.(6HHHPxB|2d^  @~^ g^   Es'[C$@$@]EMz6HH:՟l tY5]p  \BB\sQm֐I+@   .Eݷt}<.'.\Х_DcsلL@$@$@$ڻ՘c"q_-}}:.E^VH`~yO˽Է=o|t)zZS~l;^Fݦ}[WA j(}=>o`9MKPZϯsg3z|=;F ׉qY/})v2꥟S dž_v}^7|o7x_O[~}s1 'n(ognnO?|xk=~l#vHm}s}O9&j|5 %XSӖ fY$@$@$@EMWHH.l0 tN5_*  r(j\$@$@$9 Pt~eHHHr] @$@Q9"  .Gu9L$@$@EMWHH.l0 tN5_*  r(j\$@$@$9 Pt~eHHHr] @$@Q9"  .Gu9L$@$@EMWHH.l0 tN5_*  r(j\$@$@$9 Pt~eHHHr] @$@Q9"  .Gu9L$@$@EMWHH.l0 tNWVj.…evAڳW3gx]H:=zફ210.\0tƓ+/_m @ b޽{:M @8wԾ*s)Tz   EM@HHH TPԄJO$@$@$@ 3 P փHHH 5cf   P!j[CA @RS^n6ij1 tE5o~lZY!ZtY[%ԘHHꗳͯAM_)kfi?YSL^Jg˞{BOJ~c:eux~]M;m 1رz:4LTͮ= }X|[;级->x|(ؑځ5iikSFZyʶK|ؕыvIN@tTyj w;+S{R׮zISZ(sl "!c=O $cٶzRI.C\ȥT "'zrf0~|P''N߲~(<~ENbMmD}jx`+4,|b{i've=_>%e{ٕ\E:׼#FHBA(.(j+1}$8w DFw+z~N|Z[E#YWsYH:XS͔~(kdϑ+Ԍ_ ja-y%l;&`U&ʂUwA_@mEiߓ[7k{{SAoa %'G"q?]|z5Ob,\ӯ#f!on;J-t<g⭬$@h9TTc:.E.A"Unѕ^KܽfO TN#y5Jx8f6KebxYL}~iCC$PSu5"&TaJ0D>T*qE9%%V&7%>Sf`m*FZj_{cc2 sb#D*$ݻ#""˜ׯ5s%rTPiT6 ^s(|4߸„#{ڨjƫM kO|v"ld?a הӥvhh=MSY4UKJV(%IjNΡ%z!b{/8%O5pOpkUhphZ, Jc׫lwKWKU#X֌;ad$w#^}ƌh8wь%M5[ʮ˾r U;"Y,ԌCNnɻF: #n\+ԹEmWueڍ:q 5NCfޘv/ ;%e58w<#w,=pg2՜®퐜2+z4Oɓ<5BjQ!%lMo9v[&K"S.9',z!d' Y7p3>sYo:܌LL+[*ȽnF6Q*;*!SQ{"~p* UBge2GF>}% v!Ff\Jo?x7 '݇^k~{q.RP6\c57azPp3;5ceR[ qY;&ڠA=p˰T,uQK޵݄$R4R}\m.2 P@?>GŋQq*M[ɼillrz}CXw;'-,ËmTIK0YK ǫ>Q{akvti9o&sajnokqS;k^_.Mxl^)"'2dn~~UYЏdڍpp40lY<+]iyV]_%X{zΜ9޲Ȭkx$; @s æL{SoK"ewY6#uJ:Iz}"oq+۰7'bA ~l~ے,ʳ#ElvQo-ou൹H]~[QO C<>L3}-ǔZ>ן$B5r%o]xIo x2j^ |9 Z2Lyu?Л8OI֦ԿS*W3z]fzֹs紼ARz*HX!x嬑I+>~GB5)j̄y,fIg՜ry^Z?3~v}\tR~UkudwqWJ=EL5">iKƷʼ%`"拚d<) 1ES#|uu-uq-T/AI",Կ(TF_Fۡ-S"C$65T ǂ5Y||kaCzN@̩=8-45$jw)1kpRR@,ZFR*8#+wnPy<" Mx_mɒGox?͊n?zMcR4FU4𛟥b᏶3Z4^X%jIuϒwyE$@$ZgMMw zx@O0=OO|zY~V*+Ծs"|RSdgb3;{<>O2XO'JۨЫL"gpZV 9Gqp nwmvS=fCS{ bǮj$=p^[(UH^{'㥙 FFE;$'@NL waqwؕƨK (p|KZdSs=b3sZHؿx;V54d,v]6$@$@@}DM-ք[qITğκ*;%.50x=9?+Ԍ)Ex?ai7Q[S[ VW( -#G`;thߤ&@P;*]EAq@! ~}ڿݯ <E܌b']Rm7juF7)skpEi-1W˽|Kk2wf>+#7JM5>9Jr?F 40) @ |y`#f?ooR%ÛBOH o8}RnO{δnaSeqq~S܌ C|-.) կ" !": aݽ5eqsfb${Z)N')306ZY֨A=I 2Uߓ-Ɓ=۱1}R_[[<%h)[:UOGxe!~v%OHBlM܆x%wz'<Cjv:)6țe"f>S2TS}?ĺ=]\|/_$IeZwpkZ8ף w#^>qPt?{wVf5Z˾mJ/W|p\6j+˻iFܝubD`\ $@C`Lg͍7i=el 4BEE&[j'&]c<D[oxnX?1Ϝw88 BIbCsd]*ּuN@2L9_v-Ol㞐G}8_qrrJ?I^c贶ŕ!>S&ck|?;ӆRf'c3P)o퓼%sՀI% )xh(hSO$.CSrw Pke\YΟapȶ-u'NO+Ӊur9nʓcCYuu 0=#UVBdN5ZOSlI>ɞCjgl ORvt(2wY3}+޺[EկG߉S\v]Mǎ.SoNJ^?k? !| _zߓpKk[uXR(õ.oH$8 $|.Ih~95 N;5O,f4uoW]<8lTc0X{׭3gΠwF|O.{UVh/$eH&O2?VfCPy/!5]\7# @8w6C=5jklTb? Q)-=hR ߂xݚB4*iI.SFTy}UK<$@$@A!rmwOM' zjBOBFP8 $@$@$@EMCHHH@Pk@$@$@$)Ptnd#HHH(j   (j:E7$@$@$@5 t 5   ~HHH:Nэl @+yCHPg  J ,,6 @):0+ LO#   P @Q : L&`4@$@$@$ (jBX   PHHHBEM(@$@$@$0 @( ^`HHH&@Q0B   5  @(jFH$@$@$@@&zu   EMiHHH PԄB/$@$@$@ ! PցHHH `5#   P @Q : L&`4@$@$@$ (jBX   PHHHBEM(@$@$@$0 @( ^`HHH&@Q0B   5  @(jFH$@$@$@@&zu   EMiHHH PԄB/$@$@$@ ! PցHHH `5#   P @Q : L&`4@$@$@$ (jBX   P0p*;>nHHH vIBki>&?WrY#~Gۑ319aP?L:%˾L^bŦ6kD ů(hHHA (Iw!mA'rdLύ-/OBC{R~^A>D-)=e426lrQ6wApeM%u:Keiu)U MoLi$W ˗DFԕ?2&^&]}teX&sxΗhork-Qk@ 3aʽ(oצJBtD"ҳ]V$`L]ofHHH)p`Sw hx,pT`Ӳ{wK#UFxQi.NG rj& <3X[ʐ '` .hndOz2ߋ\V"2u2Nb/*GKcqV`Mu:w$HRFy8mk HH:0c%(DBۓQrOȕè{K=\5k$ȵEH}?^,qmZ2&I4NMfxD c`uO}NmZM2*;!hT5z$d⠔XXdFW+qe)R=iF¡ͧy0Δh j̽HMNBA<)R{I>J,d' I?H sO]~-֑N"Dy/ [!URJ }LQ6Ga\w#_ҭ̔鳾1H$EyB$@$@@PDM2[,+|+u.$b\&MTieF O«EEeh)Y3Nm=wQH5U"[ X.(s";{:2-9qZ? @ Q-WbJ2M4F}-?UTyk Q8s7xqS?iR;άD K@P[9\W_VE TU WdWU%r mxͯ s @蔯IpGDݑľ.CJXƈ\/.csQEۃSTֽx-G8;Uo LSL Y2GuԊq[HHڃ@HyjA6(Cpb! *.{w'JH-Ejbb̤(̓m(%T{0~&C_t|0D$jh(#/ǽD')g\ȗQoʢ`Y¼AvVm0^i!NNkX]ߕ!%2ܮCJ/ OIH:0eV_u 1ȂcA+gKt^dm*z.OS$q$qP"娲 mHDW7:(13rKzZ诖P9SvcI9dm/a^w`N^33wMyC @%Ъ/bc+vl/&`PlU_EoJlBꀶTˋ0ɋ0E`uH'e9yLCjl`ex$  !OM45vl5`@lkwo\׉GF(Ac?#O)VO/6bP7<Ӑ @#@Q:S]A\I0q]mAGZdѽ<9xF$@$@:r, WD`AiH{$Ly tl5\W[eKSnED@ku:HH+銽6 @'$) M"   b4 @ @Q1$   b4 @ @Q1$   b4 @ @Q1$   V{MBm\p21:={x=sF#H:/=zફ dZDD M-S޽fIH ;w;!Sv D$@$@$|5g$@$@$@!H&;U"  h>3c   $@Q* 4EM1 @ NaHHHO̘HHH  PԄ`J$@$@$@'@Q|fA$@$@$(jBSX%   hWQK0wʵ F p%TUUرcG{ $Z텖ͭg3b݆\ן| 5"&뉚ÿ`r`9N땈#&֡N9V>N!FwϓygGbʪxtz<헺wS7!iͨ"W[e#iOnw~R+{Z>#f!OCrC#+*Á?ϟǷ-|qmjv5$lo+hWOX^0剹K__[?N"V<4^ifP83Ys{ۑ4}z>?~l,<|4=fhJ%h&D|JUx礫R $Al*KMoP";}4A5p@ΕQq*  B@hռ.b6iEłz㦄 ^ոROkP>z..ʇ]2IN@t$nֿWx\DZ0y.v*tL;{-vd(sq4 S{qp{Ƿ-edt_|eU>,Kva!WuSm[}6UTyܕod8tڔD>i΋$ɘ4d]2Zy]ܓ:%MxgQ2nΦppaO O'˶Rt iCm5>s ofʽ{]B֓vJ|$*>Z(//O0cSRRb`Pq*  BݦSi;qM'\㿮P4ai+ g˛0E<3nO+qUxq7*{lRDفZl_2!jbצ:x >x5uΙye*j?ݎfび9(;Zr~S 0⑅XT8V"_wO-[T޹GUk0 @#.C`!P,bs(T{rD!(E#biҢ!XD"R | !$Cg&3$Y.~=k/h }X?8xo6\7 T=~2^2S̷`k1kstJ7`_@d)Pƞ0v#~loMҎúU;s)رc6 Omطֱ~47ֽ/aaڬyz/[x Cʊ;$PR#~˗y+1w<,ZV O[e2"fLPp OqG]ߚ8ڊ/C)r?=tI\ MȞ8YDӹ#/O၉N\?h?w[NkLeĶ7:h@9_p n"ϳ]%Ά>pZq'$W{p5ylKI>ta6l{s N㜚κ]BԞ~r"OwI"eGe✵#KU2.oWv|eyM"Rl-|`hRԈZ ˱cǴjVq*Ma 4r? _E(2ZyZAOA+dՉvD:@X'LoxӻU{bؔ)1Ђǎ_)+0}Q=y]j`Gbnɘ@[psܼdI-ܙW駲?qmj-nH1#b˕bL \x-oyqٴy0`6tmڴ 7P_E K,rv4N/ @]Sd=6f9N@hgb<5SՅb5txpk 2? Z!S)>ZZTմL8cTHh.2|. cV#2w?H{gH,)+6rvf k^AC;$+ %H@8; nq*GGTQ)`1c?[ڧ:>{ylեi1ŗʧtC:*-jwA f-oO [SNр MxX^|Z{_c @hShM~s/ *NAa&Sepz:߻➁$@MTs׺~5*fLqtG$e   8 SHHHBEM5]%  N; j,J$@$@$Ew6L!  !5!XtHHH;lB$@$@$Bpz=J6`T $@$X R+K'7 ( 4$N?5$}HHH `(jHHH@4d%xlWW\q 42hٲe#/\(okԱt+3&XH6j6'xL   ;A~ @Ci<& @ P) 4c EM @Ci<& @ P) 4c EM @Ci<& @ 췟bh4ceIHB@YY~wЦMDDDt3 4Hx163&F$@$OG}\_Vj_3XXɻ:ޟo]XHH (ryÖ-[SNP*x  @Ês&vFc̄a(x8WZU31ߴ*֋_`/86C/?ZtkOaÜlCA$ywlc*flڃ=/@BB7c>_vGHo;4=NM}lq>Ki[S@R8 _ΥC0Z#09v vI$[nիWq\tQ5%lT>+]SM݃H"^<(u<sWDB\,P4p"J5 ,wFyX+=#+X\Do8jUz s_W!],Yز}3ȫ[1TNcJJ:JEp='e>ʭ dSMA?Ґ5 :=(."k}2?ی[c֖?|߆hέt$ZCc~;U-w\p8"A$Ѐ3Dq5 4`C"j3 l ۩D?ށ<"u0DhS?,g![bfYtݱ~n- BI׹;ix`o,2OE[UD xpzx$yo~ 4$+fJ<[mKQ*(8f$P;z$;ˆFJ})K`J,K4|^KÒUk0=Zۄ1ݱh^fdĽ8,ߕg"( %ޑIm&} ? k Dٹ1e-jC;b! L~1 kꋲ#?TUs,1sb,|H )3~pfWZ'YG& UkkH.l—.GϟXzO>/O>YZZ 1@] / u-\眩V0e3NAaA{8`p.P!7e*q-`lG]GTSjܛ;ti+5xD(e1>i4~nCH̬ 3ZhBZ}K0x'txTĥqɫg)OHf$@@EE*o;]}f3G$S^4Vxpk 2?+FĩLE \*Z_AM3d|}#1Hd%E  2F bFo1"<6_$ @H f +{/ Q$ê0kSTQg'  7~rN?ÅQ޷aU)'OJceIN?5mm9RS[R8_Dt'Z1E7b M'0啕IHH;¬ 45MYM  h(j{ ~$@$@$DP4f5IHHi- @!@QD$  N0G$@$@M@^׼y__Bd m $@$X 7֪^~$ 87B   5 HHHo5~#   ` @Q @HHH&@Q7B   5 HHHo5~#   ` @Q @HHH&@Q7B   5 HHHo5~#   ` @Q @HHH&@Q7B   5 HHHo5~#   ` @Q @HHH&@Q7B   5 HHHo5~#   ` @Q @HHH&@Q7B   5 HHHo5~#   ` @Q @HHH&@Q7B   5 HHHo5~#   ` @Q @HHH&@Q7B   5 HHHo5~#   ` @Q @HHH&@Q7B   5 HHHo5~#   ` @Q @HHH&@Q7B   5 HHHo5~#   ` @Q @HHH&@Q -% ֳ{1{l=[{4B$@$@M@@(9&-JG%iGIz^*\#o?\#࿷Ebw.6 -Z&cHVUJz(xcue c)"o\Ņ ԧbL$@$@$@@ErOm Y$("uPg1{@IUlkti\-bг풿WK;"@d@7'j(9"[8HW3J1?@ꖵ |@3;/b>ń^Ĝ]v>ȑcmH^KyH?Ië}髏ҵ LԽEz[pO9W>MO=LpU.:!SXwWqM$@$@M@br;P<70sg~>*d@cnF}d {_.=U#72ٔ.\~j'kGwd[+Ԣ iHHz5Ʌ`@$fGIV?^)2u#_wd>]ʼn+3wGQd/|M"  H^DMCTbJO4w#HWnYOaVdzT#'cz׌m0nkwԈN1֦*Z1$@$@$@>|8~d\[K_-5f(Az_~2BJ>JO>OۓOvW/1$  A5R 4QP=zDž]`Htdݍ/䫘49+9?Fކlwܥe%VZGGC$zL^@h(^#FW5>FU>?| =Ň"YK/kO&cl0'LTȊ/w7P9Ta6<8(  hn&MʗJ +jM?EYC>?aq  h(jj˳$@$@$S#c h$H$@$@$P31 @ F$@$@$@5s h$H$@$@$P31 @ F$@$@$@5s h$H$@$@$P31 @h(ʯ@@ rׯ7ξZ6|^5s*Z4ei(ξZEoIH&jj{@#   @QSTiHHHy@   @QSTiHHHy@   @QSTiHHH{jn<`(+-D>;s@$PqeVkT.Ȍ$b(jBBkN d>nuDл_O[1&&@ND~7 Jխ7zY:I"t_o|swA<*+ǡONG-Te 7pO}4e?u'. F#5i%@D$v-jo8z `ܒٴ9$ P6ԭ#q";yٮsuٙF!M6WzUg(-.VXa#X/~;O3m^d$tLQl hcPV\.H*=]P=, `ek?Qx:pwWC~c?տҮYp8 ʌ fϟԶ PMbhZ Uf-!s:7`kXP &j,ٻ0mcIEA]ō -g63C/h+ή XNb35C+nHEsNiݖ.kN駋V5.ڃNq/-H_0O:{g?scƬ<9k:&r \s`wvQ sOqڹߞʧC7W%DX,%AA^׵m҂)X'{&h0QpuZƟβ~3IDATW~SylVS{sc19mk*_AwzՇ#C@Hh 59B CL(8|q!)>yu#_%.HY iky'ŧxLH] [_}ikaɈx;,]vUZjlH-_(toP*8l¦ZJ PhvMMVoU;F[AYN| Y{SqXd2LrFUXGzYu0WZWȝYg5iO": 3ä|&Dʼ]kR1$6'}1Qڧ>D_E}eveb~? ק1/d&dQdĆɴIqca9aZ9"ؐ| XY>~^>XdC"B\\엌?@V s7n@A%9$Ncm9˟ަN Ʈf5ذWa2%MĊàN?ړQ+: oiȀ(_5gĊ g`OwTez[uǨI~IJ^ΝAy.h p avRi*OHb$@FaEMyd͓/\:fሽg RBk|:!͘nv{$`k+ay0XA\XppqtOٖ6S"eE(񇘞 2Qj&_ߨA[NI~Eϊ}7A}3.cl=Ȓ zgA.Ǒ#L e{FfH7O? ; ƝԌOv <ZEX{0¢UK&; m5T.R7GMd(ASD? wDqݥ.E0gBK/y{au蝴  AKsLz_ܸ{xUջXFdc82nR5ܾZ3s={&2#^QyoG7H@DqR~v 6: m2#~:njD;]>rR$ qIm ]ר Ϙ9Sd: Q#!^ p1{mDza(yXeqkD[12`ji? (=1B-'7 %"`d!יKHF!|i8*սO^IYe0dtGc,;&@dY £ AJ<Kqi2rh;`RN%u } !"e9JMJ#1f`V6_ș'q2dS-Ǘs~&؝V##vFVd=]Wsn0@m_Z?H 44HOFl_t|t,_.6??!U5cI|$ixVlz%hQ;cݬijU{x|!KjpBvSIBt[.}9-ddT\B6mm%rTPyT^ ЀO P y'`{"HA.&mlZc9Tv]/ݿue`l]/[+eL"'j&d ^3>8#" (ރdM})@t|/H\@6C^"`TE8[/ *O}O?:Es ;,}^Εe1߫uf_܆9 l˹ri9 ybKgY4\ٟcY+:>[(s)d`H^Vʹy*dWRcIn6[@XZ;h,Զ j@$:pFݹ@j-b }3cF%byYZP5R$"sNcS+]Yx<=Av<~_gغėT>*-43uTw+cںHYCT ݾ杓J¢OLv8 8v 6#j'-J^\yDl r#FvO/'ԭ,لX.O}a?'#%tm6==^ K۪qd6L' W^UO`veA9gIg+9y;YeM('OdxcJШ4+k[XPn L*{"6.TMyJs>ʧenAmX8( C1D]W- Lw||. VE` ?QQ]}P͛^jFO.]YF?LW{Q5,#7hLjsmǘSU19)Q^6j{N; vɵk|W$޺5ڶB䭾JOϫS _⣿_W~_ΌHĝяleͪnϩ)!!Ѐ#5p5_RvhP+)݂׋"M %GDXQt6 oK@{AU~t/K4rF;|ڮp$OjU"f ZTMJOuVbj{my9LxQlfhs%Xo\г= S~H{Z8~OM! Cq@p` DDC\ 9/B;>)Nh)ݦ|4$иl]6f!kj ଈܿ0XcC F~8WM圪JlK ! lD$@$@$;ߙ @ FK$@$@$@K ! lD$@$@$;ߙ @ FK$@$@$@i\~ZBj4]$ :pl 2|i5 yFyTVCZ*C$nԧ.h7ʐN\YMI'Gj m6K:6HZF?@WGqr %=9isdIA4{m:$"[;GRu7J/gqEw rѳ]3B@m4!]A:pDo1F Q]Ơ* Pʾ)B j)hTf-!sF5V &jӟBҪmLͰXj^(g)6KB-6"+}e- `L`_U ?VE_݅sxIMTT8mkviݖz9M S]`OY/~;9݊)/S NF/f?NS*.؂qR)[J!H**$PK 2c&5{tqM9Nǚ/:EOyà;E[![凐>Xw a:XNjw2Q=`&skӱ`"Vb܃q^i vNq|xn!L{%$L^mq/0uxd&\tSfdUx=$R g=9OȨ]hFlxazO[\q DǾ<2[Xٿ=76ww~hQ7z-~j!;'IO%`Jedynj0xۻQM"{q`&ySH]d$bo5r5yBBQfRLp]@7U#,8xւ2X[27"dJ:ٟT#Z| Z wHb*FȂCUD3W?ԧ_ի&7<hag5nԏUZKao )?y_k{ts;TvoGQ^&:O9*7Fa*-D?Iw "jfZFh =Ъ8l8{s7}|8ݬm,۪i(}<&28c|Ҭ&bŌar1s1 }b[XL_,+ՏڍDQL+Z#2}W&R 9Hl\D9鋱bCUۿH`u~mPm ؤ{C(HFlL_ wC|,"2Lk#_K[cL[O`ƣ-#n1LL'wc=~"S$Fʌʄۣ#WAϽփ1wķ͔.fl.˜BlQCǿUx1e 6vRbGA\ F$L8;Q^/aѫ{wY8[ ; 9}|QJД` H[q۰p.p,2q<6N|6=C,G 0ixa~rA]0t/@r?`)kT5fGaњM"B\ޝl$`M yRӞ3 xl*W!,}O>eCLORJRmܟtXqfns JkC<*c߃E"hrHCXd&7d}}i\ԟ\Z97ݳg}\\Vyq}!p)Qc& *I #dߧ`O4Aĵ,GIDq2Bm(uh?*V##uWL&qr3[?G$EK.f<ߥgudD2?!ĵvHnE !`쀡!34S4z1ψf=r]܎oDv"Q&LI`DetdPv3P}?_E 'k::Fedۏejמـ&juOsʧׯ_w Ge   3aH}yeyt|[|nOfT:h7;b3Q[匾V.EC†dz|!V58lۀ;*s9T5ŘPj|8Uk7=OGU[ iQOs JBcx*jǵW%rіnH12ZO`:iuG}͚5&+W.R}WyT^ rƎخ;fIϠBI #{M2^媬 Crs £/.vMly`Tag4ctm%v$S1fK$wFeT6'KlmBjG}mQ{];)[Fuюe0@t-ƦWeeڌ"ЏkW+;.5}-@i-'ZֲBDl]2)V]%_}R*6=+*'6m",L:TJ䨠 $@A (ױ\|OK&>d;g,|]Vn9?UwDZhLOXP|+s{}-ZyG)91Z]}8ľ6V/d"ÓbaTS2|ok{KpmswfGno][L C^EgkX RG P(/(FJ|@ܯi Wir>E990 Ald wʑ(ɻ:we6>bq-hڷ sTmT؂5^ ( >R#?,' Fmâc47Л؞ZenSOHD<+ }7='S8Dl %5ny;.e &][MOqL);q$Z-a.-e:lMXgA>?KU G6A^W֢/7RDNWޓ;E6r9̾Q[*kc:Gmsh}]CVƱSHpq+j4g%R yol.+sR xfЯ}焵{FޱR%Z^C G!ur R7/Ͷ4YK.t}vCM('Odj4*M `0T~];5ms++[deQq@v!oOɟ\935UJÔܭjfr6 l}[ ـz+ 4z_itzs^8锭Nipݝ6RQ5(TN]NrCZM7/ 7:9F ի ԏ=Lg] Թƫ ;'=#K]+4[m(Dzk|^Byt[nH^}SySj E2fh Cv9c1f'z#zP4=_49y:"gؿBK uU@)s~-Ax C־z59=SH\ᄍ~hN@θ5. k,h#^tVY4yHfH&DsTn2bHMLj*TjA& zu$@$@$@EM#n\VHHڬ+ 4b5qY5  hJ(jRk$@$@$Ј P4eHHH)iJͺ @#&@QӈU#  D)6J$@$@@DM>c#ʪ?o#zH$@u!IYHHHE `#5rvHHHH.(jBeHHHEM5 "   Pc   #@QtMBHHHB.XHHH P]!    5!  :5A$tHHH.(jBeHHHEM5 "   Pc   #@QtMBHHHB.XHHH P]!    5!  :5A$tHHH.(jBeHHHEM5 "   Pc   #@QtMBHHHB.XHHH P]!    5!  :5A$tHHH.(jBeHHHEM5 "   Pc   #@QtMBHHHB.XHHH P]!    5!  :5A$tHHH.(jBeHHHEM5 "   Pc   #@QtMBHHHB.XHHH 4GeqzE @ ".9_v7UcC l}HHH! L4d%xl   a   h(jE3$@$@$@5$@$@$@EMhFVHHH}HHHQE3$PJjoggN $@Ҥ-hD]8̓(["Z݅z6ߊ17 MO~#h|n|swAH''j|Y# N#579HOt'# okQ((X.!':G]! @!Poz˅(4acEV6wDZnYVxӊWHןA]/e%^'OČ:@jnE{mz1Dv$rvĴy۞mALǴ9Uw*e,ƴ[._`p,2LX`:9>Y`yH?AOTaǰ)SzJu_>{:͋~ ^2(d a{O˵ h:hɊi[Pk"^1 f?K(!/_-eyڝ`)nZ CXvΙO rF3P=l3ZǍ `:|?ewdX0yYkZBH5:V CFGJ cw^ 12z2u@Hx~&r9J=8zQ1`exD PQ {Sz,I0ga,/[~wI{\> +VjBN3n/0+X {4SD!,%$Xp4}#6?=W⨌Yl@Jg[CeqY[;]~qh;?HH l =̀}q&h&,Zbgސ,ߞB^X~Rg$o%v`*n҈~#v?x.`\·z>-/i\ԑ*-#4SD:0ixa~27JPPzAɑyX.r_nROQHۉpZiWaDwE b>RP-CfO͞y޸ 3Jc!PO./9Z%/A+0wT,]^=/4& FܪK"y )ymJv4qb">`{DЈ(P3Iֈ܆;NAs[,?A\QZtr+Ɲ{5$2Ks2M  !@FFi- L̕уEٶV f XMF aXX;h<D4Rl8L=Nl0}r?g>OCG%iYђdK8;o?FCw0WF#E#$v(ugLp㰖CrX̞a mV njmmSJ\SUBw.;Ox(礈N)ܫk'UHG(O윬rHHa 4SE!gBwb;35ZXz 2p Y?0 ̌ݲ"Vjj1Bsum ._g1RFC| HH++۰ڌ͛7=WE$fC.Bv-Yj);׳&ُ!Q:nçQ**PtjQۋQT?,L_`vho^xWϽ6;(rxTi'y]0*&>Gi3ɳ,{dKy:=!sx|󺱹,^=ͧ6J8i,bIyy"A&k82s9tE #<2O cgf ssy%18hpbX! OaJ9E9ǩ<_\fbBX8K#5gIJ(iRDq8)>R??ZG)) %OEJE>9Ng B~t 6,ϝn+[BK J3ltkK+~woAJy_^2w9o=C,-l0oև" @9!TBAKpA| Šl@%zpm48.& &!A@Z>d YCAP<A|H 6@PT Sy*݇-FdXր ` fpNsW5a>_<(J 2C9PT*JZ*AjPͨT7JE}Bc44mvAltz z3]nE_DB0:ab0i|L1S9|bJXCMǮnŶ`;}!8S\q8.Wۃ;; >Ix-5~s$J'8B  V!Ba0I#]Qt:bxD"鐜H$TA:JB$}"˓M^D\G$'P(J%@@yB(C1apdTɴ˼%2dȖ!;J%P ^Tu z:@YɅemk*B'o #ϑ/?(A~Ҽhl!%ڰVPPpDWaLQ^V1FqbEJ@U] 40,شyA e5e;ʟU*>**UTUMTUU^RUSPsQcS{GT?ޣ>!أqAcTSIC3]sY-Ok9tE:I_ikkhjODiyKuMݩۥ;JI>AQ[[ `A CeCaa##QQmcq^& פ)ljo3kڷi!a3,Ϭl\<|yk =,,3-Y> ZoaĚm]e}ۆbk֦捭m>{v4`v]v_#zI a8a<:vl|O3 F ,:hUǕzUFwKrMrqx0f33^{Zz =OzNx9{FyyxDTd}(ϱO}j[GSYSS53 J<ȼxv^4;O<;S:b ,`'%"6 GyF!Ql,R2OMCM1_&ۦ">>-a< 17/4 ZiTXtXML:com.adobe.xmp 1258 935 Z#=@IDATx |U@ E1&*A!#dD?DGt DP(8, 0*Q Dа 9]Ngޛ_nιO?9@$@$@$@$@$@$@$@$@$pV j x܅ڵdHzuջl= @@93; PDA$@$@$@$@$@$@$@$@%@HHHHHHHH@B         (/ u%$@$@$@$@$@$@$@$@$E @y P+/A'        ?P,HHHHHHHHKB]y 2? Y2-G?#"HHHHHHHHHt.8=ydm '_9 @ uGSm'μO6WHHHHHHHHH|%XO[f u+PQ3hA1WܺGf( $@$@$@$@$@$@$@$@$pPΩ)v"y"ҝ3u:p8rc/H        8Esw G:+LT%sHHHHHHHH~BkA+hT\]"lJ;HHm瑈$@$@$@$@$@$@$@$@$pPg S"tZ3Ya6AuN6QǬIzvv6v܉ [ yfkZ_@X$''n8ujɁ&       Mo4lFq:>Sfu]jDbmW݃s{9C ĉgaԨQXr%N<8r;̙3_|_swEVVVuG XM h)sӳ`/bgjVha"76N~WjYfy[1{lDEEk׮xwpWiӦغu+Jԯ_н{wvwq!}gƲZ`cI2 _5LCOII)W9L$@$@$@$@$@$P UM+eVuf3,l =H+:6]_|EGiΛQkiKZ%ҵm1]v*mڴ  v'-Z]YۙbT#Z *{QY6[&]z c.TOJJ2/RtbC8p0T?~!g*T\M|AO8~8jԨFCzVx븇Hl8q~:txNjѢ1\WA *oBNQ3Zd~1;Mrs932!s !Wa3Ժyj]u:ǽ uZ u?ޖt_~9T[`ziª%7\GkAڵM>;%y Gl޾4,7 cH@,f, Q5gY<'cY 6Eapu;0a;{e؎G*/I&AhӦ 4ib=qK **}ߍq37n:ݻ?T{?ϸu'}ҡtLOmj֬iu67nl,HHJJ@c]Ǔ޶md 2~K;]vfw@~u^d#81ԳlgIqg̘#FTX>Oy5ѫW/;H@ |;/NXjKowԩk<}Gpf:π_K= KټZȝ:9:##}ީC2Td~!j?8- z{U?aԩ*Ct۾}{5M7dXCo"ݏ?h@@xX oCHؕ-+ޒOf|<4[> lV#-6_FנQn"HHBe/2a߶~Щ-uT;w SST_n8vy~^f   t^BBBJg=kS}0yݐ[N%ƔYqR{pp4D4<;jH:LNUU%mGLwE/MeN=zN7H",!ɹf,{q,"[]kܷGNˡZΝ5Dh GGt n0D3=0Q݇Gz"R70u;MPD{ݏ4ܱ`A\{&] x鮖XYLjl!T6y/ p.(ӵVe}s*u-מIF`afO&}<#"9 L\W^ #Fc]>Ƣ_Ur݄+ȕc]!nc,ofdq:oXfhGq!˖pD4aٺe'uѠ"}s_|}o՚ Ck:{}c5TFJ}{oAYYO;IN$@$@> &EG[,},>n=S8IF =?m6w*.uE<ij&Б^l21婹{Q$hDk#>l;,K4ͻbj'+J=f톧>ǧ/7G,?9&``O-Ą9O>,0-K? LN;BK/_~[6 W+?WxT@gs1˱i].LK݃rRsr>*:iN-Oa|(O(_**sPkjXm7l.C^ '\^Tjw.,O~OuWO"Uwa-VöqXI,;Kk.ay^2_ΊoW m5S`˶ڟkuuk6m1H9&%t0nȐ!ꫯ믿kDp?cz #pu8cVĠDZ">ea0w)i t, YD,$)2:AΠI =s< Y5"+ {::5[w9h-㌸j%Ba A1"ju6'a_Zǚ%bذgJ>{OH WդbEWc0Τ!\~O%IiY E7b\4X,`lѕo`DAF"N"E|O kj&ka+uoc%9)eh 3O/NO߃mZ5Jƙ<ɓեn0&_?Խ}„ nN*;I/wfPkzATB7j^n1=k׮5>OEEM,K  @EtV{VN|o]j6NMٱxplm|<7= ?ٻbt=ĠZ`s0W54ӱDF>0{$; 12zO pqՕC"cuWMkd -ns;S-͔3gпg;L]= )J,feu7o6wUWZ:}YGJvb"7˚p9 G{htrcLxd#^v55+Cjb1]" ܺ/kfنȫcĀgÃ/@dt,<.vM3dm Lx|sx=x8~%~. "2v Vy%E vƃM4E.yiwlelt>پ611B` 8n Gc3cdM'CvOaO@2kS[D'ܴF&ԒZ,v0.sOGw `X:֝@U/z~'.y6`%}2YGFRa]vh l"9 PWxo 7VnG`yԩΤV:ME|e,={>`VNs=гc›aԱ׿~??OBuu% :h_c0x` :֓Noet뮻:Zz뭸+NM+_WcP[vذa«&F2?'vgY'!kHvy}RR~"-~f7V|,d :׆4݊weTXg4vLJS .VvY:[e։$Q}q+_vt}}Ct(a+V|0PkW?;^LJEJ||39OM~cH\k6Wե~:3p\GS{(1 ;BI1 ^#Bi+.JǞ4wm.K޴;"At< t :&~ZßgSZ-ul?#B]V7yN$@$@bm aIAzl"{By.7Bj"6..<>k3EZZuǐcF˦DZ|u"ީO${pλ\Kr.BQxwhH=on[[4hR&7ӗr9xk0gu>sJYC~rs|[` ufb`HN\bnׄ`sǰuV׮]Щ.l=o9Z'"A,NEc 7rs~$AD~48>[ELj p7.N}0rDNN?=>[mbHv≴H?A$FǒEG2qH&,OHTѷE\_M<9ԝ41bt퍮؞{pp=dAI9"uX?׀>lѤTr-/ VCɟO$OiMzr"B]ǥѰ/a6U DڏDt6ܓ\">P˻*$Ւ߉ϑŵѸߴ.죩EKQBEkWC來5ElQpɳer[.7@O(q#f}T_ȀC$rBdɢk p9Eq䩨k:>?B&NhkA|5gi-}19}'tb".C:Րd A;Ik3O Š$y1qX2tlonv~s:k&nILPeAߥKUO5$ԪE]^~EwFE$gn beaZ–pux՘&zٞ*|yX5FD'c~a,/ډt4n$ |R$)-JLPhd'RgDz(gTx3-B/sŽsvFcX5g*?ۆ Y;k".D}q/"cO#Z2ֱû?;( dLe! ]*{`DOpu.bZWatK`ߜuhq1 I7rضBIeR.mM/mg8frt ,cEE9"k#@ƕ=eݖьHgIz14GH$ʥM[#x<]8cZe!+ۻ:pa}8vBb|D˕T}Ek(cp*J_Gj^u̸?VI߈&"*vo19.q236J|APQꓓG (sPy&*mÕJC"҉iRš]8DsdVӥK]܃#/E\/$hV?)U^;ŕVĻRmHÆ_R: :ozWI=mhÆ ɵxPVu«Bі4Y_k N7A:n\6x/nQhbX;v.6;^>} OIkK D6JdXHұ"c셔#z* GKص ŀ{ߍGBtq_*ʬIsfbKr3'3ҶD+gA`BOP,rx~!ĂQʷݔ{zagvc+}CWܝ/w޴HZ J]-[4;*8R!ȍʣFqyTfPkcREwuot: juf-  "Jw1u{XY]vȳhN>*]- ${49=MwbR08cqg(/JĽbe0b4D?} \6r@ q0\be\O΢tm82N|۽{ɤZjA)/*1qu<҆2!ê/6acHϒ).`#2U[Р;{x}Z4'wbd9v$"xn"qa+9Y\yy<7Y;US@R?OEZDH 1[fho=nG]gJ1%"]3M$]9"i/\*z4|)i`˒L.mc-E\+r]E׵Č\DaR3għr_>6{ubx#amپ,,'Gz+Gzo~?AۮzKW !!\puf\gh9]&R2g:/#I]!2z %};-itb?}җ{P|ӨhNtL҄%"X,n=-aլ0D ^kW).C"N' )IqaNvRwq)wv_^%^1MD6(+u#1*|+>Nى˻♧,$dF"QN&T3CCҚ7`%ey**yQSKEEhr#+8^d}r lb4 9P&ջ -ߋu;d ԂP u.0me!qYJˉ\KPNϪfn'OGz0"s*y5Xhr-޻]k@j)zH|wң&>lfm}mDuY2dX1 #(v ;N@ƹ;= /)/c{u\32piI-ҩ8:ېiI"8 z111%Ꜥn"u2 olKJWAhye5=3b| jj|x "YT dQ `B1`2 so蹈_2\7 }QxQ&_Dޚݪ_R1 goV {q܏pY|wP*օ,nw]H;7@eFʾM3|8qSҥnׄ  NE:Y2˛Ζ$tQ8N#vè]pCR~2Dy,I~3ٞ;;O'8AoLX6h~>g#h:[Edy/7|z6 &k%Mi9&0ջ86Ars6zp@rj՞Ȗ*͎6GeQ|LeMXA"]X~Y=aTGh PƷxhdI8T9Bw PN={fϞmt%ÕUT(wA64l>o ÇK*$r5Ժa;o :x1Y 1DmuxԽ_-rvv}Rv[3T}["] qjI?:ʭtׯwt˗/7D:TӁ:{Cz:=[eXm_9"Bl ؂e FwcTPgCg uag E"Xio{@ =p#Jq_N'艗n&%$G`nxYK0BkvG{zbQ 2UEžT(P׵“7&ry-)kW ]4(6L}̘4 w7C,XoŻ#o0~?Œ ;,ь/+4Ĺɓ'cNTN׼lؽy*? YSok\'Qw|~?w%\JȶZ PFX5N3 @I xu0(i^!jWe34F,^GU Dpsۃxl,"jM+Uؐd_}Rm2&{ob+nQ+YpLԝc_Eo٧ʐg)nʷ]mILwg7ӡ\ԲUV}3,B*b$[Ȍ -$XšцJ@E2[8,Ǣaix^wNO2vXUmY7?۷x!ut50'љa,ZGQ{rA@?:\אw|NvP^磈2ECق%MaA'cf?Oyj{e:\ҏ/͗ھ~M6 4LznBȝ;5=⧅X:k ʞ]ХVl7 Dځ"W.XS>Hh׵7Zxߎ!i} cE,# O//h3Ok RqMb>|d-B3ײ6Sp\qlI̦2Z 1抨Z>f~elJ~g>#: PIfjn d$ u!YvS)ZP4yNgYԓKĪN\lQW:(>bdo"a1EJcѬ2t" N"89V5RwVL tXZCt+2ąHuLǙH_q/)9h|9j]l}9Έ݋2>:#(_#+߇mΒ]]#uc-LӁl}Qex0D:vҤI忈xl|_0obl^Z$*ēoNb˷ ϒX) F\9@f?|MxHT<@D~X6b09 _ : OYδFzn'zwHy.\SͮYT4[f x t k-&??[̶30ZxqMURp|pgT,{~\o;)\*2KHFx%c1gpXjV%cz-ٌq%:U\y_~Z3u(sʮsĬrzn%hCOo)IJp :Ks*qI݀2pfӗE) 'Тf3={0uTo={,gN$@$@$p O۷HsG/MuL٩b&O!6S% ;`%ocnKJb ^VkK4)֡>C/~IOYapM(a%[/zDv8ERlKSMB7XĘ9ՈX6!gYzePz`oN^Um6cB͛ynLC7ūNyE}~Q q\h:O!0ZKӱtFE_t*ҩ٠zb"݃>hX⨻ZH,g͚U1Z+ECQCRFS3\&ӵ|lRO8к^_֩Tdo+ʾ*o&Yl']a4Fz*4KiY˓Y+/5myֵ_d/婦ŅPq?-i1KZZtof ̍B%IY 3(K [_jOFQT   F@':wU C&`ut26@q=E:" ӏ-/%8#u9u/۸9qIgZҩHosze(3D=J-T{ Dtq#       (Ew*Kഒq I!P!yeRowyst-bbeTO+e(_ZuZZ1(9qᮥf)))1D"ZΩOêˤBʑd?HHHHHHCB,l!2p.[G@;y'w9 NHJUQ\j};U~HHHHHHHH#jMA@m-Utf{d=^7_^DƦF}\!        @u ~ɋ%bh)]pu&^@عkQW" LD$@$@$@$@$@$@$@$p^044N> $@$@$@$@$@$@$@$@$p8_;~ @U"@* HHHHHHHH%@=8 @U"@* HHHHHHHH%@=8 @U"@* HHHHHHHH%@=8 @U"@* HHHHHHHH%@=8 @U"@* HHHHHHHH%`8 T6*6HHHHHHHH[t}=o=;N$@$@$@$@$@$@$@$PPJGm!       8o P;o=;N$@$@$@$@$@$@$@$PPJGm!       8o P;o=;N$@$@$@$@$@$@$@$PPJGm!       8o P;o=;N$@$@$@$@$@$@$@$PPJGm!       8o P;o=;%s@IDATN$@$@$@$@$@$@$@$PPJGm!       8o ̬ qmxtJy?ytHHHHHH(Ԣ"]-YJ+?&      v:        8P;'{C$@$@$@$@$@$@$@$PM P&       8P;'{C$@$@$@$@$@$@$@$PM P&       8P;'{C$@$@$@$@$@$@$@$PM P&       8P;'{C$@$@$@$@$@$@$@$PM P&       8P;'{C$@$@$@$@$@$@$@$PM P&       8QAVfֹE/49zGR ツT똬i8q43L?wrL      Tܻ1l2*bMRrZ.cW=Hmfe]lQV SCup+>6&unu_~M~-uqLٓ-=Iؘp\8#R7R V`äK!Yl=)ر3ͬ\'      *Iu@}ɬ4<'+ 4|$:$+5MZV;マwf>=`4,yNDC{sPX $g_vB+KM>"NXym,Ik":0,2 cj~\;b,c       Ou־f”Mh"q N;=@Mj'~= ɕ'NMLj{sFtZxC ܒ)2?0Lr-Q}Q1z UNZApwoF4 |S=N* ^ykiRxs}J,zvm[VOAv0p p85i-'ݫrEuW0PN-uR W{E~=(uTJ(Mg%MQCMZM>MƔ㝇UnM'@)$"%e_0FϧtEANcbb m-dqꊒ{$>5&C{ݧѸA/?7yCbh,C/L~6baSm1DmߎȦhp[h?׾i1Lj/>x?cM֭#qu{U mm0iBr\k Ƶv_3 _na^xK_:mϢ!+WUmɵFL<ݞRgq.-mPMɗ^ 9Q"ݖRIHJm@"g_zs(,9uDȋIRܱ9ƾ2N'bXz>35 5*ҥbk73998Ws 5~u}B yBa`]P!%%IgZ|jI-a @cjB먭O0@D: 5GEk@􉩋К"]ZK:sPDF>HYCu@ #.Yi4: =L=v6CH.,xUE{i[5E.W]v4n.Ш tH+lɯ'Qb4 {}݆[ ,Q/bVr`oヘ[" ;9xQ׺"I_u@3X9BCCĨ1xyK8z(F-*p<]N|#5me;3pe֥-v q ̕u+jJZ^\[^CDpHO".=9<b+G3娔' Q'˯C&tЬ O¦i{&\BBm.XE[g]|uSּG׸=#qu=׬: Y#`y }$Wi|SqK70[{Dv 7o4E#8.&=Ha7\oMS oT P9ᯓ7[&٨Y!m_'עxS t>ªn&_\)h[g9};~͑.%mCVbmHo+R\ ,R[6Ҧm_~5PF~OBg;M[ B|@fKwֆ1B4 -wPj^ A"dsD7v]֧,<2\WƆ+}"WƓt]xfei#d"ч`to%Be1$MBfE> ͤA[M,-GX[ ;'v0Yeef gz0y$$bK^ZR;pxRKwhb_k(eetπa_͢;8"׏^&'qsfK'[qPY~l%D|f"7ofzKCʾD$wF T* $+3k|-U.Ev\/l>afD ŪNgtwױ9gԴ½CRd7:MjF*) aeidq*.пv1\|zR^]gf"~L{ΰH3kbԃ1j~12g-4]e5y-:gM{_7Nb`-b0jmwo:ڣoó~ 7>1QC>1|R;gegt j kIY*.35 ckI ͳٹ;e -o1g̺v'ߔ$čZc\7FEVA65˔\/rv26Ligئ3̋cn'}J $@$@$@$@$@$@$6_EyJU8t!җ_XP~K.?GkZ @P &)N_EJ~>+ 8)nʶZىmXy^l~\х2\&Ee[R&!uLYrTWuxWs%+M,4ȹTz>;}W BfZ~f][E^0oeZAL2m^Gsf\iWZr "{;.<'     ^J_}̇t 5bmzJ-Wr:|şD4 ^*zY]h:ISXٖ%Z-i9"K|T|TSmw!n+|_bzYEKVes;MjhD(~QHIZʬHg)NTdSӳr:,$@$@$@$@$@$@$Pze| /}EA$P=nN7ز84Eųl#_c Z+#& LWb Aӹג$@$@$@$@$@$@$PΞkw"vO+;^Ee.OW>~M$@$@$@$@$@$P D0MSMgHHHHHHHHH!Ps TAdD9|W>I$@$@$@$@$@$@$@$@$P B)y sf|%;@$@$@$@$@$@$@$@$@UۢNE9_œ*'6HHHHHHHHCV5~Laj]gUޱ$@$@$@$@$@$@$@$@$PM:XgnKu& .IHHHHHHHH u g.iY,HHHHHHHH L:sA<         Z̙E"uiӥ<\'        ([nnÚŗŜk5/IHHHHHHHHF+\Y򔭉E$@$@$@$@$@$@$@$@>c:bru]aO$@$@$@$@$@$@$@$@'`uV8뺵83\>5-IHHHHHHHHJGc2 f]7e`j         UӸԴLX$@$@$@$@C 4f {:    E{d4-q T̬2^3F1 uOv}5-ti֔$@$@$@$@$@$@$@$@$@ I{]$cIHHHHHHHHJKEYMVT spg.ʹ\ @@zz:c rIr}ocd?         <#qXݺhР~œ&R*ܕ&$        ({\̥ƫuL% @ :uu-Lۅ){HHHHHHHH?!@:u XY9=&        %P/, aT&m(Hvl7?b4K4,7=hK ]<%߉%ɐ?nöȶtz?} xq2YvHll`"]Y=b+UJ~܅3*E~IZc* /${cЈCTx]C$@$@خOJvM>"*]jܧ="Jwvdq~i˶ZWIg{uAn^? Jځr ,d)ۓ=ZϪV֑cҝ~4 "8h40<tB *ʙZiIg.N4~q"Vdu+ᵿ_ó|/eY/v>4[PcߚKB2SVs_mrkg{zm5U_ˍq`lPzXvEfܾ|k3n[oQU̞Rr6p?nk*+)򵯌qYrHw&QUgL hDưP .PPiE+hEx_*VPA>h1V@X B@ BB?3s'3 d2w;$ι{9B$@&"PQ^ÌdΕ1b>w\d]+dbiY:m.9DIv>-H |Wg7ţ`'֓{$L)r2$_@Wh~7g^[(qI.=G2N,U$ʈIy۠zXc}9 @p7SQqdwy%"S~+U A}VuSD FQ^"`M{ҀݼB6n]#Eޗeۙ ]>1]'  Av'CUKd5 :rwzUǯJRN)쒝lz}e5]u7`T#l7K6-׃r?0t2oynphr3gO2NQɋ<7<ɿeo}8.=lc !PLmN_ :Gp!(=z.KbɽWc3.j?(75K˒u9$dU{#Jd+6N3eXgIE>_!ɘJVG6]KG)ˑ|U78U V8o(Y8k%)sdw 9g$歒3tONny¹e^8 C}HYٴHCR;**wX10Ez8bhlt,*=Oy|ffo󝥭s-(QKDo I,jȬ?lO._=꺨Fu0jqz&缬rZI\.J3o,-wPG !NR(JY!#2J즅-OFGeΌwGydͼdWɔ׈eYP漳ѶDxQIc0QmaW3_ J|QgVH,eIRRd m֝WHdgk|i۝#dcc$IMɓ*f24W2?,ls%K»⒇'弣J\(+>+@վ N8^ѹJ)9уdwxWoZ"_ =Rxi :y#6](iI :C|5Z{K(/ESeo=nU?HAkq]$gNJ:=Dt-XT=CHbRWIL)#(A)<)FyDyTa^$Iik2o T65YkDl/H9+ @% mtL:d~u.P8(pﬨsn{ gE28WeTIV UҕBV?d3eRT7d,x U~ N LJ)~2I)hUARjeHq2bFY:yU1NrDI^o8"kֿddăS%nTS )CQqc,wNɫ(]%<*}nd,Yz3L+ibtm BY:|DSZzF+<) (F=$@ `a//) N qE:%+i&nm ҥ{q+={k7ʴ~ zȓ[%.+%elkك?4 a:QZCZ+Tb-4U,Te[{aÕh}D${IѧuuU|TpRYXT}5JV63)WulSvP- NO8paN'CU[2!ym,OʠvW+ =9Y5}Q7܁OcȰ;tӬk =dX/_:e S/  hPa2~x?.ÞH*/<o|>] %zmHn?2.׶SaT-+.7_oy^WHB79:3-IO zΩܐ%2( %Gf}j %=׭jI9*emMɝ_fɜ̇l(tN3rWJMe$r]8IptXߙ)P \޿1݇Jvwrc;~r\- ޑy DVW"9#8uaAy˙@ \"nHY? IN/on*f\Fw7>b4$66N+VʔKvj_JV&DL$m?}r+V)TXңIjU*YIKZrkKYc8ÍUl,o\ho̽C%sq4N'}^akR˫k۸PtlyrQV ={'J:"%gfQ9qu#$ {>:cܮΏ$@$@$w$iMSn/~BŧݬbƥgN5h=wU$8s*o qPdK%wĨ9xQ|dP%OIrGu]EGQ6rĖM5U+ U:^+',{sO"3q_[\TrqWt//^Fd㙾뵟,.IZ9d(2ͮLPnߚ*~\%a˓5(212!AFܡ0e9rŗZ s:6]=r+#~"`'阠RŰ3ut5J(sk{ytҮ4;"I+u1Nk xO{޿:pR6*lcR0ڄNWJէ|&K: 㒅V6BY&z|a*ոiT|UosT 0%î H aU<,mT+1mB01Hmz$GZʿ67,S[>BtWY7//{mG(!Z!H*:ymPZYPR<yJe)}㏺KGN"{}L+*+sH8F>#7=GͱKUrS2\َq]KI/V 1o ɆgΔ&*rĺeyt˕,#㮽<%{Jf)U),tvʶ撱ƹI i[ŝ*tKT*{OTF6d8V,~YewާIvEJ|B JZ[<8n["VjIoHt}75dl0zp"cC;b̊W  h,[(/WZ&#HrtUw2EK ~8][ e#V8sldd FWNQ 9WZ%O̥۫_Y>Wqh+RIC*ZbގIlQwPi|4CVyjȈDLճg_Mb\;jpSLe܋׼D,}bd,k:Ξ*Zr6 >+IF#yR8pjȴ>1=f[m#mϞّrd& $s \D'e?߫b}cSŵKd239*ؿ,{7rHڸe"Ţ22Cq0)٪\RI) ]N&=mub3枨۩XoiJCI/W8{\ekCJrϷE"-Sd F/[bKb:w} <׻乍^LN e9摭ICʂ)iR%s>s$UdGq$^I 1M<cT\DZʵd/q}wv#(C#pv.)-2ٵf+ \o8jߗ௹ۏ)wM%hgqte!VHd+zYUɒ^喝˚;xA>n{7iY+.H9?@#%E֧lsSk$@QWK}&pX(73H?NZt __"*\*h4:"q\SHxЛ9ZzchEV]\*yM(SD WIk*nAY5_Z(`lҰ9O,i [*ԡ/ՙyhy׋u9d_€rV4|JK@dӯOm΋鹱>ݛ+E92_%|6tipy!tV9+팄Y gJ$@N aHIr3]7_&sO|TD&,4*/ؙmJJfhk\Oum$"B$@$@$@^Pf[uʜ!kS=yk빞rg.%`&vԩ( W㧬&aa!7Q+ "rP~$]9-\Wl+~$1.$ԋ?[uߢj$^$畼Z$y5 꼽HE72'   !K wwq\lG%       h{Sy7HHHHHHHHH 8u{         8_Tԝ/9G$@$@$@$@$@$@$@$@~$@Ea+        8_dҥw^INN:,>/ZHΜ9#Gȑ#-";vtyvZٷo3Fbbb>HEEhBz-zj]^    s&qFX,2h ǻH^^tM.iժ9n r\fZgϞ2dv@$@$@$@$P_- dĉZѣG%33S tdggיMIIl޼Y>g_llڴI+pb~2g]    s#P^^./,\Prrr/wV̍7N~Gy駥ƸֲeKˤCrx%   Aou8=@7|pm<-[䪫t]ʕ+ZmݺU-ᅯJ"##J:oM6n:[ԩSk.]ai߾~Jt8%+u["   No'N?.۷owh֬uIIIɓ.VuP;vL; <1 >|X(CՇ&ˡ?Ȇ?|"W_}aHHHLCou*'pU-..wv8;x`J- ;+aaa( رCl W_便 r6vɒ%zn֊;bG!   &JIy袋0txWrν/_S2!RJKKk/}rs޼yZҥS#   3Egyok bp .|.CTTQkݡC} =FT={G?۹s?a@[_.Ty桴[ ({= ~?P dP*8,Vp@?Q[Ar. Y,tn&~Xw]kw@IDAT&~xp馛\u@YΙA -=)}<ѹ3k7BqAs.`g!b i裏>*Pvao߾2|1N΀1Pފq !,++KMwC I&S[+N1X_S5P\)7ǤG>#Qlip| Gܣ و{L:wCw%8٣e/(Q ]ľIC|bǢOB!$멄,i\=2qGG!7W(頬sv@\(ˠC0@0橠H tPA`\0qr/6XA!''x1?($ 3f8w{    / =,SNuQyyE:v9eh Yqgp֗, $`!   P!7EN='dѢEZb.[qHH[r믿K4xFbb,:){.m۶.oB$@$@$@$d'|R c#14;!>1E+ yu%7-nZ7&ˡșW   0+BqRj" 2vMgddȔ)S=Fԩ>ePo'3NcQxbT<@Cܼm۶J;yd}_$@$@$@$@ @9X('Nt4C uABbe4<52>S)d3g:<&P.! b߽[!   Se5P6z*͢"J2G oWTIlLsꠤ\ty4Ulk:BUsrY}A2qB ]aܧ'Q[=%͢y<S Df"v/pIHHH  ) tK;J$@$@$@f&=HW,{bx): 49ܖs$@$@$@v bQt]lڴi= O$@$@$@$p(˝4B$@$@$BҢ.$v        hRkRŒ +*ug8/        &E&\, @.Xw"       hRkRŒ +*ug8/        &E&\, @.Xw"       hRkRŒ +*ug8/        &E&\, @ľ/bnccce̘1&{Eə3gdҿ:C9rD>C[cǎ.׮]+g}& iѢ[zryHHHHHHHHH͕b0ZѣG%33S222]v+ԩS%%%QIIl޼YM8 ˖-j;uׯ/\z)9sHH@`JRQY.5/sUUҺ ^RͰΑHHHHHMQWSS#_w}ӟeJ[lJ %ڃ>(+WjuV{e˖J"##sC ъ:ԵiF֭['z:uJvڥnذAڷo/~VUUUv}РArW:w{  0پnF.nJN9/?. |8 @ck:J0ByݻwKVBQn*++eСҡCپ}~{Jtt9Ƃo'O;vH~$<<\ W_便<%Kn֊G!  -hUcھujiqL     EݹsgdϞ=,> 74(^t_+:$OwK.u4袋^? &簾Cٳgk>};w $55U \nC > Kp`v#QC(IueWrPBoR(wňI$@$@$@$4MQPՊ;X!Wo߾2|设j01G}T'XYYY2m4eAy4i|un{    s{op S?2e~ndԨQ>ԩ7eDC* :pwEŋi/yiݺ,[Lmۆ*/H  hLU5U,^eeܡ5~5HPke< q JyyyөSjl֬Y]TT$+WÇa,wڥܰa ߮C򫯾Z.LIIaRy%   3𛢮YfB#9Y-ܢJ[\] ~P[`-ܽێϙ{%!`5l N{g  XEw_b*t Knn e ժU+9~K;(pjX_|렼s0@{Z/;;[pZXX[۵kt}|:;d޼yڭV}GC-b]yWw.&Lt8`BpڧOs% ¥x+PR Ōɡ?X ;} }2pb{ai;'h;" ͺO}q(]C>󻢮!'{}sΎܳ}HHH1TUkb)(͹Ƀ1>YǬknѶ=Q1cz&_LbX9Pyz93[&xP*{={.:sƽRѨ35d=q?ȝHp~>8l{doIWȩ3E2bwy2m u^ͶOf`;ʴis$  TG>ItgNcr@ P?GvJV=u>*oP:O'cWU ~ AJ9$C6YxG8[@cƌN}]^IHHQCJR~q^*ǔu]VI #W-x%  hPU>ca"\'wZ_G,Cݪ;d C/?P+о 9ytE+?JAA$&&XvH<EҥKoJ9M6Ɂto]ɿPHf zÇw"  F$`U1}2WL OnrhI:d$  %`va!uJ vHHHH#FQcTBSL( +|{xC$@$@$~ኺiƩk=b$@$@$@$Pj ΤJ( –TTᢃr?__1Oo(>qTԅsy$@$@M#,3Xpo6gG$@$@$%^kr} |+5a5RT:S$'.O.d!g0*Ξ[ ,.qxbO|1eY--[ʁ{c|U! \_;|e2hlіy*K%uMwr  `UH&JV) @hT%أ& \Yņ$@$@$@tz$; Hk%WHHHH TVW֛LQcaiתc}M< Т.7K# ~UJMBWVHHHH.2w2FmBe->R_3>q bQWSS#~˵ݻW-Z$gΜѣGK=r~[n]5k׮}ɘ1c$&&F>39pTTTH-wҫW/ /+HHH X TB k?.Kl&r0 ~    2V?lt؇ 4E͛壏>k4GL0`Vҽ+][IIO>-[&6mҊ9<\~XVٳ.s̑?=~   `':ĨoPЕW׌IHHH΃d SNJ<+~??v옼ҹsg@-[UW]%ރ>(+WjuV{e˖W_}%%T uܦMYnzr)ٵknai߾~Jt +R/  4v}ha"ZIE]$@$@$@M@Ee\T,ZrPeEEM#-W:>2~x[*v-ZrR]]Mee :T:t ۷o 7{WZ\\,P}kzJ_~c{L$@$@"P]S^łp&t 68 8$@$@$`   ¡0N2 @K{̣KkBBvʒ]vXuM6۷O?.-Ҋ8(~a5OS/~!kСCݺu]ʐ!C}ߔWvlEG`SBphH銊OHtXlCsV}mfQH>YkLHHY5E]YM!yfi֬C?({g7ν/66VtҺukzPAY׮];;v<{ /*_P\(c=o)L{B!k!|CC'Xer5HHLF, U3#([]]-+V_ZQV$dٳ3qx\ w4wߕ_͛c9HHL""^ò#KGE]@qs0  p% aqb.W_}]{~;ܟ'ٺuCĒC5\u?CeQuvn6.Z1KE$@$@@*a !bQQ! r8 @Uu* ̟(d SVuHf]lϖr~dddԉ߂,N_|!/o]īCR_|Q^u $eQ[ft?̘1CWݥW^ϼ!  @@Y5S$$[/. y F] >bd~ ]1lVns̩oÕG Z:]dgg?vaEļsnٳ%::Zn(2{i+:Ĩ3L$@$@$ p} LU 8Lc,55L8& 4AZ ,J7A\r  Ksg:Qm1e^q$@$@$hj.0:Xԝ:Shk O11G   j@4Ǎ`eO~d sB$@$@#`k<$,rJ:}Ia´;XlJ$@$@$3Hü+3s W;O?@[  /$uUTw7Z~p6$@$@M\"HX›)Wf kB_1.HHH H;f*F]L6u 4u5pL<6I4u\? $tbԕYK9}dɑ5kSN_Rڴiw^Yh9sFF-ȑ#-ܢ3z97Xv۷Oƌ#gɁtF-ZH޽W^&,$@$@$`6.0_#Jd ΑHHHL*%Ycr03}=y~;v̚5K+~)//'xBsQ̔h%+"u&[RR"7oO>-[&6mҊ9<\~XVٳ\˜9s?vyHHH @Q(T2*ws#  0JТ1-9͢s˗/w _r)ٲe\uU?rJp[n{GZlWP 2D+[NnV=]t 6HoזxPw}4h\yF @TTKD:,Zq    h *U8%JרRV|k {LkE((`M.H+v-Z2hQJ:ttAo߮hw{WZ\\-N<);v~iByW{iiiZ)K,}Z[-]A%ƙ0a~;"g>}g;wweIjj 8џ' 7Qpf=@qIIxW5U"#{|/aR&#آ6rGq\ǾB#/=XHHH@4P%Je~EBQ1_uoj5Ġ"cǎORQQ!}ˏ?c]}Ղxp}GuR (ԲdڴizLX!w(I&?/w}~wܸqӼP+&Q#XΨH4o_]%)XOKB]w IHHH*8A3zIaׂ 6kL+~K֭@+0S,Na :$馛tp5N!ewPhcUYk $s#*E`3F|Jbc-XuIiqOߎ@6`7ܣǮO~@]m}?5@Hκ?u@(}:wη|jW~J1o|=nPƹ@}l~& hZU:a5 bYiQ 4SKJ|lN#cLEDF.F/*VW> r~Wzsu,an @"׃B/0J7HHHB{[!LNJ\x)Sq"$<,\NR8@a311$u;viӦy_8w  !PU̮!BN#>#  h8v/:!Dh:tAIL.|L$@$@$p* n^Ld`%ΏHHHTp&fΤ&~:`d'$@$@$b+j!xRQ 4e*DYd:317:sgO$@$u5RSSD9=   <h"[$d2`'_ o 9uf i_bQYH#Gʕ+% W]Fĉnm۶װ:ͷl"=Z+ӧO:XA$@$@f!cԙBEYR^I:hHH.@ f?-/|!$ ܹS(Ç>Oveƍ^UU|KRR5J7o]7m$P^Z"##۷8p@VZ%ҩS'ke˖.} -p{뫕$}|Ώu>! \LB3CPJaoo%''GTVV֙ `y8KNNBPM:U֯_/Æ ݻwC=$Zo̙3O>ZQ7m4~ꩧ$11Q';vt$@$@$Gf9%$Е9 $  0Ka5B)a*ym>̙3%vұ?H6mk(.rG$%ǏˢE"J7Xj?~/~!kСC nݺI׮]eȐ!qoJJJܫLY(ǘ `ۣ3R W yyʊJK 'FꢢbjϨkpHH),1*ئm@3_W7n刈HaU)3$ӊ:$hݺn6PAY׮];;v<쳂]l>$3^CqMf oy#\ܧO =.CWiTעTKDO 1`ڧX_cIƠ1IH 43k>\וlzN<ٳs~YfZA|VwP9+Pٵ6??U\/1g \_ayZEy߁@GE] is,  &G@+dbQWWbs!W_sH~1`]?InݪopG}wt츃jW٫eڅhͺ.hxv(P}ǺA;7\]VZv%"   6C WI̪ [u 7X C.QF/^zo}z)uȑ:9}ݧ!+b9ĵCR_|Q^u$ٵLbp͚5nc2c GWݻw^z9>HHMZGT3CT E&3Ny9i'$@$@$*ib6K0G}T,pq%_yʊyYZKKKz*?ϴ +dٳ%::ZsѱcGyx#  !`Uj ebQ8}wj+?C>SAqjz]wiwtw^ Gȑ#-ܢ0`cƌZXlѢ[9q$@$@$h:bY);b ?Sd)Qý1e^;۾f|!  _DD_'gquM1lk: ׏?(O?>=zdffjW(p⚝]g8MEO>-[&p).i.#9stHHI+X@cO~SdklVHH )hJ 3TpWXp#bRRL8QN<)[lJ*|AYrW 8"{t0$p0t *qN֭['zk.]a1TpYt +R/  7|$1Ml1ꨨw;ȕ8e!  ` `;xS/i%z'._iYP Rwpl$YoĹaСҡCپ}~$p{u=NZ ;vH~u]aa4{u%Khw999F T) 5X~Dd81f!   %PbEFXj+."q||NouΣ qZYs\hOMJCӧҥK]\tEkTBI8a B3Ab\食ܹSe]&2p@GnrJY(qΕ;kR~UWWHio;$@$@$@E1%"&c6Uc=&;wÇkօ/.po۷̟?_ǹC,Z[9[:uJy I),] ;ngƢ~ҤIw߭E<=(<O=3S](LfܣԸmGlF=R|cǚ"kf1/1|W؆HHH#py`u0n՝_}q:u$''a52\fay,`Շ6F3ݲeKx⥗^ 3fMx%  !ٷytXzL&`% TV&ʠjQn*["NSozJt'NȱcǴ[JJZPP :5\3fs_$$$ȥ^Zg{uU 8Èsg{̭m۶r7du:g $"Uh(!,y @`PJt Ʒ;7 slFy'wޒ!SLݺuQFM<^;u$qqqr7;b!7P{7>Êq+/^JG֭[˲ed۶mNgff:oZ %!`5l !8vZ$@$@$p>$"ojg Pk%-  <dITY_ЌŜ(̼Ŧ7>"q%pIHXԙ)Mʕ -Ӆ'-ugo1iqCYZӮp.$@$ [:3P\1 /0Spp&4cǎ2m4*zW89  I f):F$̲]' @\1TatvT{$  s'P\_NjITe' 2F)? WAAS  % Je":I$@$@$(Ƞjp&LJ@}=>u~GIHHFJT2 Ũ$jL_   p&Pm;u[h{*L], Ǣ.R HHHH@:Ȍ0k򒨢E]쎊:mK$@$`U&QI<_0ΔHHH pjÑʢΪ걘u7ΚHHt:xzU37S$  $JuMxITTVe*܀# >}5nL&g?$@$@$@B1•d2VѢ,UgTA   * 3j1g    ZH`*W%1k+IHH x $6B 85DD/Xά<3@IDAT(z?\H޽]sN8p >ܥCUU|KRR5J7o-;7mڤ|WHIOO.8 VRԩ\{ҲeKH?{Uqow   hPIhXъޢmhEP[ċX!)-[ -X^PKxUZZ @y=&lKv7p|f9ߐ l+VID“gԅg$@$@a@L"fŘX vaнzN/3骪0tvesJKK_cc>ˬ͛_~?3f(~cŸkǡ ;;ԧ4 A  !M- Wl&Au=M^K @w40괮N&TiUg?_v5@mmg?я];+p)| jzݲ2Kb `lڙDHHz@8vs\!]eףK_uTS /`fz•44;v J5O:jvGtÞ={~֤尪ӑS˩FU 6_WF?OXH$@$@=Fv3u6\ XK1@ 蠨^ ,p'Qmԩfٳgy/쎣'?Io=4h'0O7P9Nߎ;?yǡCpB=@ & zV:Ou&MRp[t8apUڞSiѿN {(*ƍgo|S |М:N諆HJ1c7}lË,H4,V/ۨ~]Qz+f(QM&3xtQ\M5jg?ڨ||þ֓/M=1WfJ㽮|*32Wx:ث.] W6}Y3N?  a{jnι5Ís0{Az X'ȩ :q]js>Ϙ #THتKiU۶m{1әy5[OgUٳg>VϛDX'om~lo=(PQQdHDbbbIz=^|\lq}MPy~Sk*ё @ 謴記`_󏉶^_zGb蜭lۙB1À]QTAiX._ e*7AKS.]j6𬑎Z wBwUΨ7f4:+ONMMnAn(F!?u[z9Ν㢢"wX]Z3ΝeӸ6mo  .FyUݢL>]vt$@$@$@$W8M=X4+B$@$̌:E5R\R\2.}d=HH!&sHIF1"d3HM>6mZBMY$ƥrs/,S[e5r9mN6etIlHH 4DW1TupV]tOVHH?+vRUdI%,3b#h]blu}êTԅUs$@$@DIw},+K,#  /5`K릢&3dk_rb.VHhpHwzi, @_",颣%tZΨ&e]H ,H@3<ͨB]M=u}K$kԎR,=O0jv繙Wa0~<(*sSI_$pNfVՓe3G(&>~)9z^8յUt~wP,Wʔ` %dDD^ݺ#PQJ +?Dr|*R(16+jlU(Z+;ґ "pq L8Y}y U%wF]B?5lԽbT=&[-%乡KCpțB){,Ɯ eÙPQ@ @!p2eխW|Q9w@KV.1N7Pm:@8eĀ-YMK#L@E߹3?I]fF\|U(!FfW#@E] KD$@$&X C4g[Gӥ'UFjX/ $pRuϮڦw4k"K+˪$w6uf(!#20*tsk/{ n̄Ssq,GWbMsV%%x]JE]wn.hhq8S{=܈ahl|tj?t@X)aCچIH@ 8QUˮrdWҗ\ǚs~CZ3Kd-O9/Ő45tM9C*t p阀HJ@g]yYWti2#l&AG)35BM:$Ey<))323 uusUphᢨ32)vێuF_9+7 ֦.4ۅA0Nox)궿k*|bbSV >&έJU^*ە<.djSJQ{!k>}V!ΨMvB@)֬Y7|>(n݊G?bbbZ{q+l2L4 'L+(ю +*Uҵ؁%W:?8lU%2HbftH. Ojf,k$Zwoz)|Ƒ#G,ӟiikSSNG*Dz{RllHp$LCfIg="rPs%z Omit\w7ʛdK %Tqݛk 3k䌺PlR%*Ӎn8p.7lu vРAعs'_~m2wy'QPP`fUWWc۶m8vLl 㢋h'*;gOIkȒK_NE]wAF@{qIF(K2E{Qԩݽ7>e>tWF80%<`F .Q`JFYK?Y#b0#)bcq1ӞHx1k,;drٮ] }W0`:Ug>}z+)DG|Cӱ<NU8v2bctF AIцC9KO@zf&ʎ$c_l''ߌqo"޹{j~}0IJvk:+Š}1^>N7%bYO!Z$~_og><(//7K/ //τ ; k'B]ބ1H8߰çQ?$ -rCK'aC=(yڇJ2Z!Q҉ZWYqLQ :_w+L ] ;vuCڕ+LJ%3Kj@@TV 2 saBP _4hC0+yߑ|uGIΡnpvuj~ª$,}hI#,gjl ab;-)=+IOF3ށZdCgIstI$urDЩ>,Z^q5ufo@à<5Vv2F5S$.ǘZDMd_0G}+,+ <\%}c ϨaU^;CA;`;1b7vw]\Q'}{wo?6o*o8lD$gϨPBJQ 7쒓etRY~zT'O>ߺ4SSv3f0pB4λ\| ZK3q^\-%J/[^m_yi^v10WQ^OZ%ڊl|Xq:w%++Ϻ8BFSzw>EGӂx\U=hgZ&]WCa޸9Xu6槖W}oU_s$uqurymmrңrF`fuob.+L/'̴蠫'XGt%";;x*GU=U{/>ϻ>s&qd9ͳi~Kw|Wz9{r+Lo1v_a՗|\Oz]{vhguh=h*r&ow=otTf (E<@>@ͱ>)J^˱-.Yd.[_zM͙,z4e]jui8O;7v0)>|?B盯3;aPxnIkS:-\#ZP! a2`bɴRDiYZQ899k""U2H-EJ b{-L6o 4ZO |}$}?rVMАhp8XZٽ2ʋ8FkE[(ya8N6c%KᄄI|␙IɰAo$hL䮷$tk;< |&yy%9u/D}١V4W[@- [L4 -a4}_5HkQQ&Gs_uW jvvQPTT$5=Co_7t>}[(-gTm{x6bd)-64ޟ´.ohs OH)bE[1 "!! -g⡇u]gr{(9Nm-Y6lWqf ü9 * :kV/YYfpˌ`eee"tzIgjT,gQeTsvkrQ)P*o寯D^GE^X#|Ȼ?}{K߫ep_\j*9dDr2ދxL;Ur-f *T eiS7j(lܸ?8oߎ_WX< t*trr姦KT^̹#o[㯂]2qn0y˜$@$@$@$@]"}_h 6=]RRqάSYN7ꪫL4^x3Oө-br$yN$@$@$iD&v/=c{zDyH^SϯV{u_U cC{3SN#LBm 8ıFyu&ޢEZ-ug   1bk*Y6茫c*](r];_W]^sjĒ7 OW>>| &L=+ evXA$@$@$t!x3,r Gՙxwޝh: O pGG$@$@$@BPZQgC͐f_v0cxӁZU}0KAYΓ IHH?.6]% p=5*E @ @x:d6re   p$߿NLK$@$@$@$;t/^W%   C d^*$@$@$@$>-UZ<'   NL3"+**Nhfڻ  0;ܹoApCwYf׮]fqyzru]]m#0l#_yV2Ʋ⨻Iw>*--Ŏ;pA\r%7 4Ⱥ4fYEP[XKOO>9˛Nf;xͻJf1 }:QlF%^%m۶oy?|_ǥ^jԙwHUU٘}wW3]h`nv?iѾ/ӟ4۩6<ɢl'O?xs8]Fjlp8@jVy:s7hA Eu3U8y$,Xf"m<Ǜ MӅ BsF0tPDgu(b䧟~m/''3g4ܪR;IwߍwyOŞgU ::O08fDZΜ{gu7qY`w/`ua ~ꩧ,}u@0I _͠W\1 0-]}xWkIHHƍb碋.2Y aUWq_x*̊cF&L0hzѶ+1@0ȟ<.O"v n: 8G3ς҉c0HW^_9km/N 1112.66n?N *WsN}Yzt]#6(ﮕcw#u~;sy8mBy:9sd`~{ 6qf[otɑ.aՙ:zjZNdgyZ4t]#6YJ:},.kéF֛<؇wn'o;.R!x`*=Ytl U?5K]P/{Kz#jgN.tFc`ZG555f94k,̉j.T;Qt'v%*:tjoKAO%g${T~flgffzyUT`U0u@0HK/ܹs'?F>F;G?1 v:9Ѥ{6Rf{|f^a#<5ua6KԶօNWMڻ:jK.`/\"J8v>RAAm@GRII/bdggG X`P'05$UpbpUK-<0fv6}Z?2|k0;܅6n#N'/]شl#_`ڧ4+V`̘1f65wmm-tjnj#g|WlB6D?8,pVau qAw%Q\c0_%{>P .nj3C3u@H*̝; ?Tt'vj[̃^"/~ [?VtƁ;ēFtYx1r4]Ƀ]+%StPtJMdEe:O m_~9MxƏ6@B.-zG6Gt9At Fh+++WCjBGhun˖-4y8m+o~va}q^ClO {h]F:BO::G y-I~k#MAsNou'TsЬT*m\ַຶt#6<_U+!c+`5ؒVZe&,[;I XmP%:aAWUGTOK_ P些:r:rDTjUCf_jK%V~<F m+oa;FǞ%·5UӧCw @0HGrjIq٫E$c0IK3TyNlxc˫',X-b̗z"G7xc?ڳs'nj#UK:Kϥ`n꾾+cyf;y{Ke9:G mvu|7 '>D9sE2:9p>`y#z_`*鈫5Uۺafxa .,\'Hwz\ʮ4=FО 4}^rH}C:gs^odhSqazRDW.L3 /nzD"mBUn^޴d:*3/ BRQ IHHHwhwڄW%        Vk?HHHHHHHHHwPQ;yU        hEV8HHHHHHHHzuÝW%        Vk?HHHHHHHHHwPQ;yU        hEV8HHHHHHHHzuÝW%        Vk?HHHHHHHHHwPQ;yU        hE+([{S+QҽaQϪvXHGU9J;+Bw7&Zu  a#{v Q,:չd۹16 4s qػ`8İcX^e/ۆ%ΧJ9ƎmEKP\% ^%kixvێj @_%{{=KO5]hea"{ Q,U;~ej7wwX0- @'wuqHluR̞%>0~$uTvI Zk.]ox$%q1q垿f\n`nQhu@l5B죭 a |ǖm\M`m#~6>g !U WFWoBlƃׅLB$@I 64RUWkž2oPv iDdWbe.އ̙>?:Tʌ<& 8><ی݇Q7N{ f\n~5c 9 Ɠ7i|eXt+=c2$jSX|v&]yOwaP) mgq<)w?Fɦe(-Dlg*kyV c3{u(w .\UYśR_K_L8~p}Y J-G9ZcÏvL^ r$=}Snks`d&l޾Ǒko}2N?Fnz96KC(Lv*f ~onÎ $XoV ;0s܋"Az[P "y) 5ʳc'aL~% %:NMO C$@$ߋ׭ye^=UHq%W`Nj[L4g&&\Pf,Y(OHF>l}%eI}ӧc0m=ZSU&2XG&Ix*irm-2K XrvAw!i v$־\|L1 YX7MCK~+yav`Dʹ~vMpAd-Wv`ٚ}(|ٮʷ`L̻';zUiCKZS! Շ< )yw LbW'X`z/]Vu&nu1: p)rSִ*~ɷö.7ŒN$:*"o~^W(wC͹.hyx,NjH(C"эi7zY>lg @ ͌F P1f`3nEPX%.)'aӐIFQg&YI;)^s थ -͎%CQX<4Ȏ}kX4ws̤ع K6F"M-[^IF!ZݘZ އ|~fṸfX)V`Ȗnmo-P55{)ӑGSNQL}aQU*Y0v$k6v~b΢EDcZkkOb٦JLzhO^;oڷ0KP*{xQ¾ (?r/>2g釾x$u<D^U1m3i GW~u!èC FIq7KQ8YwUO 'fZ@|Z8_aN"斣x\SSy91V.A]$!yW(&3\a:S%XpY`-r*AXo(t8wy7|}T%`WZ~XQD2TBWNŪm5_޹WbR$\s}E0DJ"0"Z i^udra]ZLSlyȫt3PXU m< UCV7d9纺σ(T&V/"LY.bO3Sbk%&z1H;ڳ)x&o+f2_6I^&ɔ \_n}?Hn`ζyit$ݜ3YTz_ryȑ۞݇q+2~k9yQ8w}mo;9׍fɅm' \pa1naρsِOO~U}bn4eygb8c0Fɻk}cèyu;Z’;S/' .DP E' Dڸ%Ku[6B^I7e(wljeǮgK7 3 `3NmI# 11Fxy.<^<ߜq8sVO9G*GDh٢͜ \غ6ZQet/ny)ߚ#ى &mBmZWKz .m~<:%&2\sR6svl4`($?>i^NKWarZ޳q/o1tMl*\6cϵSnn[8G,-735;iT@IDAT7 o7`м^bR&r/2 ?uvӔ9"MBz\5k*!~YXJsDRs;l(4Kt{'Pi]wmL,3iOLbC$@$?'|?1v9cM^!~#axҲ91Nb{Ƭ{ȓg~NkAޥ.ml!捕xw  v]QHHqɰ&..lQ*',s;w%M-R8G)6 .K/]cχvL; snލegĘ4M[HIçSGOͦ8C4]#ˢ& W#OAq^ѷaW0my?mu6l.jUEǾi \]vR&8g?Ju+b"T}/})6l~|) 4)?S/,T΃3m)?'*qYhK\ ".wG3(bG:Xh L-vWunYwsd,yKNo;t i+`=KJ1{$9#>WWthR: ma;vp8oe%'Khʟ`2HhͫDѼsut`J:ͨ$Z *m)-/g۷V]>Òfʋ/oɛi:}'bېsN u+blhlz(T(X0*qlq&Mtٗ1)] ؾ'ǀ(/lY*ăKwݮّ~+4E(J=7sI烇hrO~jŒQتM'Vҍ<_fIͲdDa%VbGGyGg(YypV<:Urds(P/ivItjkLMlJ#ǵiriq*t%12Y7wnwxB$@$@eрtAuQԠ_sܦ!iJe#BfX/Y̜;D4OBf/lnvO.p%f)AJ=3XB6<$:d[3 RB&f䏙ZWBUi}=4Z.l.X\"T N.cy_,Ec2^WG1Nݐ{:'ωX{*|pUiZmy!n,SVv>Mǎ,ZD)WqE]6fp*]N7{Bw .ώ!t-?~$?gk9v6ϛy[{ݑH$@I ,u6ϑhbt1Es>{׸N+^ J,,]rDx|u fotŶ|־Ϣ;RMt:Gb9 \_7_1mM¸etd9`^QӾus)kDE>e3۵L {gcƕZdQ3#֝X+re=! ug1QO_g$@$@zϼEtEM-Oo}W 4N"n%]wsxPuvï%ݴx}T[Q9Ah+x)d*O',*dΚg1FYm]Ϙpތ܂I֌¶EGFI4q#3G/_'6vyeZ{%:f?Fod&q#\g\Gs@o\OL!g8;"zqg:tV$ ljmݏ]BpN7cܧW2|7JWޮex)s$v&,#[YH$@I ,uyq sX]9"eV/,8yXp>!ybﯻO1D$@$ >V&ч' @, m(V=#/l).Ouq+۹qs~'MJ8SfCI[wk?QWJy]@1EJ"&rlxv3A#'@Q+"9PQv@fL!0Z.KE^Eb\q)r$)X}`2SpyNg󯝈'.#?5Ev#-kìϋJyӆ[ds?Xe.:dS '[p\H:4L6`2yu,/A;v/Ppi7X6Ht^XdW^uq\Asg|PTb"B+gG{ȸ$iRmsFb8 Cz7mn\w(Y/Uȟ5yK$8]xjxتKI;1(.ǚݨ7͢>Xe[]G$@@X(ҒqcK{7fZUsخDPiEx,J,yL] Ydc{EȘu?ו!*6 ٷPWy=#Sn+B!s+`ݧEi$8d",Meϗuf*EuBxUw]ѽt,uȐf`.ĽEKu{RL/FuFQC}cPT& k\]9V,j(>YxB+TPHX(/6ɻ% O黜`&9G*JO6Mv)X({eob{KAB<{u<)_7hٽrٷf׽h~7keާڞS2ǵ*L6}b{*D*NrP,%ѻ .߭~ҦI$@$І@|lL%ze;54 Ԗ)w\72yE|}U\E#bd42߁v;.#*Y.X Q=4.YFRI(\["%ixHfny 4 W³sǻ/x_O8d}"JG%>?|l]G MQgΜiY@%zX[@|\j d!-Y;zndv6 Gmӆo-CFT[F>T?u{-'0T^Lij Z%sH۫Z Zż@?vhMݼ#&I!Ȟt]W ŵJ*4CĬ9xw,:ͰRv)c0j\ºTv @_&P$+:8.Ҹy{:'o$Yq}c/.Fߝڽ?o,ȡCfݻl.}o=?ip>j6u`wUƱQAWG^!iœUy_:`?$@}?.,fhRVE~Ļ.JO/gP963R* "ysQUnY `$ߧҰ3mbŚiY}V7I1,#ׂs nmԦ_\?i[^&ĜDLL8\>#*<0|% 3)d),]]r1nV~ *ds[\ͳn^>׏Z_9*˳aZVS_}gtU򗮕S`H " 477IpD8_{:_$ e{2 /ꔡ0xD?9U6x]QdeHH3SM22w&)@pTʠkf$,p Jlj>OHH@]]9ju.3y[=HHHH t ^[k uRS:g6Rnd$@$@$@$К@UU 6C FllOs&Qך @HHOQHLJ4Cp, @RSS 6c$;p7'; @5T4)CĦ5#   iI$@ѩ  " 9IN;Uԩ]KHH6HHHH :]zk0J:i$%9xBZZ|[x$   9Ѳ$BvESaNuё >Egs"۩+tZ+3Nq_mHHH"s5( ]vyU%&QunȠZ @:.55))9qfAĘF|XUеUq/\'   '6L?n2nhɓM] UU9C{̑HHHHqQQεf ht4^[3gb>q⤹FlPDE|^/RʩNYJ;,Ɍ$@$@$@$У/iǢE>sY2Z^ Quj*.ZmfF`-{UA EG   .d誮H[t 쵮IjD.mĞISS ni; @g `EYjNuld;Du-yF||5E3S%]måu*"jP`|    HNom5^M$@$@$@$|t+<˘gr2QZ~xN$@$@$@$:| SBHHHH"`lYK^SөBV\x<'        iDRoOŜё bSI疲N/i̕HHHHHHHHH@ zb͘SuvS @ uV.S%*KWLCjZb/߅%ˋ%][NˆhK],WfZu|;t /f{Ϧ{6}<#  >@K}UNd)8;U&= |TS?]XXmr݀lL"\j)4D?8*@vxSrl_yxs ?e`ml&U$Η*T!g9KagH?x)9X*ɼΩ]Xb?Yhqt)lN‚-̻ %sk^s*\A2SN-ylon+ys` +ExkW{Mؼu0k;QN\N$@$@B 9]k" 1aL51Fzs-?jvYr.lFÖś02SٖR6r,_ _R'"Y\o2-V?'V/JVuDhrv9.$@BzW%VRڵUHJors7"'3ڃN70U{w` (= ͹sN@^>oZ-KLh!OʀK|)AYq GεX~k^6&z .Y>-{PaFRmȿ}f==#L/vOvơFMvVͿ٣elU\Sr*9<3AyoV[暎C0-lx_[#X<\.V9>eH;H w eYXbrKvw2yȕ_^{LYlL_sY_1asյm '~`cK#ۗa@mEXeUSV,6hIE2z X>&EK1~͙ro-FNrOhuik~^^ވ҄qXivŊ6`S "G6Vbq(Y%Oe8? HHH D d#<ʻ0Wd*[ٽX5$֫kŽag3Xds0Je12(~[1׊l'+sp|#(2X7iĎu+A#u1,g9倊w'[ Ǵ0% k>6#x}S^ɚ[Y2Dɲx lUs`dn *D̛ޙGU{YYB 6H!GTˆ ޑEAuD3 < 2E\p Q&(a KBտ:9%I'sTթ[|r+IQs9;YyG*&Y2W&\"MƁ\uc9991S1MjY73]woVgCսT9u,_NF:IwU+Vu_yJ|j} 8WZ]Q/zN>T|Lߖ<',1uHђE Λ/sj[1>ug6IW SC=`! eITTīdfK:|圡kpL5?Hqqu?rXҥ^1['S8t)%U}FM[Mdr%uoht8{J6X1<]Je?gVF'J e6-wwI~ hj {fs2{8Dօ;8;D;< 4i?[ʚ9JPQԎcSkR%tOt\xPN{LrϢrm8}9ojԕF>&1_)dĤrט4~D_TNZ(̰,sN$@$@$ H-˶=&l R&b͚P)]`xRYϛ&k6:ZkLJpa<ף('X RGl6% Pɖ3,T-WkVҥPRrڠ\ W!lr.Aa!c@9gr2ƈ7Άݖ~PiY' rzjI( ;qYJIwQ_C䟬Jb%Wʁ#Mf 9|R)+]yJIq/yk.^҇ uP1.ro_ҔUYٞ*淞r;l|}>ΜLЮŒ5gR;ߍ݌n rd)̑Wj 1݅W\4Y ~.9JL֦'+AnWK*ɺ#2\\UŦ^vX)?y Y] PNy~l[M~tPgX3|PN.9$ ./ݍWueJk9~OT]1|*AuL_,RTli&Yf*4<۱l2x-# Wp׹zv%)eU2CSJ|A2{)IEY dɜ..]ۄJ>QƉ"CQb zː΢+B!7ON*LY9M!TU ]*MTpQ]=t훔uۖ%ϋ89E.3杤vdH{MT<ƥj!c׹')v6McPrS\wvvqxg\aAYl\h9mzKⁿ (CHѯzij/RL_/  f6;Dvr_˻])_67]m$ H Q.QN0dKruv R RfWQljnufQypLFYCzQ*9aO)kw7dv%cMtC1^9ǽpWi) <R7=֤Sc$w+śƟu3rIRw?-6I;Bmrׅ81.C6,&>گJ*UF38eZdAQGr=E}x\)uUsxh%j3XYjjk4xH$8ȊJ=5N[sݚ|8 &kFȐO2E&NS&JaRXj>X!RT*D:alyi%A Y2[:_үNF]ىdɔRj#&Y{Wřy% lKeMfC:;nCyז5W2X%mTrv%8n׵#F rU<8|dޒ*۰i\$H&b/!>3c  Vɐ3 ͐*lٶRyI\dgtJK5Dz'ݖ-G7Ts_R6vnۑ)yC!۩IpLSV}˜>j $o-mzSRWW6Q.86U jC4/2#y+YɕbT'3HrGSNJ$(%Nv;M c//! Ď?i9nS.[N'To v*Vjr 1vUKG*Z{+6$h>X)vloKдoL7yQ[Qߤb{CW9iPͻ@m|jelAʚlJ(b--K'pX,P\m:̷̠tQDA\+x;mn{`ۇc\ƫ?朁1yR`UsQ?bْ@ć@D,T7}+Omx},X i#fo  40B.::ܰ3[:I\ dbDtLPl$WdžSdšKSe>1S˒uON1c$ͲY'H*nK钿Ea,sP;蓤!,yX-AP&=H|1͒ We7S5ŴuSfJLu**p1R[U0Gq< lkz{9SOeMSφk@s )WϨa7",U Q;0edܺyl)3Nl'ܫ:ϗUFH>{\LO%8K/;ƥ^F%*nksQF'nPIe|zd)YP]N /ԇ{"{%es\p7]BPs$k2\V~z\@.@%ڲFҤu9 l庾bc%GC_qULܔ6t$U.Jy;B /#>Y^&q'+WQY{q2?rnZ8Br/U2h$/!}tPյRWT27=LJo{.:^sW1/)T9doC98ɸmJ6Κ(He?;K 2%V ԏ@x?Ν:JxLJ7j>ًBcIeʓ9ZIgKT.wϖgFUS: ]. s'g?v:uΐ/Mw(UlQo2_=G.[LYPȶz(KD.׸{U1D p~x'=/ǤLYo*M)5Yrk,A8 F)n *PO{#]Ǭ0MƏX[q<{+[xc3`6lfGib̹:c.&En%S=8Vq6(198q sz\r${GUdˌ?e{7zrTԒlH!SrJCq\X=2bTumTֲ쏶ɶhx)7[GH}jG6W2#I7|G=8pyT=ǘf"r-otի<   $I|P%tN_K99LJf_%&k,,&͖?6FōU y!et1tU\;!?$K%ȄhTc*nbO쥽\/ u rw8|Ȉ'UDF+SGymYIl&%#X+l6fP3ܥ@dWճP2R Q*!I윧7CcUpB٠,Sݦu\Ynޙ/Ƀ'SI2+mtOc_β2_ wiU;^Y[8~֩1$y\i6EZT~psUk.wEt:&g,W9b$R1M5zL[iZ uoM39 We綛"otjyD$(\$oMQ^02?7͛gܗdjyX-7Yeƽk8j\7}] 襣@0a.I2VO-_*1Z-!. InUs&^U$@^$9cQH 8|cR@\5wj~>GG!6?#1ۮEʿ~2br))*\HR~or/5t}W+CdKJI+;_l|cH4\fCp}خ6w`APs$ @SQP1|y=tml\pEֺdvCS纙v9>^|9uZtJ CYbTTT8/~ڵ}vR={$55UeժUϊ{F[ <    J-˗Knn x 4i8qBfϞ- 6T[n-:u/Xڶmބ$@$@$@$@^XfQQFɄ QZII̜9S"""dܹ;.rkZZ.--M kܹsZɷw^]_.]o8]_,1]_$@$@$@$ ޽[N:%=X0kW_ՀӧOXAwq[+6]y駟+в,PO1֑ :, %ɖ-[.6m8- X(VtݺunI6n輄q.=zpZA+..EEEk.Сkދ/(ׯ 믵nɒ%2p@.g9   h = l26:a9^`e 6Y("2ܯky7^P~Fu$@$@$@N2:(3sҥKUVN!ׯG:g}1cVhw9s+l :&|F@IDAT(؃$ϛ7O_&OA@u n `X)[/ʳsp{%>>((^ Ϯ"ؠ K HYYfx/dԩo߾ZFH$8.DFc!NN\_#0)d:W*9X͟?_nP߿_ (8XR 1kdWOpyV\#3;ߚ;#V>S|Xu`cX!{Z3P}RkyB D oYs1 TP͚5K["Hpj߾}l=,>7lo^#Ri|n  h,s}lڴi*mĉ*GK(C}v8s([hM_P`SJBŞ#u(dh B$ʪU+,~)+WD.H\B$@$@$@$I?lF6@UW]mYl(pnXf#<̃|hG͠W   B LYUa?PKTl+ˈ3b#w)%q". y`׽>{{_q{չ>>(׈S)-: PT\e:uGcHVeuP M&xL\`Z s:f,WF5σT3$YfQgoŬ5쪺׹}L|HHHH7wy61/A$@ A`;'hɢc^RQY!Q1& ]gK)_rxYCLc ` &pE cw7ƹ xɄ5$@(+[7IKŷSE'\^O-"{R%(]tlzr^zIq4 PdS{}0;Qp'ׇ>O3MרXDzF5Yk:R\Z$WȨ+kI)W֫?;Lj,WN_d:˲zI$@$@$@$@$@$@G^Q1}0 ʪzgG E:_dXO$@$@$@$@$@$+^HPaa*]lP$@MKϻ ˚%%O>śyHHHH@YREF7:v$ue +*pePA}%r5.IHHHj'P,"N1 @=PQWhB$@58YT b,t)%@$@$@$@$P+FF_E]cʙ HN3c sb)UNCQ2IHHHEb1B].&L|t !W(S뫜>JiQ IHHHA U@bԅ1k.'L|t !بAo\Pq¼]f @P%¢gIZ"*ZIcmSޓIPֱ @ܠIHHH7i'pd}o¼B$P_T՗ K\T+z1iKEz"   !uz L&Qo~H$u ԙ Ko|8{T)ylv    @yyDUĚ($@@  ?J:/:֟3{ @ kx]_aQWE@}I|V @}IY%)WH#F 6.}HHHH @UUURQU= ;bQQW_zG$P*jk$@$PGkIxf}-iFY1q9 L^Y&d|$5$@@  ? gҥUŨSOqu}$-U ;w><'   &'åJc!  DZ9Ν;>/&LPq6\nsYYl۷OF)cǎhSZZ*k֬>}ȕW^o[Jff-v]W^2d<\:Hْ3zw66*NlH&a/IhQ灇$@$@$@$PZ c_ XjQw/;~3{WVVʬYRmi&YzG; eŊRQQἎkۥPɞ={$55UeժUϪp @8D}"aQOM@6$@$@$@$N^iכu9k]x- ,R/;C.R3RDfΜv1"dܹ[oX~QQQryu(rAk46ln.gϖsi%޽{u=]toYt_|!DFF Ƹkt= @] ,:pviŨC>?"VjゅHHHHq 茯Z)Jf}m܅HԢ.p}믵2͛EumڴqmV• w+nݺM7$7nt^ڲe >\zᴘ}yQQڵK:t pb_KFFC=xhd8pvYHHQuZ%2"ҫK1]}A @BYEaQM]I|̢S+>,ݻ3<-]_~Z9gz-f̘]jcbb;9sW^qvCL<(؃r>o<ڿ0b7}ΎHHuf}EWub!  XU۝w|w}ZqR@=t萶~sO2a1^$))Iǝ,B>H yĔ)Stݻw… %]yyvs- k; E(:u盢Sx+ jJkc< )1YbcZ-{'8zJ$A;byhm`7ק >m۴2QX|W~;ÉmO0s{ @c@0&7ܘ{@!`bnZ(3޽{K~~ .d qo-or-٭ņs¢duh= {'Lw! O|Yb.>]Z֚ M@Qf2<&&-Z$˗/\-[L}yc=!8e` @#Y_YwIwF>oE$,SA۹sڠx۱c?~\Rڑ#GdZaw6d|5[BBWhsY"QǰCӧO{ァ-pogggcuPjJFEeMHCĹgWGW(ĹcvZŬf<<&L@x5uAփ$5k)a.aC0$2{C^mnҋKx d`P&"1 @]hET L&A%E O'Q7.Ptڴi`(8qkN v3K8Zfś1,P3h su( dh{ADYjp|y}rJT,$@$P~!qQ\mPU u! 142FHM c ƏI|֓ N5Fup}-,>)Q2" 8 0Hv} LQ7L !I: HH @'IDG\S=-Q$@$@$@$ЀjE]eUeΒC TL&RWM$` (q$p2*Y_]^ RQJg$@$@$@$а_p$d% $@$r钘iJ&QV^*Qҥ]76QǝX,    %`Wg}Ut}me$b .=H '!IgW]_"8I{aW+Vc @WUHdD`uzõu#$@JlC$@>GW:v}=y&ӚVQ\zV%v:mXlXI$@$@$@l7erR ֡DΖg$^G%.*^JJTRuyնBz&]khuR0& O:Y% 8QC" 4ʗ BLE S @dGG5-gŖ$@u# :|\~$&&ʵ^1gʲed߾}2rH;v\JKKe͚5ҧO+͗o[JffΝ;%;;[vDGGK^dȐ!brc1R05\Hj#+ g%eE*oAuިHHH|GnHW`WdD!E7zpO?-wuvW-++6m8]]۶mzPޙ u&7tlܸyi˖-2|pѣbHv%:tx{+nFF|Ci-YDe?3}x@$@V>|ܓ10 x> xr,<J#ThmOټgC~\^ΎHN&u΋?}999ZW7|#K.uZ[+pq5'0ͯƍzKZj%rQp]l(0`>-,Aވ]sL0A>oHj$wt:c0qyHHHL"(, H:H*Ũo~#YfI^z%駟dZavءg k_]_>DCa}8wO{O[̥1Qq_SkQ_YHH_zG7E6(KHHHH@NH&u)Heup-5K1SX%&&jڏ?e'NvɬYqP` h"ś~a  4H+ZnC:DAA|P qPVZ]aq|k>@V\*]H!P,Pa*`6$@$@$@$в hh0F]S}I XsG5fg\Q/_Էo_b K8P-X9\Tf#nFc\7"F~P!ʵ^+*,\c<& o` >):ɍ?ؗHHHZ"|HS`pmj? @h0Et݃FννUPDJAyxwos 0n".uc_   H@Yj0$=G"hjk{lϞ=Z5՜x_ O0 TQ? > @K!+G5!0BѣGwq&rw4&y.<&-Z :P W CCQLB 2HI k׮裏6)dޜH 8vsʪ'"QݻwסDx ]v쮨C\ J:rOW ԃ FbL+ B$@5yE]Mk$@$P_׈v&qXHH1bs5EuVVٳG 5j% .|󍤤رc%66V'Ocǎi&_xҥK~,)..=z9  @&:Gar@qg˻ @@ @?HPp$@-hP{znݒ˽*f̙uVp߾}2e +Wʜ9sZQ#<"2p@ꫯtyN$"MWPr Zԙ`H%]_p/m#6HK|ЫKkRRvݰaݻWǪ[pt('OʫqPM:U`w饗vW_-?lٲE ٧Oݻ :9AQQ{U>?|K<רWb0~}X1,(t=WtN#+,C F:EEEMI{ 4[URi]'t} 1%۷ohB}^W]usO>lܸQ&M4>>^*vu{|^Be]Νo~Z"""roZ g}hn n|NũqbB&P6V~ j=SH$"\VL"0~M$`Qn:oeJ.L_q/n.ru>f]wrwʒ%Kt;E 4-` xGTY5! jT@$@ p} #҄H/BWrNVseǎڂmK"{%11Q<dEȑ#vn6pm׮N*; @ ovrL&Ty_Ut} Օs 4((”M] %\} ŗ[ h.~z?~Gdi=s 4H>SYhnf0`W ,_~Y_C2 $@FY(6oެz=Ç婧uշo_ׯ$@MaC"%[0mQ,$@$`%*ꬤɱHZ ** ut!; 4.X̟?㦨_xӕ2:]dwvaEļ3Rp޼y#ud]vC:cR IPAA 7 +9@h.53iHHr⣄#6YHH  Kܽ||-5ff:< h9ć EσA  !| ANE]]= @K! gul)$В DG0&&!0Т W  fsKpٔH;ٳ[1IB khM&/ ԋ-ꅍHZ:ĖpH SB&]_!Ⱦ$@$@$@-,T^xlGb01 $H PQBG!h< 4>J2L֌w"  hSp( Ѣ$l |H#.j >5 45^V1sdexr *N  z˪%uZB$@$@$R X*v0&V8$@KI͛ᅲ=zo[Сq/ٳgeٲeo>9r;#SiiYF#W^y/[ٺudffJJJܹSn K "6ͥOHH*ڢ.<\mQ]Xք >(r AY.ւs P"`EeܹҵkW;~X**BA\YfieӦMz ?+VkۥP˓={Hjj$&&ʪUgU  KTV ]X~NY HHHE.h*YaLZێIJ2:X(o9s挴kΨ(JJJd̙{8[%66QQQry9pC]AAB$@$@$@`Q`(k0@E2W(j %kP}:_~e'N`g͚%3KE( İC{̠Au] ĬCHa$@|P qPVZ_~rJT,$@$-$ª?    @1WwvNlA$P7a I|]" 4\C1^ݏE mWs[$c0ǣBnqzE :X-]TǵF<`^o\Wgl5 |sK),:.Cz ds쒓 t{ v CHS(-Sq-4@Qqԉ岸Lk䂣AN(1QqvzFot[ry_I5j_du?d  /אνUP JAyxwos 0@1XR]@/ @K#of}mi%E òc{b ) 9(٤;0 뎅HHHH?u`ݩV:1X؂HDr:ݽwU}FoA$ВT u}nƨ/9#  h*+@*vte2 hr @[W \VM&bRrU8 @(W:a=(Ƥ2(I  | BE p6zK$@$@$ `4x"0u-}g&$@E]C$@!K$%;٪@`_   ha |_1JsA e > ԍ\VSRt]غgk   LJYEX aB$@VdVX$@$`Q3]@W f@ѣ;8-vq1O?Tz-iӦu]r{a ԕ']_~lO$P;؊}lA$@AEXw,$@$\:uJrrrҭcǎ/m& {L cr$@ALaAbQQ~,$@$`%ZYIc #c\გ@(سg\q2j(/:u D ܴiDFF]tʒbѣ\{ҺukyB$6$X\_54c|*hjkI%JX!0I뎅H |޽[rssOyyQ;{RPP 3g֭[eĈo>2ejʕ+eΜ9ҿ{GGd8p|WaDDVΰ| I).<<\ڷoE(ܹ~O evuN[ 6<AmF(a nHԗzy$'W\dzH@0k xI @yvei9cFGGka 3+PZ*]:y7;%Kw5 6$ dØOGMA{ 4{UǨQMٿ$В|:@|:baرC+kq5\*11Qm߾]m z#G^p9pKlvtL;݉HB@klPGI v$@AǬQ]x?)HOeѢEǽeYZϜ933F';5dsEL9sA\;$X`I kII>bp:|͛'111\c]ڵkkXG$ X` -;HIt @m $FZi^'6Ȇ...νso^u2;H>P1(YE ,U![o%y*[nV7ĸzYYlv9rRZZ*k֬\yK֭[%33SRRRdΝի 2ħ`2OHH*U% &Jd! p%?yY]G @(G@Xr$ eQ8X{'bnҤIr ={vU0nW̚5K+զO.6mիWc;|ᇲb QPw.,,P "qjj 0UgA~|% + TVV(,TY.H 4r$^YHHL.0<^ LjQ,$@$`%, P-_\!avӧ]TC3gsǰuy6PrAk46lsi%޽{u=Pdž: pH2! @5$آNY0FR$@$@$@$?J d0VsKXb>,$@$`%,0 ;P!p֭]6m8]]۶mٽư֭tMqFZl"Ç=z8w/`@0]v 2! {/_^222믿zH+nɒ%2p@g}HH&:>Ju]X w5q5   0pĨky:#06oYHHJYԙ'7?r-dCϙ`%7c 1`7gyWd1 ,qYڿ;֒ ԇz{^pʣkpgACrEs=ҳgO5j)o $IRR;7Ŷcǎ%hou)Sww jK:nk;pZqשS'$Ez+޲yk\By ̓kTZV$Qq.u]JUF0m[~lSS4yޗH7GLP9\_k(ΞpP{՛"wނF9YQo~7noj+=\31vq?(  [X{haiOoc I@'P!b    ."H,Ŀ5c+ LQe#<E +J?.P9rD/^,hvءg k3/׮$@1+q ;ĹC{O[̥T[ǡ>;;[}Fk_aV ?88@IDAT%` FrƨubJAyxwos 0Њ㣄M,$@$@$@$@iXݫV:0e0: @ jR5~s"7Ɵ H$*j`܅ wHHH1! T01Xc,=A-@Y5ծ]ʣ>T}IBâ΂jWHHHHv:>RKA:m̈ > ԃۅr}ezg   HuNWaLߊ|fhPT5(^N$RQ`ƨ HHHZAOuTYH@} @ Wa2@/ @K"XV.]_$ʱH@ odH$@-@\/T]XZԵwHHHFi}Y_)uٞHTIHjQqOc֑ B]:6/I, @E9 @"IhMg Gb8J$@$@$@> @ y/;~$2PQ2֙OI$`!+hE:YHHHHj&\sƼ&7L91^$PQVH$`)3KN(AHHH TVULAdQά!Ñ@$@͗u :1wb3'  h4*\H :F۴iDFFѣK.z~AXz!^{n}x $.H$@:+ir, AJE(Z6io>$ CI%]aatEzݒ=@7sLٺu1B'SLR[r̙3Gu>yINNW_}x܀$@!GJYEX`zFXAc E; @-jnnJJAZ2 @W^>}E]1$9gw^n…:СCrIyW"JS .R-[Ȱag0ݻwo:t1kQQG]sS&5j)>_,UU%;a+JX(WYc@@ 6NP\(l],WAp[~  VX!^~eg_uUƍeҤIf,"D^^V!vmȃs:FO?p܃魄bP|&okלF zQ%6i@7@KLE[,L(H :Y ?~\nywĉ^g YfiFU$aG@HhВ6[cqKZ.BZ@eiHe\"TBk uSWnD!AMG$C;Lf L(;}{yP#۶mkW^zjrcݻ,Pt5;;C SO=GF0H$@"REg1^a>$@~3FIakUU筨,圞{V/n6# hNt*8}_rͻªTkhh06IFUV\):PVVVzP$l7[.'',вUI)N>my#tU 83z:h 3 9*]~.P;$6ɭފÇ{F/TSp/S0u'2U2 U2QGׯ_o([ S @0ѴQep3`O$ktWі,Y⩚ڌ1c٥ɓ? ZTg>c]^h6xG<$tgW թ¯Ԝ~&-[ft$@bhF5,\Б @4 DUQNtw0tIGNm٩`.尺0XkDVu1-#11̾tOGb>cWEGz5\ /V펽@hًLJ}t^wX1-2YTIq8Q׻ w#Do}9?`x\Sk0?Ⱥnz[oAwәtjnANuWFqwٜ¿z $곞>ߑ(2NLH47Fi4sdDfl䥿Ný:sO`;|GG$з6#!ujNt$@$M|8bԨQ6B .qw賜?ui^W2[d:-S;1;LV< @PjS.m@$Ї vUWE8t 7VY-,PUku`; hitxj-jX? 03;4B<)KЩ{ꩧRX=g|+LG$@! fԩ̨k$ @o',TXB$ tNz;]yf7ݝ駟63ԾH %*t0UiիW[;?׉*tކ ];UY/!//JS hG@u 2 Md"$ @o'2X|\ ͨS9.Ɣ`H/E] pFu.@Kng Z@(y NQQF)"  *׮YiM(E$ &)!9ع.+ڋ:$@=Q' 8 x\HHHz9枴Nt$@$MlF]4+I^]vzH0. $e.{Bg f$Dm4:H5: @hM iˑg$@$ ^lkvf3 ! :!  c\K_cekuK$bWۛHH :\#Yv ?F_ @&`W3*HH &ME$'DF =:    TQC n&J$9*"g$@}@4Gsu^g'  KO謰D$gү\Ά lf4WFPtFꢑ    ^M@e0F\L.}`-HΗf @h.)vVC@$@$@$@0IDKO׆tNfC$ I$@$wuњQ'?ê#   MUV!F\h$z!M= y `]DiÌRQwzE 4p sC քz *zKO$@@tWΨ;gǂHHHz,]c3 \ ؛'$@E] v D$Ԧ\\fq3l֎HHH f4$b>Z␊Vz8*zx$@@ dNCI$@$@$4`il٨AWr=Nb}I PQڑ 貋|<.}NfHHHbn&-,ZS!7MC$I$@$ЇuQ;RZ:    ʌ:ݼ!7`]Hw_ TQ-3Α9 ~:s-ZvEK͡PMC$}@$@P!1!jK_ &" @#`uQG\X2' %@E @4gr  HHHO$\N5v5!B 1vv؁W^yGƌ3`}q^xhjjB1f\wu4-v8/Hz,UEː1ۀ'A#}Y 83gĥ^"6HH <-qaV47xu-/2;IpkGN$ Qi&ٳ?xb477kߋ/Ӊk]z5vA$@ ,ѳQϝ¢)̃H fkx1yd|31[]]]ԏ!tF]BI44b/6xX96~'Q%*y1 P1;:~`Ȑ!x1k,:tW^ye޻曍N.BWw}([o1??̶;uvڅ?Æ C^^.vyӃH uCs6 D=Νcǚ VVV &4GWEK/a&\1u8~8nV0oC3V0up_S#وhE2)hHN%;ϠXyMv6D;{ p;b@_bgkiQXx9?[nY8mIi{RN>e?A:lHz 9GAHlvհ y dtUT<_?x E?q{b;/hA{8$COB@{8Huj*8Q4H1N~U h9#Kn[efJξqIoo1|c16J%ϙԮ??ug?yN-d};?=5QSS#bO~yg"Q~ZM!7 U&T w YEѣGqYl6KG*T;vX;|Bζ;}4׿ⳟ]oqM7 _x TUUemT T](E]ԔQ%g .ٌَO{qTY^`"\#/胆 k=} z ?-Ch&?wpO9:.>Ouw u#{ῢk Lr ԯN7 ,G81so{Cy88ۂړh8-q4 KCk|\;ϤEh*B^8$S8} sۉz3cq^aV߈k /|bq MH/O8 f}އC+p6Iw4,Cf赢9-]Xל@p {'^=EXg©{sZ| d衊x>rHx۷Ԙ6y`ժUQRRb[o`С[ F,NdVO</jс5}D0菓b }jAkR iNQ(ĉʪ%wKgKYq7ravX#_.}kğP9}X}EH^Zig Z].21^+6Q ;-I"o'$Աj$TAcfVQL$~,r{v6Ch'q,l2O^V[O}" Ql ]m10w}KC}`tq52hX@Y+ÿYnH ṫ(ÛŹTV(Gufq9eeP t߳acGe"&3XrӜ2OMК^'3",uK71" U hL8[sҷ#RqI s(њ!b(.-| &^t'M8a< 5' gk1?}oO*pF?E;dM$քvaQYK]0k-+Ndd.Նzy8/5L />%s+c` s8_O$G(1[OӞ,ڷSئHJ3b]G#"2 a #8% ۅI;m΀ OFnvOom?K{)V647C[oG hb/p詧|XF@(Vd4R>$& N3Ny֯6sh: S:&xvSlHNhVljFri43lvhW's QRbߨXbVQ 7RSS͏Kq~zO&LU}ٳgÇcٲe4i}s.mq/V9uTAL_"~VZ+3tt85@a&+L|S/:ma_=XXg"~Yk{Sc-hCC^txu:o?׵8L4J8vQk00G]_݅jF h+ܔgI`Ea~Y|j%XTY5 -7v+n{d=}ly Qe05ooo0z6teԩSq{R0v;+Y~ $  N_4c@=If\JrGeD&gEdޛ-M2Q7?/In 2[b4@m pԔLzhOI_Os=%OO]r N3)vQxB> 6'Z Y&X(dH .vZW_9kWuI$P_4HK Xo@a.RjiN,vi^\m) $̺7]ŚןU%1 "%EFLYɓƎ 7`rևz(V\ViW\[_:&NhfYq0fLo/ DJPHzt5.dv;RkljΤPNe8o d/^GzFV_ OqHH ln]~YDUDXi.QY] 7xWW!oYzWo4yNfHHHH\ lb>̄S;nw*:tkDS9g5:ȪtsxE͛$IHHz8Q~УTeψAd1~~&B7Qcċ-2Z_wu>%O߿Fu1-7)[]2P.ՙx˗/Y&ɐ'$@$@$@$rzՋ1fΕSLe-K׾5yg8U9J,YL}4itÑ#G`P? SJC    F Lӊ: c:β-gsY7}t#쩽;Ku=VAHHHb@(.XϵΪ YaGxYU}ӟ6&KM$@$@$@=@(.f7U% sN?N??IHHH"#[q@(CqHHHz" Aܮ$@$@$@$@玀.u ;w`I$@$@$@$p 􈥯 K$   #jDڞC   Bt~F]oj+ZUUtڻ6;ٳA0~xkٔrǍmv՝wu.'> wx5c,%$gxpWq|j=,2d/Y<?5ػwS*]d*++ӥ^jvr袋" c@Xf }]L6 YYYX|#yw߿gφ655yZwƿE^EN;^0ׯE^>;(\9wAX~*6M-[stgi+>|8.bmDgu(fc!'xc/33s1ܪb;UĔ)S[oN |S &Pycvҙs;vTBg}[ɓ'rB73Y.2G\ӟd>OEV1&'{뮻/2y3&#%%۶m3K.(wtw_Wfd}g?Oǎ tg~Vh5\qC;(O"НdU'\?yGB7;8k8>WIb1jBBZKLLĀ<~:^_z%\{o}Տn?я0p@OD_Pwu:G;;W㾕#]yYGsY[#kmwA )ղo>ҏX]o& ZojsKt 4\wGCC?U:qFZNdՁzZ4t#Н}5tU>R>SM~(:oNU'>Rt9]8cF="*?,uՇAs=g.鹎ª9uLVgyǏAӭݭ酙bg,K>ΝA vk.T[Qt]#}KU?|0ޖ3%ej$p~ __СCMe;t*ª*N;Hk/g>Ɂ}s?O9(%]#Cl67|`Vl𜜜Ut~-sa9{SUk/GL Jt@wQ]z~IvRA?Ryyy/rddd|h?_G}ԴD?T56}H~fv˰B@ot,PC;([H7}ɫVNvmwQ09Uɺ4ϵk"77lڦT>s tn>jm-[xj~ NK_!qu*3T%.yEB}ZسK 0ϐ>j0UwsSj.|?8Udf~f@w-9RZ ?nXގo;Hj+V SLu@wQ 9sd'=T7R#\QAe; tGW_Ms3QQ']2|jD(FY!j|z5*/tVGMXOjoOkwQa4c_P.S:D%%dOkUOt>ҼՖΤgH?j1c~ٳj[g̦zw>9rH6լ;H7Ynu"#]}y"!c+'k%mذLRYz5GHHKt~^BgשMӡz6tW(QarVa@UTT;3%^r +?#Н},oauL h[*D#3K-KoZ<!wĉVU?'<-NjuσOH@@}m[UQU"xYa<[s;Ң?e{͌BǏF.]%> z;LdsG ػCz׏sAKbE3]`ﰘi`/HW`ft,P2uїHHHb@(.Xe F$@$@$@$p~h K%        T Tԝ,HHHHHHHH|PQ烃$@$@$@$@$@$@$@$@$p~PQw~T        !@E^ !@ERIHHHHHHHHu>8xA$@$@$@$@$@$@$@$@u;K%        T Tԝ,HHHHHHHH|aEU+h/u_#YwէzDU;  ɦ7"@$@'W=FNrٳ㻰EǴ"E$@$yW,ÁƎ輂;Áɓ܅+~Sv]I+QR5 [5`i/yA$@}@f1_e2&oD9QYCd0Cz@ _WLK$ ]E]|?[G@s-y;G~ڷ h;lutÞ$%_fRk]oQ h&uBN]1։0 iac+-ۖ`m#:69)Ȟv4%`&Ymr'2x=} Hb@blV{jUv n*F#wfΫ7KQ !-+s=ɵ(Zu pE[>lʥ/3FaIUfnkuNd/blҺҁQ{/j0߆rۇsYCP$i27P.JG%z529Y 24PYs JH=g"U&Y0_e(iC򕢺Ne3a߈y8C$@$K܌by 9ꐚNFs(+- y$ j,²=հ_T;7`s%(F}ZϚKq*D+Rd4䌴Kx,i6L+} l^{#gD8E>ꩈ{+:<3gEp'J׭Eg-?X4%ǓܞH=ޤWD)޸%o;ίۇ՛Bdlk,}C,WǏ(yZSu9Mx}(s?&^ޛŨ2N9v)--qP$Ȼ{: F7aד{>: ejLGiH$UIUc(Mkn\Y|9|Xi&V܇a k+z{-&ݿKgM=[4Y9+Q_gTnp`H.ty/6a,ϱ|ܔâs]WeKw4kḮ˗.¨EX8kQҧ_'N].'яHH@@se Q>-mt9 P_JfWE3!֭DC$aِW( 7n\;  Xl]ar*A@fNk:kށC;+XiϚrJc鏗b{( -u3E.wյX`ێ1KGɚ(bȁܡ$s*#ie#3s :dW,Bv?$uK_9]7('˹e>wARlש?c*\);k1Ar>h"p@kijT]5I;T a/țTn+D; i=tnt#i\L.O9r׎w<"lZ)Ӂ˱Q&o t0Z;<&]H91n_cߡW†sհOkO/bnL,eE`"O\y!ꕼ r8dcTv@=`q-aɓiũDP ŧ\q#N8eSVa3L‰XvJp9Pٳ53΀,lf?ҖTd!f5Ӝ{˰plZpIdQxٌ~q8|+3mMVWbL򭙒kOC\\%#0Svk0H۾/6e͋蒘+k0i[aFo#_ _FOKr]nF=bOWM3GCj--¾ ҲLd}>'p7ϻ s7EĬȺR 39LWf7 q$uMzD9yKD0"LܳڗU ^%4N@RWaxMf=2n$3WF &MgHHGo_ZZÓ w!1ANXWLy?䤋E2[6@CT̒wOW@g3W%X4VI2sgv?(D;]bÉS!o!;|RaSM\RlrU dZ&HE+dFRglb{ׁicgb{zB7or4Qxn84~XaHSDq}0'!SNȒYP`5;z|Vn{ 3 jaPN==Oq_rkW9X'ފuoNT`@ћ5?\]9 B<<'O?-Iߋ~oHɌ:9z#\ajs,\.LL-=r?tcX-z?vxk Ѥt$@$p E'D)X|(<.U?N^B;ԗM7Ő &мJdi;'D\wn!ͧ [I4Ek ĥxݯHiٙ=Ho KxB`fL.,y:/w!U uǝgViaNm:NQ$>QҩP$Ϫ\ʌ*Y8yvh끵`V| –WDMY)ّ!+4E(J=7r!m¨\sZdnw7ytf?,1ip~{Ʌ8Yѕл2J\yo->8C2es(P3\Rۖ䴸%: 5ɽkJŦb/Kž5a1}ss]ْe93PD_KCmx@]|PFOoT锄_/3FceX!6: n$-eE6fp%Gv/%l7EF7Yc&^bqqzp7}rU57˓^T6ezK833"Ē^k>Pw"&JW2xF$@$@b ,PD_l:7Jw;[EI9kݜ)^Tfy{KFI7O"O1e<5#GJ >3Sҽ/T0NXTM;+,WS&G[0ɚQ1->P6J$IN;+Wf:9a%~Rl &6ef{~ E2p8WwwEy.ב E|Do;V< $#uphӱ%3EZVy%zy(e ۷U;m=M߶B,Hq5~G.o *O2Iüc,X&-ق#roY#2u NʒtT>GdŎEZ1+.6Y~ \ᲄ\G@$W)843BX=e9rMBlU!6ԾwuF!KfF kY>j?_.%We qE^\o5UEز>"9ڰ$df'#Tf͑T_ZI>rC/0#a#3>^xEqYܫ@FZ}QZq3~ \%j'uRjd!R1}-mg6_C>Cjon.1d<Fʌʧ73EP VVzB~{Ǒ7҆=j'KrOH{e&m} " c.+fr8^?APFM\y9cKPXƎS.4_p:4̆ f}˵_Y _qVWJ4bd"&r xvS#/bM%xtv{NTWSH=,3 wVɒ/^1V!+Nvم1<)qTyNgsGT`# i+y8̽]L4uMț6"S=,]u]ɦ r=;p _IDn~lr,/D;ؼ&7XD)6J$WC$:׌,W0uIW\ |-'Om[P\~+>΁ʷ~E&Ҥ]7ٰXpqpʆ\!}7L%dKrfwN-)N} ̓T}紉Fv(87EldNǁJ? 8z.>c.[)sk%6,c&BZFJI)ӻe(䕛364A2*6÷ SE(NhGrƭ%6ykbﱽ\Y"B,mX*Sد%SH6$py2XeeT'8UƃQ6u־,rov:EidH30S bҕu㓲{*L;X*d7OE sQx% D5 kWX-xT+TPHX(/>ɾw YedCrIcDll5TTze*Wb˿Qx0oԕ{2n: ^j|Lzl;jN[1܇Aʪ6N{1='p{v<>eIt.'~5% \+ʙ91!KFA3hNi GԖie<]72^ZS_Ä%X;{DXU\u8>m?-W fT ]2p#)IT; -AC+E޳% O-b8O7Ef`ٺ2X)"dh8d1~K ZZ+Vbvw # QIMᕢdeNTrO|0$a>ݼ1y+$^d(Oy1&0cr,GXXrEWVo)tB5(~d(E-9?<CwnmYy+T:֞}!I(P2]Q~ B~Ļ W% ;^T @چYڃU{vaۏ,T3sMTiI|c˴Adg ɲ <Ok >IYI$$ėuuWzF.X>J4էާjQ%ʷDlvްWywOBG>m U80L*GB,1wFgP|dO"$@@KKgM Ⱦ F=ٱL" %;Mv1$@$ 8ze"Iʸ$8keuiRG{LF '>OHH@CC|Q]z%AvA$@$@$@KCgΘ ~ lo]=ƚ /:x=n< A͙pF/;^ @L2d0ǡ%SceHHHH:$0`4Mxe&Q@dTB_   8otkl5dp!bDБ qI??RS"55SQ$F!   sE@rjN wS&/%% @l8v()H%|XJ# @˒m^wKAN9=eFG$@$@$@$,:KKNDx Vu>f$@$@$@$;>]/!Zbnp.S,ʻ6T    9tΠ0?^m6pBBwVd`.QtJ:΢ $@$@$@$}jqkz+=fl555W \|qF g$@$@$@$@]&܂#~$ʹV, gg8s'N`;'GM/F\-hyRNu: R`qPf    k[-ŋ|dHHHHB@jo*ǩW}-*̨e*RH$@$@$@K_ U]OlѥȲ׆FնmĞIsK0w    H `%(+"&l^$|k\]]]fOtgneدSQUx6IHHH"'jm?zGIHHH3`2] :%?j\;IHHHb@0!0vjȚ X:kɫzv;IHHHHHHHH:G@lڹ6pzU?:         !`V鹥") (UOsjE)-u@*|HHHHHHHHF )))lt:lFbblթRyG6̈zǑJTT~ԢF='005Ӓ?)>e\n=qVb=O_jƗg#oD'GM=T8\0WLw.3xȿvhGѣ^?QlS/:E԰WDD9-H<hnnEͳ ".c$4D}@;9#r)DS'__P;W 59DA"k,@žxO4⋇?G(^:L-7^F~i;seK B L|K408fϩbRi8 pŵX=!yXv&2UxkjڧI}t}K +ה"J9sD\YT^Ga]9f݊( Z<]r"WىgBp}Fl"x|('$@$@$Г 8Pj%Jڷ!mLgdSD._G3<'ىa]\%ut C L32,[>Vd`#?L/y/8:;DPӜ̄(Av+d(];Ο1HO3t}Y3嬢,u# @d)X%BǢY ceذ-bg.()|W+IXb9<4n U 6kNt'= ep)Iǎۖ.¢KWHJ<$ iA 2.Ws7ծ E;˰s]D;Aep  uC6Ld`ݹF{}3JqwZ"@ǜ)U{6cƊҰH<^/CE]dGGGIgY%ka2Ȩrs5> .yHƱeaG\I!{']2 9 Eф_w"H df JQ\j~|ꏺ߁*^6nE1Whͅ?ޣv P}5wH7OYm<Y#$$ ׌C2Om{4ϕl@A'c$?{2\ڦ_u+QlPsKaƭ\Ar2 L{Y`sdlU\s2/AՐLpMے_;*տ։zQv.üQ JW6cUȜ[o@[62sMGY6,Xv> 'o[X~dyOma6<GMJ>_媠kk>s62Os-̚+Oa y.[]-(\#]/oM(]tkZ޽_2n[%_u徭ok;w\GfVNWkh*/_Km{~Q7Le~q*l/nCE8LdWb/!(L{ bڬq(ߴ:CpW%HH d_'2㲮r,tLE&ڶ[T",S8k> II`d5kQ`rODPdLc܍9X8{5oKG?1i%XbɴD䅽rx &췠򠧲WwIn5]ktVcWYg:]Tnb,ͫ6!K\G˱PN@9(|&HQ;XaX=`APr[]Ґ?k,e}7R {gM ?*wM]7 Zdh\KWdWWnd (HCȂLMŊV_gV*ʆ\0YaH 'lHJJBl&J9o}9ήsh#Pa f]˕& ';7;Ř¥lz4{/w\tȾ>G^g+MH4R\<{Q8eG̺ *FPei@zwulTsvVN3"AQY(BAR\yx-u0݇7KDIu :vO uvu(Z! 6Q٬e.mdioAY&.dIgOD\g$˟XbFVw$R\oQB)vf!#ۙ@Kl]D=c SnL.rR(Ô'3DR{#3,ߨ~Wi7Y.[df(5Edl)GTO./ZRa} VCFȷ߅o`r% Ifl?m;浯CnG HH:E&Dnp\ݸq$3@:=LFn#P+V@y׋ѡ̕EG*,9;0IW*9s## v%De+VWel,6vZ}2;.f\ްŦmu"WR=^Zm#r8SV^+EҲp)YiȔΞ zvDŽ[H 8< :we&UVu YݑZ/j[n-bgҊu0RoVNEhQYHkjk%uL"5np.E1% ^ KCdfQuC VOrTsu@f V`Ǵ[ < J{f%}qbM~'jz^.#ur9*TIS2e R+d1$.J{d?$@$@$e6OPع+Q|2蛹e?m&.g! XMzDkn9@L~D hǏUN:|f?ah4K \N`SI@ q mm5/%TY[vwd8&;;:ň\{C cxdɓkɍTm';îueOӤ}*Ue#:lG;R[6UeY   An w]x @bsss,؟t鼯;*$|!ܣ7fb|rٷ/ \{{oE$kYchvYKe-g!$0Vz4rfÏezm;~B7sfZ>|nHKSc$@m!)1..yz$J */cLo2K< 9 ދy"ey"+e6h%$Ul0M\NyfǙ f*[ut񱰝ÜڦTrdcT 0qGЃ@f;0Ikq_dPQW9r`s6PzӻܴVHɵIЬUe"-ͳUV$G8i:&-kWN4M!1.9fpy8$F[3>Yx),}bp!8+u{+21$@$@$=_q-!75TO0X;ׄ^ HY/mY3/=U(4KO`U.3Ûs=5-M0d'kfC)& 3eXlʥ8,0`O6:MRa Xo&i17+.f)/1{#`5u1 O2rt^YYwX~ޜg&,Eg14oϘ Qλ̸K GT W9+ճsT.!Hxu2 nM %5k1WzŬ]$%_,HwIabӡWRy|ZfZf/rFC];ͩ~b?GszAe3֟/OǎMfM#9%e]uYaxpn^2e]>&oa,FK};dKKeج?eC7 ntWԿK3[f|3IN.6z!f ,zg\cvTQɰ^TtMv,uYou`h5NI%%kjLeL9,?K c?(B9L;oZFDdܩCLIfOI}͢ɵ*dy6q* 9a[>þX8?m^IYoSG)2c̮8Hvr2|xo…1otf!MY ZxR]*Zq@.yx,j6,TǴ͚uC-bOu6VM0kCx}Η1f!\qsL]:lg6U֟w ^#׼.ێU7LY#$Ȃ@a}IWu}Q%&Ϣ,F!=N2;`nuSOLs^~>ˎL|3y0;61&Ģ1?fgv4y<Դ@mKM1YG=HWϬEvYe)t.EecUˢ2寪GbOqaӄ[/޿\;d^+W-5n3ۇF;*v}_HXQ*kwoQȩkS=őfC{kYwILl<2_,eaH9TR_ָ*3m\'>1do,[s`zkMrPU1)&ן+רȿݱqavxR}3p\8c, ZBx3n/L!;/Xj2VJec/1Z4(c5JnTyL.3E͓f,?e'GMȴH7;͖K7OS!Q,gYkL=e-xrzͲ ʬGl͞^͔?$<= 3IV'ih: $t5ҵ{]m/boN] "ȉ.nB GUT#|0U"8j>9e'7$8 Ͳ$hN!5yt~TȿdMULJ2zhFR4o5ƊXH4QGѭ3~ _ߴb#ؤsl+|2O7(ؖR7:=ttw*j+vξjyy"]2pLյYYX~{,L..J,sMX>n6MlBjZǴִy!@c@HHr yMYfP&ii+1|0USEREۜ}cs-fٿ-Ǝd]NB-B6ϙ?nD:3 5u%ճ f3o5YcBH 7q.997H&$@ɓE'fG9G'ٲ"qQ yWg}Pln!2+Y,vx^5MƄڔzJ$@$@$ pжP2cr-ٶ LVqL@!w}^.XUU$ٳ^xf @zR񅱨kg6|ZSeӦ )5H!]|vwqجtӽ縎HIlYphelraҿ}3'&$&t}MGIڛ m3-,#j-*V?@Td㏍OWQg;HHHB Ӥ.])ԥ#$@$@$@$>2 ]\-ڧ,HHHHHHHH:Btǽt|%        'b$@$@$@$@$@$@$@$@$@P+9#        (9ɬHHHHHHHHH WrM.6xdԨQrᇧg?lذA9y|ᇲfR9ꨣdR\\". ʕ+矗=zUW]%Lu֭['K,]vɸqdĉZ-Zs8ĭ /P~~֭C|/^{M?:YbO;0}[VVO7$@$@$@$ 8@\ >%[lUUUr뭷JQQ׿`bxꩧʤI45\#/p q;/ÇgG)3gΔ{Jntطo_;u 0@~;#1ch}|J$@$@$@^_ mg 棏>RoY=ӟ<SJEEtE-`ڵ'{OZ;<( K}, xokԏ?Xn/OŋU慷~`@$@$@$@a"Ԣ.urKLM0arZty`}-2ezm ^{/.Ag/X.z6 vO?&q?-ܗ,!C$Ae?L!a/`Ba/9`:yMpzv[wޙKW   H %Zh[D:]$%:cR$<!AX x<0&ewuU <   _m۶tu"ڪUtRvիn0 '0!æ?T`Yn\]p_d!aOe;<].QHHH:0 6bs繤b)|7*g?sNM0+`%?^  !ĺիW 'NO?T燈 ҭ`%`Ly)([&Y'O! 6L-o&݈vw]tǓ\X-[Lф.|%   N+PaNQ T!5*-&-;"B`%_~e֦]r,֤FeTbƌ:_gZX ųpΜ9@$@$@$@!P`,!O0URZR^OfDeL 5YZr3dt b;>[ZZrd.z&~?M mG:Q5d?GQuMt-;0&}:&nbx7@`KGaʛ%X>՜_T SP@IDATp{|/q~y,rGzT{ 6`׎'r]9],꒫*֭   hQGO$@$@$@ ^|EUL>L`57iҤX"}ڵKnfYfMܖ-[JnV)**_׿zjRg-fo~..eF1OdOQoGYxtݷnj:{K +]u=] 7?y^kYY?|#.U)+k͵'] 7gkU{ql5uzk?x@w3PkO,HHHF7谠El [rΝ;֭[k5NLSu.XB9/kw}3_,]tQJiveר'N)+û/&z8/m)%XȯЀ‚QѪE}:mJr!4tEv7K X95coNYַ6(?wR-V˯S&)\K~rNJ =K>r+I&6LL"Oҧk3}\!   8tf-aȐ!RQQO]9XAK< BOOqCnkj`#q]wݥX2!U\5[N65}P&w*nZUdSP8Kn M{iW[{>ɂ<ڏXЍ\[t}- @^SO 2璺jժ9M! 3K׫W/>|XBmۦbv}{k,?^ ܎9]bի2c aO?UWŦ\!"$׀#J_T0;s&ukOSAE]?htzbw| }:!X[,ę0i_iilQ{+E-Z$ޓ[<- '?PwC<=ꨣdGXG'   0(6}+'OV "rMuMr'n|@Kaq{ eZ-ln0 XۑZk}H̑Hmldwt뎼 r%0Q k|Gt oKy]V>` O>'x rx*o޼yrI'{ァy CloR!hE͚ըM_laUTd©mV3[cܟ}%unKyfkhB_ia,`͆E|BHT5຀.^5PF ˷Ā8yOn<7ۻ[NZk1x '߹%oY!<``P;Q>j/4izEܳ:+br =$fM⥹'jeQ֊HNƹsE؊>wl"O}}F[!r4m!Lq @pC4h?/^{m_~˘bݻ<GL>]k=clPXAƍLv7py^yc0b>JE%XGta?#kBa%`'\DGcp /K!ʳ ~3Eqω=c?ɇvd ֳ4/E=$gټ g>ί T?+v|g/D<1b>&^"^з5ר;3{$@$@$@$@$@#TtVFkrv׶BaTo=ޮ¼7Tk޻)cz>lհ y0P,{GsV\)?]K//^Xצ\Un l A2|M+//i=/& a!l8qyP(ibG$@$@$@$@$xu1q%lSJƽMefP|jEhcQE] f@O0BZUz/o^z-˰nD 6Ț5kdԩ vwرcA/u# ֦˽W nI7x].]'LVw˖-k\ z8HHHHH <bk%ApIl෈cbsLf"BCƆ~5@s:gB]^,X",n\Sq[fȐ!8qVt#U8q/]{]+^>e֫_|xHHHHH XܣhԌIbQn> 'u+,6X0{؊,TµVQ[3%KBO>5ձ{teeeǜ^}6k.ϩRLD$@$@$@$@$ AF"L`%"v#8lQK8*TBj8k=C訸w5 _}kP!bQמ ̙3۳ ,HHHHH  YEo:XYL]|Rq 4.7hF]0n&k,Ƶ5-TǾu/Wᄼ@k3MgIHHHHJ@&4iهmcWl u~f9$@$@$@$@$@@q݃{O˨9ksk=ASΧtv޸Z%VIky~,S[2IUl'w"9F=L2 @` 5EtÃ) q\ck9mH.ٽTB&`3 mh'!jCPa>.ԥB/k P7qG$@$@$@$@$`M'FV| G_:.ШrCC?[c,2N14rmQ@;SF[oN$@$@$@$@$Ё @d+G#J]P\_ 46Q*YP`FXv ẵ-dԄnblVuׅ#p}t>a+IHHHHZ!kY}EOhVᴮ_C9(Bl[YEBSZM:y,Q[h;}qH-")eCqHHHHHEA673H16L\u.upms#\_-v}-0 }U]-4$E+&|l"aꗋzn-츩l:.9HHHHH 9Fh0i&4zc=~kh֮+,ʹqY@8B+|'gJЇ6NlN/Qm` kyHHHHHL@]_m\L#dӖIv 7>uaEXgjlwk\`gO8Z"f5V54@05!H.+3b$@$@$@$@$@$,`k _bRulk:XYܷ-ԗ+j4"3$z`؈Ƕ캭çxTO1l`"    <h673F;W:|_!jع&Wt;L|L Fԥcg)vA|F;0 $@$@$@$@y%rJyGrUWS*ުU4x?e)**EImm /P5[e.F쫯P[ԩ !y딝E]{u"   v"/_/F|g|r4iRFX'S<p^xW'?S6m$]t'jyfyWT;sc,nQ>Hk~{%Kȷ~BqT,h>-cQ bqa FF\;FXԅF3f[[ (3[WwXsl! gIHHHZصkt=~n۷oXeo߾} /%\BZNb?P7˸ڵke֬Y2h 6loُ?Xn/Oŋ˂ to]3f|w*إʟǢAnw6n_EFMN[k#O: R3 I}Cx,t\ lƳ߂ê:|b|h(sѢ.Z @wGNZ<']ΝX^{5ꫯ[oUެ{NM&gy+ydر* :TOF;{l]J,r3M"bTҔ6ֈ+55U 7qVjxX*5`,N,3U G?.:wU매z4]): [UVbk5c܊65'73,UURZ:#lP-)#   @ÂγKH;6h-6^{St՞z8EΟ?_x$Э[7⫮V9vmr}3<#_|\tE-u{ר'N),'e;uI6[,ERXY'~ 45Tyxd_¶ht6tr`6Xj;I0!RW_ZO/]a*)-\Vdj_[^e|fYTl*k6_T]_[+ D obkk+-~ӟ&&mcM& yB!@æƃ@bC2dxHΫ&0ouu:=6aPdIHHH! bS9ւ IJtPqXw^ظqTTTu^Qȑ#AZA0D=P.D=nղuV-NCO?TSOMW@L67B>iIv#ZWOTAlwkC$ 4,ANJ<< KpY ܯ| * E7l W\qq*A|Úsذ#lxHY!AK _~_-]<\P>1uY2n8K;1C|եV!nؠuAXpI9eq Vt&Xe]MZ %48pEjXt!P}9b*ԅJ.#$   IEñZrH/9Ngt٦Ayʄ;,Bp{r艋9ZFz VX#d"[2 eEMb}u/(h"[BuB pYpS*O6}l:1?P֬Y#H]:(=z$@$@$@$@$@#L$nZZ>m&@0сP =o3T'R+.d5{]~ɾ}dƌRovI˖-ݻa&ޅc5k5JEG}T֭[\l"| 6L#O?s=^EyHHHHH@ ]Q̨) GD ,dZMɤю\´ZZty|b-jݤ6&~9HYUWWիVnZ>s R_bạEݖuai{ܹw޲sxMYޓk0\b7lؠix׾+AĀ+++Ղ{]'|R^9U;ě7otI.:\     А{7ٖqvז6[Ŝ8-רPLoMbw/6($ /̢lpgX` .pQ!\r%ʄ  Qy[u须[NZz--B{+(l`j> ~GEŜrIU GO&]jGD:\Ԋb֨+,GJИYzZtN=Im|`溇=ZDZ6*KD΄:` u3Nj{6nܨ˝s9n֭D]7裏dܹcI.]T;蠃4% :upEy^{v7py)h&8:c"j> ~/G!)> ~$t pĝfbv l[r엶@0 e}a&WP Y[Ʈ"mziJ{VcP;ׯBDB6'~xbVTTxՕOgW1b>&^<a y睲g/_IHHHHBB@E) q7BI eA@@b]IZaE!QAԮ!_j<sb.Az-l6^Riiq /`rJt-m۶ vpFXk=׿]=;XBk!͙gbnܫ2|M+///@^p0!aÉ;OH     8c{#9T o[Pj$~Ͷn&a ,´F\VmJ[|D-> #3Wc7x`WX75kԩSU\3gN1\Tk 2DL"r?cdQ nI7x].]-[&h \pW     pa "jZ٪/_(A>VQg+%,c(09,}M=cBݸUq6ǙPb Zuztp,^Xp6tL8Q.5y^gu7NˆˬWt8 @v}-IEF *؊Vx)ZaB(N]X\ZUxr1V6\0k؊{GMҲŘ7}6MGEK u^S l~xla-AӵlkOz â&h?dV1HժVDW/ $k-],"<ȾR1Nz(5xjۊZNSG3E]&:'pZ%F+"uuu<=X=i&z}G1q~Ϟ=doo߾2n80`@b|1k{7!V9%U"dEV)0 APV1Da'C-mb*n!uH& )G   pD`׮]j ewG۽ ,Sydڵ6l {O H˻Aɰa~[|rwk駟./ oucƌQC4 `ĸDb7ё\4ڎdAU mjcC aY}b-CG?ێ((-!9:hQ~aHHHH]@;#eCK555j*^N _r%ۄ eusɴi3y:w;tP>}ׯ̞=[L"UUU2x`9S3}R1¾z)0 m7mRZ[7ulC *`gֆ"SFo:cl_+L04OQCV+;$`ľZS|9%kІg3~ uW9^1ΰ63´ŗHqqq~PeA$@$@$@!!! tXc!QDPTTSŃPxQeƍr=jΝe֭D1"LZ_]E:(,/6p;R-KGf*f̭Fcv @v"IHHH . +*N]pU"Uʎ[KƞEmF]kcUԝc3!qۀRtV̑HHHHHPk{TuLsas% &2WXo<_nUp dO9yϹ4)1Fl ֝UdƉ ^X#     PK*ZP..+XPkFGXD+;:Rp3 їSڇ:1j߁9 `" uVHHHHH =&q!ԙ]f?D"Zq3kY u]_]&'ueXS|k*-lJP %!     F1 DIh; "" N.r- ! {*p:经O#%@\+rp#<g_5TTxHHHHH `FGuIA@?AUt1JB4 fy\ CǾiX.~5K$    `MrL:7A舘uYp 38pb[(|֪^b.Sq2ߝU48=JfhB]     Ȗ+K*&РK}[-JrwDch 8 EdI/gToV19 m)OO=SH$@$@$@$@$G1:`U" !vF3l]Hg+\!}X\EPȃ'[w -9΁Bk̏HHHHH  :EJTd &0wwFE2 tO^/k YEI2pmq_vMe (q @lJ؇,,JSaabV4vAͷE,lE15#bdgbE#&3b+1n[ǎ$@$@$@$@$@DT.ndp`ab W,t|.d߾}2c oV[C=$F/\^{5qTׯvarᇧl"| 6L#O?s=K#l~     ݤګ@9.nY6 l}wh6@s`QWB]8FnbٿH4*Bfk uu+̢pW~XųnMjeq\?IQQ̼뮓\N*ҥKkl{rgh#G3g޽{[n*Wx ԩL4I q?yw83f~Ǜ7$@$@$@$@$@y&%3hY5ې=oǍXaF3"ò^D]ckQiE0*XE-I0Y}SX".z-;wωy"_^>h ٳG>#{+ާ nڵk]'|R^9Ut@@IDAT;do޼yrI'{G+TyHHHHL@7pP( 1*8J|8.f[5"Թ, B *̙EٺwbW24O% 7ߔٳgΝ;k|~W?ʭ*|K w?82n)//N;Mky뭷ԝb ' ={s=K`ԜH}nePCS)}TRRq^a I \ A #*_lz鱙D٪e8%Pɸ68<~|%}XZF3pw3F>,,EP VMM@pC 7uXW_{DTl6#OJoܹc,tA :upݸq\{*p R^^u^B>෇Q5d?G#֐L bOͭO`֊NA#,A0`KFJuP\zoʣE]޺)]\Z8D6Xy;`'nfLb„ i+1ǨIgOGOɄ}\sp;ӺziJ$@$@$@$@$,n, V \XظjD| $6m/4 ntMM~dTm>olr7`TVDi 8JKK#^xA+W?km۶M}Q|t~&}c n u(sgK{UUUZg!k嗵(n "l8qyICCW     PkMj7jY'֗IHHH J^|EeԨQڬ>L/_.&M7E3ӟ_Ӎ ҥ>EyfyWtNw9ȱ7mڤ6ǫ7Ǟ={Ԓo}d*BJQY k*;"N-0lWQ W Ϯ.,10ތ] C;foSl0HvVtѷibYLHHHHFݻw'[*M x )za:71x3@Lkڵ2k,4h 6L~y5ˏ?Xn/Oŋo]3f ! &Q!"$X\>HkԩPg|ZkCcs yW0j+p8p6 -EbQ羚7י3gf1IHHH a0/@K+x[nl5{l͢skW_}aMcKsɴi3yرcrnС2}t=ׯ_?ʔ)SN?/TKu8zT޸5m1M^u̖SeUq)fmyŃ߬ ΑfC¢B6WTIA]mB}CI^ױdSĴU5Qhlf-1}'OSƚ*ٯ.K%N-v eSqLZ*L?9Ox,HHHF R:xO ojO=T٫*WX옘d*u!!/w}3_,]t$vY/~_֞xumhJko~J60EEQt?ON{j|ٌQa}Qt1lvl(XӞZ)6߹6ܐWiEn{~<6y-clRb'4]]sn:'gti6$@$@$@$@DZc-)DoY.r0aBrc9F`l<!Apn- x< ">u]oe"j 0 %,.kimt3v>܎1.ð+q],W`LgEn߶C@yŪ4q;h'$@$@$@$p/2.YDׂ\RWZbYx|tGgVΝ;U۸qn(8rFx j+,D=XA[zu†=zPaO?UW=SӮ֬G`5lCDܥWyb؍#ԉ$@2]cwWp%&BlA{ݍjC\9Ekbg'E"l t`pݰa\qJa*A[b l6]jV"s @ @;PRlpsEG}4[x'N gu7NN pim78vgWlP ,\P^b9\ ʮgQgezuYňk,ʟv:j`XFu"bq7", @YYYz`'*^rMR wXT CXr>Zp[ ;۠$Ghe 4!=|bPs9pt$ԩ.9 !Ny64CEՇZan Ps˓ @Ϟ=iǢu 8& p;ZBزZ[e9x1W;:Xԅa6':_)6ǁA '|e’!X(?X    >@Iua=[ ?7^saert{?6r3/!NDUkYF*-?X1`kC$@$@$@$@$ uk%N6Ԡ%]'bF3aFf31X5܎\4-+ F]EY @T { /XY[90 )A L! QC Bkmpzi[ޅ7AUQ{Ȯ15 ^gP"*7 ]u%'lUp#60VE.BdhNZv1W2Zaz7R Dn,\X"*7`#huS>j]FI\YbE"5]֢zwYa ~B]w+@$@$@$@$@$ pVt!AhȳԖvą$FnŁ:|;,ǁQWPzp} E,.D]˒N6phakkD$@$@$@$@$Y?uaqW'(Ke]Q:B*A`(,./{8~L'[ؿ<,I -uY<>\OY* @$#8dfћEzI5ߛI׈5 f .?׽O]_\@jAO*P;Zݗlp>̉HHHHH21JV'DR Z`B@- ;+Z_T|U\.|bV2/'H$@$@$@$@$@V #*AK[^zN-` i=ՍPG|k_>fC8ڐ fZO[Ύ\+ZugIHHHH \XRٸE^Q ~XeJ݊lV(M<]΁ ]_] =;a0/p$AUdB]5"    @@oTĬI&ehSAUԲHv}Ő Pg*jmhڊ滏]^#Q }G_T  @F5#5>NX@ttjm$,+Εu$0X'х%#-XghEUڇfWF2$@$@$@$@$@%JUQTQc⥝hrĠ&G!+ts[/]DX5mƐi]=@;V FjF      7\I!VE V[1]N5JzŅaj2}K];6@B] "    HG@[S*y*]):_SgVDڤYˬ"S+r Z\}]ms !X|#M @ 444HNR=jq4uubhg]g7U\X5bWܔ}t\]_yԛfƄ<_~aT9"՝l D`E\ݼ V.2ʐ\Xԙ-43)% Ը~keKᏩ.v|,n&ֆHHHHH \ ~I~u]m ZgWp F,rpsY3QЋ:f6p:ee h|͢HHHHH ĬoT*Zo\T PC;vqHIV9xqsѣހ"cG,oTh5鷶B-HHHHH @ gy; . 1A ʸ,*k1hu- EiW#pC  Y48ZqBfNA/^,+VCʴiӤYolݺUjjj[nӟTN8@054? (+W?/=zJH2$&8BhЛ6zFyw_CAb褰V zG :Ob3ߣZA"X!Puϗwo>1c׷ l2;άY(>- @pOȥ^*#FйݻSA$t}&Gcza%;ǠN~E+Dv up:BH!ű1wߠ\."mqC ytfuuZJΝ+}nMje-J;v u8qw |TdLXJ4hhA/֊lERE*/Q[؊-*R\p4T%@ e̜N2!H䓜{~ϝgᣏ>2=}]9̶ۿ?VZ;ww>|8Өl =+Wbĉ2d)\c ׭[=zxԩO_xݻ^z) Lu=ƙ^y8f}.ַZgjqU@=m0}{~ji?jm>v;ۍnݨ52T1۳gO#h*b:x/̮۴i?bذa #,я~|J2Խ d'rϗh_w,>P̴FqL/ Ϊ)?jENQ(ԡwZ +-%8K- z0[^[g6?mGJSe|_!Mm]wINO~mz郞Ewo2dV ~Ie ɛ׆Y_q|^anì}3"Zy}^U#7 k$LzEV!YYپjMRq1UEF9E;EV*O>K> ][.6yNt\FV{@R|RE@?PW2;Mר}cZ j؃/t^p7O;ǡ<8Umxja6MyC "FqeH S(noC2=HVlYCf_>WhECB>WJgQamYѾjgs6M|VwwA/)ÆY_ǰXρFq5 @oWݐ?%J@wOU|dJO%4kD>95*Cdve-ϟum "/D޽Qh/2qHŦ~ezVUeC2RTݨ JϕtK>y.RdS @5u5⤓N®]ǚ5k6(~).̙3'x"V^mfzdggPu=;z䦓 g6sT#z_bY/Opы[pT_(zw%}IHϳl [G&tW.s/E}Ķj꺪w大Jyu23$MPR^Ffnߩ7.,t'.VN' ^C\wvТת(zyh0E'0뫁5^AT;k%_@^ (NIF IOy)$1~4ZymSU_r%+M5*WygPeU+ʢB"+w9zc\(epJ*2vi,)zR Fφ}VZ%+k< jY?ikd8U=gAگ/PӗFaiȖ0{FD|<: 3Rg.T' 23Eg*FL6z%JVz83 11hG4ܽ&+!R[ 榝"+?Af1hcpir ȲuGq |4 Me|Ro6?. ա^d;m< ȭڿ>htPAm֙gބ|t+_5E wz+tƍ7ވ/<ٳPuV:}䙋e3::*}@yg bp+TWfQyPD/,ۖZ Y~#u)FQqL=57֛C~%}y~Vkoa=|f€Kg7 s7NdOsscM=*;ܤ}^ Im4No;M5#v67,Z9:yAݺ*xOM%k+;7ʐ`H驳|UנEb8ӈyN^ef|AnJ2PwTnYЗ8n LP8,BeXcuȨ@j~yе微Va< uȯxuraʶ[|kӻǼ|/H^D-U7kꑸ];`VcdRC4凞!mOdӿ?HFIxՓtQ@. Ā7C2T1B4\BraƗ8*ibZ:gY}vDnf 7n u)2Gcz@Dzzp$Hg;sM]TPjNluvXt)\}fM`xh5 Lأs Q :;.jmuVcV]OgY%TT}B%VҶ䷕je G_"}>ztԙ/:]85⥇DXWY)k\3Ԡ6bV xQͪeO݀l2{xބ @ Nfs}6Lgʨ>zpy^(|6+u0 ^ tTPBSCܫr߿[Ut:]_@;H.U??1/1fi|+F3HHHH !0%K>33tOad/Tәu!guI:SO=efu@VIu='I^ tD1cIFQ^_)ݦ&k혆9k?ԩSH֧׃ (Ux|up݈XO}Y3SN# B89cL;3fΜL6\ /HHHH2e3 ^uTSLR75Mst?o&ؾ}Y5'?1q< !  hڕβSKgٽlx RXbHHH'.|$   Ce(ǥp    鎺uG@KJJIgk:Ü`vZW^ =E͹? Cur]==NM)Xe:T8ҭ,a͚5Oq'&=Xo!D6Ey={%\bI 2ڸq#^{5ƍ^LF횀VZqSN1mnɻ ӟٲe {=\s5ꇝ\Mv?3f̀5])+{hZ6Q4v#?#=0asʭNghWя?7p}]ݻ?aϙG t#GDRR N0-N*[ thck?K:s'7Dg};}=& $RFbV{t-#H6R~z$Ё {a=oРAx饗ώnIOODzeˌqO>'/1+%,}ꩧmDMCiϨQ jyM7aOSah@"c+~Gq1?8 %N?zWUSf',9995ƥk׮06֭Yge}3_|=\uc/tךz:K $RN͕}\9ؚϒ:g17\"&RFPG\uh8g7G"e_VuM`gt3eHUgXn/lVC… F>Ձ{d'x"rss;>܃DHPe3/CjTUf8%BF*2#U̮Rס6>Ho6@kSyqm$ K̙ÍAfO,i$B>Z1x`sn:xee%iᓖhto%K[+tO}݂ΝsiH7יg TQӥ z.y娩sxDʩo/;vmMqlϒ~uT=MнZF Q2R*"SL1>f/O|e%VgIyHh',/0: nq82׉.3fBEMr~v Pvz:dQ}A<H|Zpiq7000.-WDhn3AtRS7͂u_ Ց+Vǣ}SeON,mYڵk_PC. u$.~21գ^Gau_' $RFZa3s_^}/ˇ);&vZ:Du;4nϋwo1;݁ZH:> AwtH|tp/w[ǔmluzL^;w.@M-Lt҂~gU[ήSݛ.6.}ͦí'ب^!Juj.***2# 7^eYO rUβ#c[|ͪoz])#_t_'ݯ^-DH[@N5i$ W}s=1lmݢ5" 6,Ax 7@䣆RݻsiX DGV &R>yRN??tH|ta]oncow!)//oPmʪR_\kYkt bvZ^xf~gBFz%{_R 4-d|-Lm!#}CeVNw^uT-+###,X [+XfetZ+8aC$Z|S: uy1HHH%X . !ДN=ꎌLX+ D.oHHHHHHHHHpg$@$@$@$@$@$@$@$@$A!        #C#Ý @"pHHHHHHHH  wJ$@$@$@$@$@$@$@$@h        82h;2Y+ D.oHHHHHHHHHpg$@$@$@$@$@$@$@$@$A(޼GM EK:D?>.BCojkzI$@$@$pʊQk7t='P*ݳʷ  hsfF]]}-uض~YWNƜcPjIN=w݆wW`" 9HqseQ\ӆK㢡 ~ROcicKkȺ|fN]*j򕬝ُoȉ1}l?xZ iCp&L2OŤ4?[%E˦a#]^+mޭQ0ڏqSfbbX@Ks1UpdLh zuS+ƈ Ec0st**- @(^}UӌIXĸXYjfE1 wdcwb^XlE5Mqc\1Ulpx1U3n qrAX7뭧>k^ {ew fO_lS]1fOY#11(kÃ8m:ƝVSI}8^X=o R=pWD|NUxt9MS*z-?q-ݷ/ [D,)pِ<Κ*פ\lXx7o3gbU"b6Zޒ @G"!fԭ 䕸{ oGθ Sئn {p71%F4+_fe ϓhg9Ze)\Eͮױy(c57R,49sApٸW`I'}gOpc07Ip7{| }9 wL)B`{&&/^&WuXNTիyx^GUf-=j1sʊ0mܰذqER@J`̞+A |I%F ބ'GIm-IHHHdo9wwOEJ`[c Đ:IS1yX,{S}'ݒV?l؆!Rd Kev'qȏGlo܅qgZAA _5w?&.5{;K8P$Ȋ?Al;)hS=M`,x4۰ix#? /ڀ'=Yȸ,x'> ĕ#읅{vOqي5Ccpuz}5"3u[7b O_G (!mey\:HHPs+ƠB.P"#Š%QHz6F55Қ|N ڍrxm! y=¡VY :y|6 1.M|] /n6]HHd6.Ԣ ?xH&PrBD=mr1d|>nyNF/K\ĩ۲ @qUT[FXc )cYBƮrhּfWUMaif\t>5_fX xoeA3F߼yNe¡!W Ԋc[?[#(#MCb4ξk.}**%a=>5e-j=}XH7P4'19eU ܗ ]=  LCΑ8`{]ff4,@TJ^Z*Fy>٘,F}o(j*m8UIvmFi"d|yQ1y/O`T<1^U(lN#̩2C<" 첒bXc0Ptv{̔/Sr;, B,=rݍ-> @k gMꉜMiN C=8Y:Ttyԋ$Hʬ}1G1\+]VsINQMd$k`)$*dp4oKݣ8=]Cahc&ۗG`QHқA =JBJ\/.K?U3}TF"sc;k݇|Yޒ @G!! u#G-2RffBOFs04RSef2SdSǗ}ڴ_$ɢRY>%#M͹>#Us߫$:rz6REyY1 \`l[g 7xX><#=\c8yVlOLE񬱘}j,7*\ތYz}csdIMJi]#TH`#/A,*DգCE^"X8S+P[Lx}S3 IDAT19(; S/a:9Y} 0ro )HHH }&ׇEW>Y|*Cx n,B,]+ݕofX궮}k|=XJƝ1XtĤ)r kX!Qu|t;秨Օq$fWJD>9J4:9d2YΚľ~)jEqߝ#E d'Hퟏm2bY*7^1h-!±YbZ+695sv, q-7Y";Dm3be3`,s9"xX:yYLqT #Au7ӱԁ2\Zx_Y=ulŦ!1OSWp.w#F::νE?b]&l{CHHH ARqV,]-,Ɂբ0XN߮u,}zRWYk|OZwՔ2XJ2xdmslpYq#qwz9)adzE߳]>f.cec<$h"xp=2\36NA[BR]rH-ityU'{M77. 8gBTÑ3dF=!i6n>زw& tt$ѤYhJ#-5لi'5G_ȎGYރH9irbhhOGDqMuk/bó"U'I?K#ᛀO vG k؍ hZO1$@$@$nT! M'ڨ!1u1dN#m̶-F/3|> TQ.ӏxR_~£yu5 @s:9Z'   vFNHNNFSJ]&Xe8 GfNkJK_#`HHHr޳iiiQ%    hCUUUcۧO%'5H    #J_]] #   XP`tꚦO猺%[HHHٳ<{=e;Y]   v92)9 ԘܻEC; #]+ = L:Φ;b`$@$@$@$bz޲2tLsuh$$@$@$@$pANSʝtgp!   h-n]R]]ct~p;.Hl|x:]+be @$%ɒl6)*sz:.8:    O̢SNLTKIIF:Ufr:5E9yM$@$@$@$ 0׳GW5ҩ]FWIHHHڌt]vA.] e^prr. /}u8.. @=Lve nՀݻ=jjjt+ԈWV^mY" @ c;5jᯬD}!nSGJ$yc֗4ʩ:cf @ x<;I )gZ]    hs`k.I">NO]WW[xtޓ -t誮ȐekUU5|Ted?׶4    >9'~nZ򚔜TI{髧A ֯ fIS^vNb~    dހ8 utIHHH0X:]Č:͗2;8m<}    C ~ZȖ X7Dxsq^ iwpgȵ s @[Qg ;F=> @ ugs~b $@$@$@$@$@$@$@$@$@ u@g5Lzt8^ 1ԩ-fl[^s 4G&aY۽霾x " q6is5 03Њ`.         أN vhwVh8xM$@$@$@$@$@$@$@$@$rK_ p\rA$@$@$@$@$@$@$@$@HqZy]]] >p"ʘΝDZZ{ "ƚ54) 8'T~06.ő tfԩQԀ꫋0tN@۷> aN        K |g{cìNÜ4dh'       <RF9S SiML׼Ss<,K$@$@$@$@$@$@$@$@Q]F6g5n nѾcÝ۶mÇ~qK"P4rפ%.qEMM :[  k=HHHHHHHHS03꒒̙F95ЩaiS2^)'0FaÆ+0pJ_`;^yk+ .ݒ9M__ǖݦ~z=e fwGż]b;99ñ]F_x=~zg3YY ~l[@E)tt9  KzgN Ex>N)EޛH!       ‡IXLmdWMQP}٫pqs޹*/=l*øh9^ N>ɨ$pu! yryvm O >{y-_(^/vs>c,zɉ(Y+{}4 \nj |GAcFA̓ /g#pl0$@$@$@$@$@$@$@$@@=9給Qj56pކ >gӇJ5&_#5T~JC-pta|'eGQYOb8lo ̟`ŭg_WK~ .o(6>gi}=Tx}8kD,k>">MCjr*|]q%d@/pRTS~jf\5k=2Α'M$w9/a7KYE:Rup;ۡt$@$@$@$@$@$@$@$@@xbpױJ6kbʕ~_1Nbӳ ӈu+l2C0&xポa &]CCI | 1`IL~lnK صMښu NN:lLrqɹR1kpz_c\8a\5cw 0vm_ #        !`:FzlF+{LV ӴhP`݁-x]8^{m3'{_t=2|;]@R]J~ʶ?$ (tM Wvq~㤾ZY]Ѕ˓cr:XpxKNҵcxi?ᴬ1^Pt?} tnPF(-INK.AJAٓO>i;qL3i߱IcZwL*?+OQw_8ni*u@_d^ڗon:~=N4nΝަ]H4Nfs ygH=)Ȓx\0eN#        K b w霆;^{m؈g uLP{~=ל*j8,㿇/eW@m]-׿wy/X/P@}m iQw18k?~G],5CvRp9[!473;~o _,ճ_ Pw`;^ l?f,~qV`$9b曲uHP^q,|} q6'       \g5eSTv6:ók.#[]  <{ a>8XgZ|3spO~K 1' Y٬/_UrߥK:Reڮ%߉~(z,ZP›/</NPnPY-?{˸1mۡ> @&"Dц4ţa֩QiϞ=]^AmT0P>JX]]Z˛> @ <&8`aW:        HF:;,h#[HHHHHHHH@NMbfrk ˚> @$ݶb wUkg1L<˕> @#`fԹEpG3鬱RYґ t^P6c)#JJJB]]]gxyDS 0 { log.Printf("Initializing quota (can take a while)...") qm, err := quota.New(server.Path, server.MaxRepoSize) if err != nil { return nil, err } server.quotaManager = qm log.Printf("Quota initialized, currently using %.2f GiB", float64(qm.SpaceUsed())/GiB) } mux := http.NewServeMux() if server.Prometheus { if server.PrometheusNoAuth { mux.Handle("/metrics", promhttp.Handler()) } else { mux.HandleFunc("/metrics", server.wrapMetricsAuth(promhttp.Handler().ServeHTTP)) } } mux.Handle("/", server) var handler http.Handler = mux if server.Debug { handler = server.debugHandler(handler) } if server.Log != "" { handler = server.logHandler(handler) } return handler, nil } rest-server-0.13.0/quota/000077500000000000000000000000001465071536600152225ustar00rootroot00000000000000rest-server-0.13.0/quota/quota.go000066400000000000000000000066561465071536600167170ustar00rootroot00000000000000package quota import ( "fmt" "io" "net/http" "os" "path/filepath" "strconv" "sync/atomic" ) // New creates a new quota Manager for given path. // It will tally the current disk usage before returning. func New(path string, maxSize int64) (*Manager, error) { m := &Manager{ path: path, maxRepoSize: maxSize, } if err := m.updateSize(); err != nil { return nil, err } return m, nil } // Manager manages the repo quota for given filesystem root path, including subrepos type Manager struct { path string maxRepoSize int64 repoSize int64 // must be accessed using sync/atomic } // WrapWriter limits the number of bytes written // to the space that is currently available as given by // the server's MaxRepoSize. This type is safe for use // by multiple goroutines sharing the same *Server. type maxSizeWriter struct { io.Writer m *Manager } func (w maxSizeWriter) Write(p []byte) (n int, err error) { if int64(len(p)) > w.m.SpaceRemaining() { return 0, fmt.Errorf("repository has reached maximum size (%d bytes)", w.m.maxRepoSize) } n, err = w.Writer.Write(p) w.m.IncUsage(int64(n)) return n, err } func (m *Manager) updateSize() error { // if we haven't yet computed the size of the repo, do so now initialSize, err := tallySize(m.path) if err != nil { return err } atomic.StoreInt64(&m.repoSize, initialSize) return nil } // WrapWriter wraps w in a writer that enforces s.MaxRepoSize. // If there is an error, a status code and the error are returned. func (m *Manager) WrapWriter(req *http.Request, w io.Writer) (io.Writer, int, error) { currentSize := atomic.LoadInt64(&m.repoSize) // if content-length is set and is trustworthy, we can save some time // and issue a polite error if it declares a size that's too big; since // we expect the vast majority of clients will be honest, so this check // can only help save time if contentLenStr := req.Header.Get("Content-Length"); contentLenStr != "" { contentLen, err := strconv.ParseInt(contentLenStr, 10, 64) if err != nil { return nil, http.StatusLengthRequired, err } if currentSize+contentLen > m.maxRepoSize { err := fmt.Errorf("incoming blob (%d bytes) would exceed maximum size of repository (%d bytes)", contentLen, m.maxRepoSize) return nil, http.StatusInsufficientStorage, err } } // since we can't always trust content-length, we will wrap the writer // in a custom writer that enforces the size limit during writes return maxSizeWriter{Writer: w, m: m}, 0, nil } // SpaceRemaining returns how much space is available in the repo // according to s.MaxRepoSize. s.repoSize must already be set. // If there is no limit, -1 is returned. func (m *Manager) SpaceRemaining() int64 { if m.maxRepoSize == 0 { return -1 } maxSize := m.maxRepoSize currentSize := atomic.LoadInt64(&m.repoSize) return maxSize - currentSize } // SpaceUsed returns how much space is used in the repo. func (m *Manager) SpaceUsed() int64 { return atomic.LoadInt64(&m.repoSize) } // IncUsage increments the current repo size (which // must already be initialized). func (m *Manager) IncUsage(by int64) { atomic.AddInt64(&m.repoSize, by) } // tallySize counts the size of the contents of path. func tallySize(path string) (int64, error) { if path == "" { path = "." } var size int64 err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { if err != nil { return err } size += info.Size() return nil }) return size, err } rest-server-0.13.0/repo/000077500000000000000000000000001465071536600150365ustar00rootroot00000000000000rest-server-0.13.0/repo/repo.go000066400000000000000000000506031465071536600163360ustar00rootroot00000000000000package repo import ( "encoding/hex" "encoding/json" "errors" "fmt" "io" "io/ioutil" "log" "math/rand" "net/http" "os" "path/filepath" "regexp" "runtime" "strconv" "strings" "sync" "syscall" "time" "github.com/minio/sha256-simd" "github.com/miolini/datacounter" "github.com/restic/rest-server/quota" ) // Options are options for the Handler accepted by New type Options struct { AppendOnly bool // if set, delete actions are not allowed Debug bool DirMode os.FileMode FileMode os.FileMode NoVerifyUpload bool // If set, we will panic when an internal server error happens. This // makes it easier to debug such errors. PanicOnError bool BlobMetricFunc BlobMetricFunc QuotaManager *quota.Manager FsyncWarning *sync.Once } // DefaultDirMode is the file mode used for directory creation if not // overridden in the Options const DefaultDirMode os.FileMode = 0700 // DefaultFileMode is the file mode used for file creation if not // overridden in the Options const DefaultFileMode os.FileMode = 0600 // New creates a new Handler for a single Restic backup repo. // path is the full filesystem path to this repo directory. // opt is a set of options. func New(path string, opt Options) (*Handler, error) { if path == "" { return nil, fmt.Errorf("path is required") } if opt.DirMode == 0 { opt.DirMode = DefaultDirMode } if opt.FileMode == 0 { opt.FileMode = DefaultFileMode } h := Handler{ path: path, opt: opt, } return &h, nil } // Handler handles all REST API requests for a single Restic backup repo // Spec: https://restic.readthedocs.io/en/latest/100_references.html#rest-backend type Handler struct { path string // filesystem path of repo opt Options } // httpDefaultError write a HTTP error with the default description func httpDefaultError(w http.ResponseWriter, code int) { http.Error(w, http.StatusText(code), code) } // httpMethodNotAllowed writes a 405 Method Not Allowed HTTP error with // the required Allow header listing the methods that are allowed. func httpMethodNotAllowed(w http.ResponseWriter, allowed []string) { w.Header().Set("Allow", strings.Join(allowed, ", ")) httpDefaultError(w, http.StatusMethodNotAllowed) } // errFileContentDoesntMatchHash is the error raised when the file content hash // doesn't match the hash provided in the URL var errFileContentDoesntMatchHash = errors.New("file content does not match hash") // BlobPathRE matches valid blob URI paths with optional object IDs var BlobPathRE = regexp.MustCompile(`^/(data|index|keys|locks|snapshots)/([0-9a-f]{64})?$`) // ObjectTypes are subdirs that are used for object storage var ObjectTypes = []string{"data", "index", "keys", "locks", "snapshots"} // FileTypes are files stored directly under the repo direct that are accessible // through a request var FileTypes = []string{"config"} func isHashed(objectType string) bool { return objectType == "data" } // BlobOperation describe the current blob operation in the BlobMetricFunc callback. type BlobOperation byte // Define all valid operations. const ( BlobRead = 'R' // A blob has been read BlobWrite = 'W' // A blob has been written BlobDelete = 'D' // A blob has been deleted ) // BlobMetricFunc is the callback signature for blob metrics. Such a callback // can be passed in the Options to keep track of various metrics. // objectType: one of ObjectTypes // operation: one of the BlobOperations above // nBytes: the number of bytes affected, or 0 if not relevant // TODO: Perhaps add http.Request for the username so that this can be cached? type BlobMetricFunc func(objectType string, operation BlobOperation, nBytes uint64) // ServeHTTP performs strict matching on the repo part of the URL path and // dispatches the request to the appropriate handler. func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { urlPath := r.URL.Path if urlPath == "/" { // TODO: add HEAD and GET switch r.Method { case "POST": h.createRepo(w, r) default: httpMethodNotAllowed(w, []string{"POST"}) } return } else if urlPath == "/config" { switch r.Method { case "HEAD": h.checkConfig(w, r) case "GET": h.getConfig(w, r) case "POST": h.saveConfig(w, r) case "DELETE": h.deleteConfig(w, r) default: httpMethodNotAllowed(w, []string{"HEAD", "GET", "POST", "DELETE"}) } return } else if objectType, objectID := h.getObject(urlPath); objectType != "" { if objectID == "" { // TODO: add HEAD switch r.Method { case "GET": h.listBlobs(w, r) default: httpMethodNotAllowed(w, []string{"GET"}) } return } switch r.Method { case "HEAD": h.checkBlob(w, r) case "GET": h.getBlob(w, r) case "POST": h.saveBlob(w, r) case "DELETE": h.deleteBlob(w, r) default: httpMethodNotAllowed(w, []string{"HEAD", "GET", "POST", "DELETE"}) } return } httpDefaultError(w, http.StatusNotFound) } // getObject parses the URL path and returns the objectType and objectID, // if any. The objectID is optional. func (h *Handler) getObject(urlPath string) (objectType, objectID string) { m := BlobPathRE.FindStringSubmatch(urlPath) if len(m) == 0 { return "", "" // no match } if len(m) == 2 || m[2] == "" { return m[1], "" // no objectID } return m[1], m[2] } // getSubPath returns the path for a file or subdir in the root of the repo. func (h *Handler) getSubPath(name string) string { return filepath.Join(h.path, name) } // getObjectPath returns the path for an object file in the repo. // The passed in objectType and objectID must be valid due to earlier validation func (h *Handler) getObjectPath(objectType, objectID string) string { // If we hit an error, this is a programming error, because all of these // must have been validated before. We still check them here as a safeguard. if objectType == "" || objectID == "" { panic("invalid objectType or objectID") } if isHashed(objectType) { if len(objectID) < 2 { // Should never happen, because BlobPathRE checked this panic("getObjectPath: objectID shorter than 2 chars") } // Added another dir in between with the first two characters of the hash return filepath.Join(h.path, objectType, objectID[:2], objectID) } return filepath.Join(h.path, objectType, objectID) } // sendMetric calls op.BlobMetricFunc if set. See its signature for details. func (h *Handler) sendMetric(objectType string, operation BlobOperation, nBytes uint64) { if f := h.opt.BlobMetricFunc; f != nil { f(objectType, operation, nBytes) } } // needSize tells you if we need the file size for metrics of quota accounting func (h *Handler) needSize() bool { return h.opt.BlobMetricFunc != nil || h.opt.QuotaManager != nil } // incrementRepoSpaceUsage increments the repo space usage if quota are enabled func (h *Handler) incrementRepoSpaceUsage(by int64) { if h.opt.QuotaManager != nil { h.opt.QuotaManager.IncUsage(by) } } // wrapFileWriter wraps the file writer if repo quota are enabled, and returns it // as is if not. // If an error occurs, it returns both an error and the appropriate HTTP error code. func (h *Handler) wrapFileWriter(r *http.Request, w io.Writer) (io.Writer, int, error) { if h.opt.QuotaManager == nil { return w, 0, nil // unmodified } return h.opt.QuotaManager.WrapWriter(r, w) } // checkConfig checks whether a configuration exists. func (h *Handler) checkConfig(w http.ResponseWriter, r *http.Request) { if h.opt.Debug { log.Println("checkConfig()") } cfg := h.getSubPath("config") st, err := os.Stat(cfg) if err != nil { h.fileAccessError(w, err) return } w.Header().Add("Content-Length", fmt.Sprint(st.Size())) } // getConfig allows for a config to be retrieved. func (h *Handler) getConfig(w http.ResponseWriter, r *http.Request) { if h.opt.Debug { log.Println("getConfig()") } cfg := h.getSubPath("config") bytes, err := ioutil.ReadFile(cfg) if err != nil { h.fileAccessError(w, err) return } _, _ = w.Write(bytes) } // saveConfig allows for a config to be saved. func (h *Handler) saveConfig(w http.ResponseWriter, r *http.Request) { if h.opt.Debug { log.Println("saveConfig()") } cfg := h.getSubPath("config") f, err := os.OpenFile(cfg, os.O_CREATE|os.O_WRONLY|os.O_EXCL, h.opt.FileMode) if err != nil && os.IsExist(err) { if h.opt.Debug { log.Print(err) } httpDefaultError(w, http.StatusForbidden) return } _, err = io.Copy(f, r.Body) if err != nil { h.internalServerError(w, err) return } err = f.Close() if err != nil { h.internalServerError(w, err) return } _ = r.Body.Close() } // deleteConfig removes a config. func (h *Handler) deleteConfig(w http.ResponseWriter, r *http.Request) { if h.opt.Debug { log.Println("deleteConfig()") } if h.opt.AppendOnly { httpDefaultError(w, http.StatusForbidden) return } cfg := h.getSubPath("config") if err := os.Remove(cfg); err != nil { // ignore not exist errors to make deleting idempotent, which is // necessary to properly handle request retries if !errors.Is(err, os.ErrNotExist) { h.fileAccessError(w, err) } return } } const ( mimeTypeAPIV1 = "application/vnd.x.restic.rest.v1" mimeTypeAPIV2 = "application/vnd.x.restic.rest.v2" ) // listBlobs lists all blobs of a given type in an arbitrary order. func (h *Handler) listBlobs(w http.ResponseWriter, r *http.Request) { if h.opt.Debug { log.Println("listBlobs()") } switch r.Header.Get("Accept") { case mimeTypeAPIV2: h.listBlobsV2(w, r) default: h.listBlobsV1(w, r) } } // listBlobsV1 lists all blobs of a given type in an arbitrary order. // TODO: unify listBlobsV1 and listBlobsV2 func (h *Handler) listBlobsV1(w http.ResponseWriter, r *http.Request) { if h.opt.Debug { log.Println("listBlobsV1()") } objectType, _ := h.getObject(r.URL.Path) if objectType == "" { h.internalServerError(w, fmt.Errorf( "cannot determine object type: %s", r.URL.Path)) return } path := h.getSubPath(objectType) items, err := ioutil.ReadDir(path) if err != nil { h.fileAccessError(w, err) return } names := []string{} for _, i := range items { if isHashed(objectType) { if !i.IsDir() { // ignore files in intermediate directories continue } subpath := filepath.Join(path, i.Name()) var subitems []os.FileInfo subitems, err = ioutil.ReadDir(subpath) if err != nil { h.fileAccessError(w, err) return } for _, f := range subitems { names = append(names, f.Name()) } } else { names = append(names, i.Name()) } } data, err := json.Marshal(names) if err != nil { h.internalServerError(w, err) return } w.Header().Set("Content-Type", mimeTypeAPIV1) _, _ = w.Write(data) } // Blob represents a single blob, its name and its size. type Blob struct { Name string `json:"name"` Size int64 `json:"size"` } // listBlobsV2 lists all blobs of a given type, together with their sizes, in an arbitrary order. // TODO: unify listBlobsV1 and listBlobsV2 func (h *Handler) listBlobsV2(w http.ResponseWriter, r *http.Request) { if h.opt.Debug { log.Println("listBlobsV2()") } objectType, _ := h.getObject(r.URL.Path) if objectType == "" { h.internalServerError(w, fmt.Errorf( "cannot determine object type: %s", r.URL.Path)) return } path := h.getSubPath(objectType) items, err := ioutil.ReadDir(path) if err != nil { h.fileAccessError(w, err) return } blobs := []Blob{} for _, i := range items { if isHashed(objectType) { if !i.IsDir() { // ignore files in intermediate directories continue } subpath := filepath.Join(path, i.Name()) var subitems []os.FileInfo subitems, err = ioutil.ReadDir(subpath) if err != nil { h.fileAccessError(w, err) return } for _, f := range subitems { blobs = append(blobs, Blob{Name: f.Name(), Size: f.Size()}) } } else { blobs = append(blobs, Blob{Name: i.Name(), Size: i.Size()}) } } data, err := json.Marshal(blobs) if err != nil { h.internalServerError(w, err) return } w.Header().Set("Content-Type", mimeTypeAPIV2) _, _ = w.Write(data) } // checkBlob tests whether a blob exists. func (h *Handler) checkBlob(w http.ResponseWriter, r *http.Request) { if h.opt.Debug { log.Println("checkBlob()") } objectType, objectID := h.getObject(r.URL.Path) if objectType == "" || objectID == "" { h.internalServerError(w, fmt.Errorf( "cannot determine object type or id: %s", r.URL.Path)) return } path := h.getObjectPath(objectType, objectID) st, err := os.Stat(path) if err != nil { h.fileAccessError(w, err) return } w.Header().Add("Content-Length", fmt.Sprint(st.Size())) } // getBlob retrieves a blob from the repository. func (h *Handler) getBlob(w http.ResponseWriter, r *http.Request) { if h.opt.Debug { log.Println("getBlob()") } objectType, objectID := h.getObject(r.URL.Path) if objectType == "" || objectID == "" { h.internalServerError(w, fmt.Errorf( "cannot determine object type or id: %s", r.URL.Path)) return } path := h.getObjectPath(objectType, objectID) file, err := os.Open(path) if err != nil { h.fileAccessError(w, err) return } wc := datacounter.NewResponseWriterCounter(w) http.ServeContent(wc, r, "", time.Unix(0, 0), file) if err = file.Close(); err != nil { h.internalServerError(w, err) return } h.sendMetric(objectType, BlobRead, wc.Count()) } // saveBlob saves a blob to the repository. func (h *Handler) saveBlob(w http.ResponseWriter, r *http.Request) { if h.opt.Debug { log.Println("saveBlob()") } objectType, objectID := h.getObject(r.URL.Path) if objectType == "" || objectID == "" { h.internalServerError(w, fmt.Errorf( "cannot determine object type or id: %s", r.URL.Path)) return } path := h.getObjectPath(objectType, objectID) _, err := os.Stat(path) if err == nil { httpDefaultError(w, http.StatusForbidden) return } if !os.IsNotExist(err) { h.internalServerError(w, err) return } tmpFn := filepath.Join(filepath.Dir(path), objectID+".rest-server-temp") tf, err := tempFile(tmpFn, h.opt.FileMode) if os.IsNotExist(err) { // the error is caused by a missing directory, create it and retry mkdirErr := os.MkdirAll(filepath.Dir(path), h.opt.DirMode) if mkdirErr != nil { log.Print(mkdirErr) } else { // try again tf, err = tempFile(tmpFn, h.opt.FileMode) } } if err != nil { h.internalServerError(w, err) return } // ensure this blob does not put us over the quota size limit (if there is one) outFile, errCode, err := h.wrapFileWriter(r, tf) if err != nil { if h.opt.Debug { log.Println(err) } httpDefaultError(w, errCode) return } var written int64 if h.opt.NoVerifyUpload { // just write the file without checking the contents written, err = io.Copy(outFile, r.Body) } else { // calculate hash for current request hasher := sha256.New() written, err = io.Copy(outFile, io.TeeReader(r.Body, hasher)) // reject if file content doesn't match file name if err == nil && hex.EncodeToString(hasher.Sum(nil)) != objectID { err = errFileContentDoesntMatchHash } } if err != nil { _ = tf.Close() _ = os.Remove(tf.Name()) h.incrementRepoSpaceUsage(-written) if h.opt.Debug { log.Print(err) } var pathError *os.PathError if errors.As(err, &pathError) && (pathError.Err == syscall.ENOSPC || pathError.Err == syscall.EDQUOT) { // The error is disk-related (no space left, no quota left), // notify the client using the correct HTTP status httpDefaultError(w, http.StatusInsufficientStorage) } else if errors.Is(err, errFileContentDoesntMatchHash) || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, http.ErrMissingBoundary) || errors.Is(err, http.ErrNotMultipart) { // The error is connection-related, send a client-side HTTP status httpDefaultError(w, http.StatusBadRequest) } else { // Otherwise we have a different internal error, reply with // server-side HTTP status h.internalServerError(w, err) } return } syncNotSup, err := syncFile(tf) if err != nil { _ = tf.Close() _ = os.Remove(tf.Name()) h.incrementRepoSpaceUsage(-written) h.internalServerError(w, err) return } if err := tf.Close(); err != nil { _ = os.Remove(tf.Name()) h.incrementRepoSpaceUsage(-written) h.internalServerError(w, err) return } if err := os.Rename(tf.Name(), path); err != nil { _ = os.Remove(tf.Name()) h.incrementRepoSpaceUsage(-written) h.internalServerError(w, err) return } if syncNotSup { h.opt.FsyncWarning.Do(func() { log.Print("WARNING: fsync is not supported by the data storage. This can lead to data loss, if the system crashes or the storage is unexpectedly disconnected.") }) } else { if err := syncDir(filepath.Dir(path)); err != nil { // Don't call os.Remove(path) as this is prone to race conditions with parallel upload retries h.internalServerError(w, err) return } } h.sendMetric(objectType, BlobWrite, uint64(written)) } // tempFile implements a custom version of ioutil.TempFile which allows modifying the file permissions func tempFile(fn string, perm os.FileMode) (f *os.File, err error) { for i := 0; i < 10; i++ { name := fn + strconv.FormatInt(rand.Int63(), 10) f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, perm) if os.IsExist(err) { continue } break } return } func syncFile(f *os.File) (bool, error) { err := f.Sync() // Ignore error if filesystem does not support fsync. syncNotSup := err != nil && (errors.Is(err, syscall.ENOTSUP) || isMacENOTTY(err)) if syncNotSup { err = nil } return syncNotSup, err } func syncDir(dirname string) error { if runtime.GOOS == "windows" { // syncing a directory is not possible on windows return nil } dir, err := os.Open(dirname) if err != nil { return err } err = dir.Sync() // Ignore error if filesystem does not support fsync. if errors.Is(err, syscall.ENOTSUP) || errors.Is(err, syscall.ENOENT) || errors.Is(err, syscall.EINVAL) { err = nil } if err != nil { _ = dir.Close() return err } return dir.Close() } // deleteBlob deletes a blob from the repository. func (h *Handler) deleteBlob(w http.ResponseWriter, r *http.Request) { if h.opt.Debug { log.Println("deleteBlob()") } objectType, objectID := h.getObject(r.URL.Path) if objectType == "" || objectID == "" { h.internalServerError(w, fmt.Errorf( "cannot determine object type or id: %s", r.URL.Path)) return } if h.opt.AppendOnly && objectType != "locks" { httpDefaultError(w, http.StatusForbidden) return } path := h.getObjectPath(objectType, objectID) var size int64 if h.needSize() { stat, err := os.Stat(path) if err == nil { size = stat.Size() } } if err := os.Remove(path); err != nil { // ignore not exist errors to make deleting idempotent, which is // necessary to properly handle request retries if !errors.Is(err, os.ErrNotExist) { h.fileAccessError(w, err) } return } h.incrementRepoSpaceUsage(-size) h.sendMetric(objectType, BlobDelete, uint64(size)) } // createRepo creates repository directories. func (h *Handler) createRepo(w http.ResponseWriter, r *http.Request) { if h.opt.Debug { log.Println("createRepo()") } if r.URL.Query().Get("create") != "true" { httpDefaultError(w, http.StatusBadRequest) return } log.Printf("Creating repository directories in %s\n", h.path) if err := os.MkdirAll(h.path, h.opt.DirMode); err != nil { h.internalServerError(w, err) return } for _, d := range ObjectTypes { if err := os.Mkdir(filepath.Join(h.path, d), h.opt.DirMode); err != nil && !os.IsExist(err) { h.internalServerError(w, err) return } } for i := 0; i < 256; i++ { dirPath := filepath.Join(h.path, "data", fmt.Sprintf("%02x", i)) if err := os.Mkdir(dirPath, h.opt.DirMode); err != nil && !os.IsExist(err) { h.internalServerError(w, err) return } } } // internalServerError is called to report an internal server error. // The error message will be reported in the server logs. If PanicOnError // is set, this will panic instead, which makes debugging easier. func (h *Handler) internalServerError(w http.ResponseWriter, err error) { log.Printf("ERROR: %v", err) if h.opt.PanicOnError { panic(fmt.Sprintf("internal server error: %v", err)) } httpDefaultError(w, http.StatusInternalServerError) } // internalServerError is called to report an error that occurred while // accessing a file. If the does not exist, the corresponding http status code // will be returned to the client. All other errors are passed on to // internalServerError func (h *Handler) fileAccessError(w http.ResponseWriter, err error) { if h.opt.Debug { log.Print(err) } if errors.Is(err, os.ErrNotExist) { httpDefaultError(w, http.StatusNotFound) } else { h.internalServerError(w, err) } } rest-server-0.13.0/repo/repo_unix.go000066400000000000000000000006411465071536600173760ustar00rootroot00000000000000//go:build !windows // +build !windows package repo import ( "errors" "runtime" "syscall" ) // The ExFAT driver on some versions of macOS can return ENOTTY, // "inappropriate ioctl for device", for fsync. // // https://github.com/restic/restic/issues/4016 // https://github.com/realm/realm-core/issues/5789 func isMacENOTTY(err error) bool { return runtime.GOOS == "darwin" && errors.Is(err, syscall.ENOTTY) } rest-server-0.13.0/repo/repo_windows.go000066400000000000000000000001311465071536600200770ustar00rootroot00000000000000package repo // Windows is not macOS. func isMacENOTTY(err error) bool { return false }